PageSourceSearch

https://adenastevens.com/athletes

html adenastevens.com collected 2026-10-05 22:01:27 UTC 875,494 bytes, 19,736 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="f21s3uk4">
7  
8  <!-- Primary Meta Tags -->
9  <title>ATHLETES | People - Adena Stevens Photography</title>
10  <meta name="description" content="ATHLETES — People - Adena Stevens Photography.">
11  <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
12  <meta name="author" content="adenastevens.com">
13  <meta name="language" content="en">
14  
15  <!-- Canonical URL -->
16  <link rel="canonical" href="https://adenastevens.com/athletes">
17  
18  <!-- Favicon -->
19  
20  
21  
22  
23  
24  <!-- Open Graph / Facebook / iMessage / Discord -->
25  <meta property="og:type" content="article">
26  <meta property="og:url" content="https://adenastevens.com/athletes">
27  <meta property="og:title" content="ATHLETES | People - Adena Stevens Photography">
28  <meta property="og:description" content="ATHLETES — People - Adena Stevens Photography.">
29  <meta property="og:site_name" content="adenastevens.com">
30  <meta property="og:locale" content="en_US">
31  <meta property="og:image" content="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg">
32  <meta property="og:image:secure_url" content="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg">
33  <meta property="og:image:width" content="1200">
34  <meta property="og:image:height" content="630">
35  <meta property="og:image:type" content="image/jpeg">
36  <meta property="og:image:alt" content="ATHLETES | People - Adena Stevens Photography">
37  
38  <!-- Twitter / X Card -->
39  <meta name="twitter:card" content="summary_large_image">
40  <meta name="twitter:site" content="@adenastevens.com">
41  <meta name="twitter:url" content="https://adenastevens.com/athletes">
42  <meta name="twitter:title" content="ATHLETES | People - Adena Stevens Photography">
43  <meta name="twitter:description" content="ATHLETES — People - Adena Stevens Photography.">
44  <meta name="twitter:image" content="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg">
45  <meta name="twitter:image:alt" content="ATHLETES | People - Adena Stevens Photography">
46  
47  <!-- Additional SEO Meta Tags -->
48  <meta name="theme-color" content="#000000">
49  <meta name="msapplication-TileColor" content="#000000">
50  <meta name="apple-mobile-web-app-capable" content="yes">
51  <meta name="apple-mobile-web-app-status-bar-style" content="default">
52  <meta name="apple-mobile-web-app-title" content="ATHLETES | People - Adena Stevens Photography">
53  
54  <!-- Security Headers -->
55  <meta http-equiv="X-Content-Type-Options" content="nosniff">
56  <meta http-equiv="X-XSS-Protection" content="1; mode=block">
57  <style>
58  /* FOUC gate: opacity on <html> cannot be pierced by child visibility:!important (submenu CSS) */
59  html:not(.hydra-ready) { opacity: 0 !important; }
60  /* Critical: hide nested menus before menu-styles.css arrives */
61  li:not(.expanded) > ol.nested-sortable { display: none !important; }
62  .hydra-seo-nav { position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0; }
63  .hydra-seo-nav ul { list-style:none;margin:0;padding:0; }
64  .hydra-skip-link { position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden; }
65  </style>
66  <noscript><style>html { opacity: 1 !important; }</style></noscript>
67  
67<script type="application/ld+json">
68  {
69  "@context": "https://schema.org",
70  "@type": [
71    "WebPage",
72    "ImageGallery"
73  ],
74  "name": "ATHLETES | People - Adena Stevens Photography",
75  "description": "ATHLETES — People - Adena Stevens Photography.",
76  "url": "https://adenastevens.com/athletes",
77  "datePublished": "2026-10-05T22:01:27.500Z",
78  "dateModified": "2026-10-05T22:01:27.500Z",
79  "inLanguage": "en-US",
80  "isPartOf": {
81    "@type": "WebSite",
82    "@id": "https://adenastevens.com/#website",
83    "name": "adenastevens.com",
84    "url": "https://adenastevens.com/",
85    "description": "ATHLETES — People - Adena Stevens Photography.",
86    "publisher": {
87      "@type": "Organization",
88      "name": "adenastevens.com",
89      "url": "https://adenastevens.com/"
90    }
91  },
92  "breadcrumb": {
93    "@type": "BreadcrumbList",
94    "itemListElement": [
95      {
96        "@type": "ListItem",
97        "position": 1,
98        "name": "Home",
99        "item": "https://adenastevens.com/"
100      },
101      {
102        "@type": "ListItem",
103        "position": 2,
104        "name": "ATHLETES | People - Adena Stevens Photography",
105        "item": "https://adenastevens.com/athletes"
106      }
107    ]
108  },
109  "associatedMedia": [
110    {
111      "@type": "ImageObject",
112      "name": "f21s3uk4 an5m6dbk jdi7x8 Athletes 001 basketballA",
113      "description": "",
114      "caption": "f21s3uk4 an5m6dbk jdi7x8 Athletes 001 basketballA",
115      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg"
116    },
117    {
118      "@type": "ImageObject",
119      "name": "f21s3uk4 an5m6dbk 8bymb5 Athletes 002 stevens 018",
120      "description": "",
121      "caption": "f21s3uk4 an5m6dbk 8bymb5 Athletes 002 stevens 018",
122      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_8bymb5_Athletes-002_stevens_018.jpg"
123    },
124    {
125      "@type": "ImageObject",
126      "name": "f21s3uk4 an5m6dbk kbdx46 Athletes 003 hsgsoc099d 1920w",
127      "description": "",
128      "caption": "f21s3uk4 an5m6dbk kbdx46 Athletes 003 hsgsoc099d 1920w",
129      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_kbdx46_Athletes-003_hsgsoc099d-1920w.jpg"
130    },
131    {
132      "@type": "ImageObject",
133      "name": "f21s3uk4 an5m6dbk ggcqhp Athletes 004 boys soccer 1920w",
134      "description": "",
135      "caption": "f21s3uk4 an5m6dbk ggcqhp Athletes 004 boys soccer 1920w",
136      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ggcqhp_Athletes-004_boys_soccer-1920w.jpg"
137    },
138    {
139      "@type": "ImageObject",
140      "name": "f21s3uk4 an5m6dbk a9ajqg Athletes 005 sports04",
141      "description": "",
142      "caption": "f21s3uk4 an5m6dbk a9ajqg Athletes 005 sports04",
143      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_a9ajqg_Athletes-005_sports04.jpg"
144    },
145    {
146      "@type": "ImageObject",
147      "name": "f21s3uk4 an5m6dbk 40o8tu Athletes 006 softball2 1920w",
148      "description": "",
149      "caption": "f21s3uk4 an5m6dbk 40o8tu Athletes 006 softball2 1920w",
150      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_40o8tu_Athletes-006_softball2-1920w.jpg"
151    },
152    {
153      "@type": "ImageObject",
154      "name": "f21s3uk4 an5m6dbk pfew5a Athletes 007 basketball3",
155      "description": "",
156      "caption": "f21s3uk4 an5m6dbk pfew5a Athletes 007 basketball3",
157      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_pfew5a_Athletes-007_basketball3.jpg"
158    },
159    {
160      "@type": "ImageObject",
161      "name": "f21s3uk4 an5m6dbk qy7wn9 Athletes 008 college basketball 01",
162      "description": "",
163      "caption": "f21s3uk4 an5m6dbk qy7wn9 Athletes 008 college basketball 01",
164      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_qy7wn9_Athletes-008_college-basketball-01.jpg"
165    },
166    {
167      "@type": "ImageObject",
168      "name": "f21s3uk4 an5m6dbk ds305y Athletes 010 sports07",
169      "description": "",
170      "caption": "f21s3uk4 an5m6dbk ds305y Athletes 010 sports07",
171      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ds305y_Athletes-010_sports07.jpg"
172    },
173    {
174      "@type": "ImageObject",
175      "name": "f21s3uk4 an5m6dbk vbj1zs Athletes 011 volleyballA 1920w",
176      "description": "",
177      "caption": "f21s3uk4 an5m6dbk vbj1zs Athletes 011 volleyballA 1920w",
178      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_vbj1zs_Athletes-011_volleyballA-1920w.jpg"
179    },
180    {
181      "@type": "ImageObject",
182      "name": "f21s3uk4 an5m6dbk 3yl151 Athletes 012 baseball3",
183      "description": "",
184      "caption": "f21s3uk4 an5m6dbk 3yl151 Athletes 012 baseball3",
185      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_3yl151_Athletes-012_baseball3.jpg"
186    },
187    {
188      "@type": "ImageObject",
189      "name": "f21s3uk4 an5m6dbk 60t74z Athletes 013 volleyball 1920w",
190      "description": "",
191      "caption": "f21s3uk4 an5m6dbk 60t74z Athletes 013 volleyball 1920w",
192      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_60t74z_Athletes-013_volleyball-1920w.jpg"
193    },
194    {
195      "@type": "ImageObject",
196      "name": "f21s3uk4 an5m6dbk f2yfb3 Athletes 014 golfAS0531a 1920w",
197      "description": "",
198      "caption": "f21s3uk4 an5m6dbk f2yfb3 Athletes 014 golfAS0531a 1920w",
199      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_f2yfb3_Athletes-014_golfAS0531a-1920w.jpg"
200    },
201    {
202      "@type": "ImageObject",
203      "name": "f21s3uk4 an5m6dbk o2kuzl Athletes 015 christine caron",
204      "description": "",
205      "caption": "f21s3uk4 an5m6dbk o2kuzl Athletes 015 christine caron",
206      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_o2kuzl_Athletes-015_christine_caron.jpg"
207    },
208    {
209      "@type": "ImageObject",
210      "name": "f21s3uk4 an5m6dbk r6n52d Athletes 016 softball3",
211      "description": "",
212      "caption": "f21s3uk4 an5m6dbk r6n52d Athletes 016 softball3",
213      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_r6n52d_Athletes-016_softball3.jpg"
214    },
215    {
216      "@type": "ImageObject",
217      "name": "f21s3uk4 an5m6dbk udxl72 Athletes 017 stevens 033",
218      "description": "",
219      "caption": "f21s3uk4 an5m6dbk udxl72 Athletes 017 stevens 033",
220      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_udxl72_Athletes-017_stevens_033.jpg"
221    },
222    {
223      "@type": "ImageObject",
224      "name": "f21s3uk4 an5m6dbk trbiqh Athletes 018 row2 1920w",
225      "description": "",
226      "caption": "f21s3uk4 an5m6dbk trbiqh Athletes 018 row2 1920w",
227      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_trbiqh_Athletes-018_row2-1920w.jpg"
228    },
229    {
230      "@type": "ImageObject",
231      "name": "f21s3uk4 an5m6dbk 2f1ypc Athletes 019 fencing 1920w",
232      "description": "",
233      "caption": "f21s3uk4 an5m6dbk 2f1ypc Athletes 019 fencing 1920w",
234      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_2f1ypc_Athletes-019_fencing-1920w.jpg"
235    },
236    {
237      "@type": "ImageObject",
238      "name": "f21s3uk4 an5m6dbk hhiwjj Athletes 020 cheer01 1920w",
239      "description": "",
240      "caption": "f21s3uk4 an5m6dbk hhiwjj Athletes 020 cheer01 1920w",
241      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_hhiwjj_Athletes-020_cheer01-1920w.jpg"
242    },
243    {
244      "@type": "ImageObject",
245      "name": "f21s3uk4 an5m6dbk f2upki Athletes 021 julian hightower",
246      "description": "",
247      "caption": "f21s3uk4 an5m6dbk f2upki Athletes 021 julian hightower",
248      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_f2upki_Athletes-021_julian_hightower.jpg"
249    },
250    {
251      "@type": "ImageObject",
252      "name": "f21s3uk4 an5m6dbk 82az38 Athletes 022 softball 1920w",
253      "description": "",
254      "caption": "f21s3uk4 an5m6dbk 82az38 Athletes 022 softball 1920w",
255      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_82az38_Athletes-022_softball-1920w.jpg"
256    },
257    {
258      "@type": "ImageObject",
259      "name": "f21s3uk4 an5m6dbk kvy9i5 Athletes 023 volleyball2 1920w",
260      "description": "",
261      "caption": "f21s3uk4 an5m6dbk kvy9i5 Athletes 023 volleyball2 1920w",
262      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_kvy9i5_Athletes-023_volleyball2-1920w.jpg"
263    },
264    {
265      "@type": "ImageObject",
266      "name": "f21s3uk4 an5m6dbk bstl5x Athletes 024 sports01 1920w",
267      "description": "",
268      "caption": "f21s3uk4 an5m6dbk bstl5x Athletes 024 sports01 1920w",
269      "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_bstl5x_Athletes-024_sports01-1920w.jpg"
270    },
271    {
272      "@type": "ImageObject",
273      "name": "f21s3uk4 an5m6dbk 2oajxn basketball portrait",
274      "description": "Dothan Bardichev",
275      "caption": "Dothan Bardichev",
276      "contentUrl": "https://storage.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_2oajxn_basketball-portrait.jpg"
277    }
278  ],
279  "numberOfItems": 24,
280  "primaryImageOfPage": {
281    "@type": "ImageObject",
282    "url": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg",
283    "width": 1200,
284    "height": 630,
285    "caption": "ATHLETES | People - Adena Stevens Photography"
286  },
287  "image": {
288    "@type": "ImageObject",
289    "url": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg",
290    "width": 1200,
291    "height": 630,
292    "caption": "ATHLETES | People - Adena Stevens Photography"
293  }
294}
295  </script>
295
296  
297  
298  <!-- Preview mode helper script -->
299  
299<script>
300    // Immediately execute to patch request methods before any other script runs
301    (function() {
302      // Detect preview mode by checking hostname
303      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
304      
305      if (isPreviewMode) {
306        console.log("[Preview Helper] Preview mode detected");
307        
308        // Extract GUID from URL path (first segment)
309        const pathParts = window.location.pathname.split('/').filter(Boolean);
310        const previewGuid = pathParts.length > 0 ? pathParts[0] : '';
311        
312        if (!previewGuid) {
313          console.warn("[Preview Helper] No GUID found in path:", window.location.pathname);
314          return; // Exit if no GUID found
315        }
316        
317        console.log("[Preview Helper] Using GUID:", previewGuid);
318        
319        // Store these in window for other scripts
320        window.isPreviewMode = true;
321        window.previewGuid = previewGuid;
322        window.isHydraEnvironment = true;
323        window.Parameters = window.Parameters || {};
324        window.Parameters.isHydra = true;
325        
326        // Helper function that adds the GUID to API URLs and relative links
327        window.getApiUrl = function(endpoint) {
328          // For relative paths
329          if (endpoint.startsWith('/api/')) {
330            const newUrl = '/' + previewGuid + endpoint;
331            console.log("[Preview Helper] Transformed API URL:", endpoint, "→", newUrl);
332            return newUrl;
333          }
334          return endpoint;
335        };
336        
337        // Helper function that adds the GUID to any relative link
338        window.getPreviewUrl = function(url) {
339          // For relative paths (starts with /) that aren't already prefixed with GUID
340          if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
341            const newUrl = '/' + previewGuid + url;
342            console.log("[Preview Helper] Transformed relative link:", url, "→", newUrl);
343            return newUrl;
344          }
345          return url;
346        };
347        
348        // PATCH FETCH API with enhanced URL handling
349        const originalFetch = window.fetch;
350        window.fetch = function(resource, init) {
351          // Only transform string URLs
352          if (typeof resource === 'string') {
353            // Case 1: Handle relative paths starting with /api/
354            if (resource.startsWith('/api/')) {
355              const newUrl = window.getApiUrl(resource);
356              console.log("[Preview Helper] Patched fetch call (API):", resource, "→", newUrl);
357              return originalFetch(newUrl, init);
358            }
359            
360            // Case 2: Handle other relative paths (for content/gallery links)
361            else if (resource.startsWith('/') && !resource.startsWith('/' + previewGuid + '/')) {
362              const newUrl = window.getPreviewUrl(resource);
363              console.log("[Preview Helper] Patched fetch call (relative):", resource, "→", newUrl);
364              return originalFetch(newUrl, init);
365            }
366            
367            // Case 3: Handle absolute URLs to the same domain containing /api/
368            else if (resource.includes(window.location.host) && resource.includes('/api/')) {
369              try {
370                const urlObj = new URL(resource);
371                if (urlObj.hostname === window.location.hostname) {
372                  const path = urlObj.pathname;
373                  // Already has /{guid}/api/... (e.g. from new URL(getApiUrl(...), origin)) — do not double-prefix
374                  if (path.startsWith('/' + previewGuid + '/')) {
375                    return originalFetch(resource, init);
376                  }
377                  const newPath = '/' + previewGuid + path;
378                  urlObj.pathname = newPath;
379                  const newUrl = urlObj.toString();
380                  console.log("[Preview Helper] Patched fetch call (absolute API):", resource, "→", newUrl);
381                  return originalFetch(newUrl, init);
382                }
383              } catch (e) {
384                console.error("[Preview Helper] Error processing URL:", e);
385              }
386            }
387            
388            // Case 4: Handle absolute URLs to the same domain (non-API)
389            else if (resource.includes(window.location.host)) {
390              try {
391                const urlObj = new URL(resource);
392                if (urlObj.hostname === window.location.hostname && 
393                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
394                  const path = urlObj.pathname;
395                  const newPath = '/' + previewGuid + path;
396                  urlObj.pathname = newPath;
397                  const newUrl = urlObj.toString();
398                  console.log("[Preview Helper] Patched fetch call (absolute):", resource, "→", newUrl);
399                  return originalFetch(newUrl, init);
400                }
401              } catch (e) {
402                console.error("[Preview Helper] Error processing URL:", e);
403              }
404            }
405          }
406          return originalFetch(resource, init);
407        };
408        console.log("[Preview Helper] Patched fetch API");
409        
410        // PATCH XMLHttpRequest with enhanced URL handling
411        const originalOpen = XMLHttpRequest.prototype.open;
412        XMLHttpRequest.prototype.open = function(method, url, ...rest) {
413          // Only transform string URLs
414          if (typeof url === 'string') {
415            // Case 1: Handle relative paths starting with /api/
416            if (url.startsWith('/api/')) {
417              const newUrl = window.getApiUrl(url);
418              console.log("[Preview Helper] Patched XMLHttpRequest (API):", url, "→", newUrl);
419              return originalOpen.call(this, method, newUrl, ...rest);
420            }
421            
422            // Case 2: Handle other relative paths
423            else if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
424              const newUrl = window.getPreviewUrl(url);
425              console.log("[Preview Helper] Patched XMLHttpRequest (relative):", url, "→", newUrl);
426              return originalOpen.call(this, method, newUrl, ...rest);
427            }
428            
429            // Case 3: Handle absolute URLs to the same domain containing /api/
430            else if (url.includes(window.location.host) && url.includes('/api/')) {
431              try {
432                const urlObj = new URL(url);
433                if (urlObj.hostname === window.location.hostname) {
434                  const path = urlObj.pathname;
435                  if (path.startsWith('/' + previewGuid + '/')) {
436                    return originalOpen.call(this, method, url, ...rest);
437                  }
438                  const newPath = '/' + previewGuid + path;
439                  urlObj.pathname = newPath;
440                  const newUrl = urlObj.toString();
441                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute API):", url, "→", newUrl);
442                  return originalOpen.call(this, method, newUrl, ...rest);
443                }
444              } catch (e) {
445                console.error("[Preview Helper] Error processing URL:", e);
446              }
447            }
448            
449            // Case 4: Handle absolute URLs to the same domain (non-API)
450            else if (url.includes(window.location.host)) {
451              try {
452                const urlObj = new URL(url);
453                if (urlObj.hostname === window.location.hostname && 
454                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
455                  const path = urlObj.pathname;
456                  const newPath = '/' + previewGuid + path;
457                  urlObj.pathname = newPath;
458                  const newUrl = urlObj.toString();
459                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute):", url, "→", newUrl);
460                  return originalOpen.call(this, method, newUrl, ...rest);
461                }
462              } catch (e) {
463                console.error("[Preview Helper] Error processing URL:", e);
464              }
465            }
466          }
467          return originalOpen.call(this, method, url, ...rest);
468        };
469        console.log("[Preview Helper] Patched XMLHttpRequest");
470        
471        // Note: Link click handling is done by the existing gallery link interceptor
472        // The fetch and XMLHttpRequest patches above handle programmatic requests
473        
474        // Add debugging aid
475        window.debugPreviewUrls = function() {
476          return {
477            hostname: window.location.hostname,
478            pathname: window.location.pathname,
479            guid: window.previewGuid,
480            isPreviewMode: window.isPreviewMode,
481            sampleTransformations: {
482              relativeApi: window.getApiUrl('/api/menu-styles'),
483              relativeLink: window.getPreviewUrl('/gallery'),
484              absolute: new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/')
485            }
486          };
487        };
488        
489        // Test the transformation immediately
490        console.log("[Preview Helper] Test API transformation:", window.getApiUrl('/api/menu-styles'));
491        console.log("[Preview Helper] Test relative link transformation:", window.getPreviewUrl('/gallery'));
492        console.log("[Preview Helper] Test absolute URL transformation:", 
493          new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/'));
494      }
495    })();
496  </script>
496
497  
498  
499  
500  
501<!-- Early CDN retry helpers: MUST run before CDN script/link tags.
502     Changing script.src after a load error is unreliable (esp. Firefox defer);
503     always insert a new element pointing at /cdn-proxy/. -->
504<script>
505(function() {
506  window.useCdnProxy = window.useCdnProxy || false;
507  window.__hydraCdnRetryLog = window.__hydraCdnRetryLog || [];
508  function toProxy(url) {
509    if (!url || url.indexOf('cdn.neonsky.app') === -1) return url;
510    return url.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
511  }
512  function noteRetry(kind, fromUrl, toUrl) {
513    try {
514      window.useCdnProxy = true;
515      var entry = { kind: kind, from: fromUrl, to: toUrl, t: Date.now() };
516      window.__hydraCdnRetryLog.push(entry);
517      console.warn('🔄 CDN failed, retrying via new element:', kind, fromUrl, '->', toUrl);
518    } catch (e) {}
519  }
520  window.__hydraRetryCdnScript = function(el) {
521    try {
522      if (!el || el.getAttribute('data-retried') === 'true') return;
523      var original = el.src || '';
524      if (original.indexOf('cdn.neonsky.app') === -1) return;
525      el.setAttribute('data-retried', 'true');
526      el.onerror = null;
527      var proxyUrl = toProxy(original);
528      noteRetry('script', original, proxyUrl);
529      var s = document.createElement('script');
530      s.src = proxyUrl;
531      s.setAttribute('data-retried', 'true');
532      if (el.id) s.id = el.id;
533      // Dynamic scripts ignore defer; omit it so the proxy copy executes on load.
534      if (el.parentNode) {
535        el.parentNode.insertBefore(s, el.nextSibling);
536      } else {
537        (document.head || document.documentElement).appendChild(s);
538      }
539    } catch (e) {}
540  };
541  window.__hydraRetryCdnLink = function(el) {
542    try {
543      if (!el || el.getAttribute('data-retried') === 'true') return;
544      var original = el.href || '';
545      if (original.indexOf('cdn.neonsky.app') === -1) return;
546      el.setAttribute('data-retried', 'true');
547      el.onerror = null;
548      var proxyUrl = toProxy(original);
549      noteRetry('link', original, proxyUrl);
550      var link = document.createElement('link');
551      link.rel = el.rel || 'stylesheet';
552      if (el.id) link.id = el.id;
553      link.href = proxyUrl;
554      link.setAttribute('data-retried', 'true');
555      if (el.parentNode) {
556        el.parentNode.insertBefore(link, el.nextSibling);
557      } else {
558        (document.head || document.documentElement).appendChild(link);
559      }
560    } catch (e) {}
561  };
562})();
563</script>
563
564<link rel="stylesheet" href="https://cdn.neonsky.app/menu-styles-v260923-002.css?t=1791237687500" onerror="window.__hydraRetryCdnLink(this)">
565<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
vendor: 1 bytes, line 565
565
566<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
vendor: 1 bytes, line 566
566
567<script src="https://cdn.jsdelivr.net/npm/magic-sdk"></script>
vendor: 1 bytes, line 567
567
568<script src="https://cdn.jsdelivr.net/npm/@magic-ext/auth"></script>
vendor: 1 bytes, line 568
568
569<script src="https://cdn.neonsky.app/style-customizer-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
569
570<link rel="stylesheet" href="https://cdn.neonsky.app/palette-explorer-v260923-002.css?t=1791237687500" onerror="window.__hydraRetryCdnLink(this)">
571<script src="https://cdn.neonsky.app/palette-explorer-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 571
571
572<script src="https://cdn.neonsky.app/sidebar-manager-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 572
572
573<script src="https://cdn.neonsky.app/image-processor-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 573
573
574<script src="https://cdn.neonsky.app/image-uploader-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 574
574
575<script src="https://cdn.neonsky.app/page-layout-presets-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 575
575
576<script src="https://cdn.neonsky.app/site-graph-presets-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 576
576
577<script src="https://cdn.neonsky.app/site-graph-sync-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 577
577
578<script src="https://cdn.neonsky.app/page-manager-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
578
579<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
580<link rel="stylesheet" href="https://cdn.neonsky.app/page-styles-v260923-002.css?t=1791237687500" onerror="window.__hydraRetryCdnLink(this)">
581
582
583
584<!-- CDN Fallback Handler for CSS and JS files -->
585<script>
586(function() {
587  // Global flag: Set to true when any CDN resource fails and uses proxy
588  window.useCdnProxy = window.useCdnProxy || false;
589  
590  // Global base URL function: Returns proxy base if flag is set, otherwise CDN base
591  window.getCdnBaseUrl = function() {
592    return window.useCdnProxy 
593      ? window.location.origin + '/cdn-proxy/'
594      : 'https://cdn.neonsky.app/';
595  };
596  
597  // Utility function to convert CDN URLs to proxy URLs
598  window.convertCdnToProxyUrl = function(cdnUrl) {
599    if (!cdnUrl || !cdnUrl.includes('cdn.neonsky.app')) return cdnUrl;
600    return cdnUrl.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
601  };
602  
603  // Utility function to construct CDN URLs (uses proxy if flag is set)
604  window.buildCdnUrl = function(path) {
605    return window.getCdnBaseUrl() + path;
606  };
607  
608  // Resource tracking for debugging
609  window._resourceTracking = window._resourceTracking || {
610    resources: [],
611    proxyFlagChanges: [],
612    imageLoads: [],
613    networkErrors: []
614  };
615  
616  // Track when proxy flag changes - use a wrapper function
617  let proxyFlagChangeCount = 0;
618  let _useCdnProxyValue = window.useCdnProxy || false;
619  
620  // Create a setter function that tracks changes
621  window._setUseCdnProxy = function(newValue, reason) {
622    const oldValue = _useCdnProxyValue;
623    if (oldValue !== newValue) {
624      proxyFlagChangeCount++;
625      const changeInfo = {
626        timestamp: new Date().toISOString(),
627        changeNumber: proxyFlagChangeCount,
628        oldValue: oldValue,
629        newValue: newValue,
630        reason: reason || 'Unknown',
631        stack: new Error().stack?.split('\n').slice(1, 6).join('\n') || 'No stack'
632      };
633      window._resourceTracking.proxyFlagChanges.push(changeInfo);
634      console.error('🔍 PROXY_FLAG_CHANGE:', JSON.stringify(changeInfo, null, 2));
635      // Keep only last 50 changes
636      if (window._resourceTracking.proxyFlagChanges.length > 50) {
637        window._resourceTracking.proxyFlagChanges.shift();
638      }
639      _useCdnProxyValue = newValue;
640      window.useCdnProxy = newValue;
641    }
642  };
643  
644  // Track resource loading
645  function trackResource(url, type, status, details) {
646    const entry = {
647      timestamp: new Date().toISOString(),
648      url: url,
649      type: type, // 'script', 'link', 'image', etc.
650      status: status, // 'loading', 'success', 'failed', 'proxy', 'storage'
651      details: details || {},
652      useCdnProxy: window.useCdnProxy
653    };
654    window._resourceTracking.resources.push(entry);
655    console.error('📊 RESOURCE_TRACK:', JSON.stringify(entry, null, 2));
656    // Keep only last 200 resources
657    if (window._resourceTracking.resources.length > 200) {
658      window._resourceTracking.resources.shift();
659    }
660  }
661  
662  // Track image loads
663  function trackImageLoad(url, status, source) {
664    const entry = {
665      timestamp: new Date().toISOString(),
666      url: url,
667      status: status, // 'loading', 'success', 'failed'
668      source: source, // 'cdn', 'proxy', 'storage'
669      useCdnProxy: window.useCdnProxy
670    };
671    window._resourceTracking.imageLoads.push(entry);
672    console.error('🖼️ IMAGE_LOAD:', JSON.stringify(entry, null, 2));
673    // Keep only last 100 image loads
674    if (window._resourceTracking.imageLoads.length > 100) {
675      window._resourceTracking.imageLoads.shift();
676    }
677  }
678  
679  // Track network errors
680  function trackNetworkError(url, error, context) {
681    const entry = {
682      timestamp: new Date().toISOString(),
683      url: url,
684      error: {
685        message: error?.message || 'Unknown error',
686        name: error?.name || 'Unknown',
687        type: error?.type || 'unknown',
688        toString: String(error)
689      },
690      context: context || {},
691      useCdnProxy: window.useCdnProxy
692    };
693    window._resourceTracking.networkErrors.push(entry);
694    console.error('❌ NETWORK_ERROR:', JSON.stringify(entry, null, 2));
695    // Keep only last 100 errors
696    if (window._resourceTracking.networkErrors.length > 100) {
697      window._resourceTracking.networkErrors.shift();
698    }
699  }
700  
701  // Expose tracking functions globally
702  window.trackResource = trackResource;
703  window.trackImageLoad = trackImageLoad;
704  window.trackNetworkError = trackNetworkError;
705  
706  // Function to retry failed resources via proxy
707  // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag
708  // The proxy can handle both network errors and 404s, so it's safer to err on the side of using it
709  function retryViaProxy(element, originalSrc, errorEvent) {
710    if (!originalSrc) originalSrc = element.href || element.src;
711    if (!originalSrc || !originalSrc.includes('cdn.neonsky.app')) return;
712    
713    // Prevent multiple retries
714    if (element.getAttribute('data-retried')) {
715      return;
716    }
717    element.setAttribute('data-retried', 'true');
718    
719    // Check if element still exists in DOM
720    if (!element.parentNode) {
721      console.warn('⚠️ Element no longer in DOM, skipping proxy retry:', originalSrc);
722      return;
723    }
724    
725    // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s
726    // We'll be aggressive and set the flag for any CDN failure to ensure images also use proxy
727    // Check error message if available, but default to assuming network error
728    const hasExplicitNetworkError = errorEvent && errorEvent.message && (
729      errorEvent.message.includes('Failed to fetch') ||
730      errorEvent.message.includes('SSL') ||
731      errorEvent.message.includes('secure connection') ||
732      errorEvent.message.includes('network') ||
733      errorEvent.message.includes('ERR_') ||
734      errorEvent.message.includes('SSL_ERROR')
735    );
736    
737    // Track the error
738    trackNetworkError(originalSrc, errorEvent, {
739      elementType: element.tagName,
740      hasExplicitNetworkError: hasExplicitNetworkError,
741      errorMessage: errorEvent?.message || 'No error message',
742      errorName: errorEvent?.name || 'No error name'
743    });
744    
745    // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure)
746    // This covers ISP-affected clients where SSL handshake fails before HTTP request
747    const reason = hasExplicitNetworkError 
748      ? 'Explicit network/SSL error detected'
749      : (!errorEvent || !errorEvent.message)
750        ? 'Browser silent failure (likely SSL/network)'
751        : 'CDN resource failed (using proxy as fallback)';
752    
753    if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) {
754      window._setUseCdnProxy(true, reason);
755      console.error('🚩 CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests');
756      console.error('   Error details:', errorEvent ? (errorEvent.message || 'No error message (likely SSL/network failure)') : 'No error event');
757    } else {
758      // Even for other errors, set the flag to be safe - proxy can handle 404s too
759      window._setUseCdnProxy(true, reason);
760      console.error('🚩 CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback');
761    }
762    
763    const proxyUrl = originalSrc.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
764    console.error('🔄 CDN failed, retrying via proxy:', originalSrc, '->', proxyUrl);
765    
766    // Track the retry
767    trackResource(originalSrc, element.tagName.toLowerCase(), 'failed', {
768      retryUrl: proxyUrl,
769      error: errorEvent?.message || 'Unknown'
770    });
771    
772    if (element.tagName === 'SCRIPT') {
773      const newScript = document.createElement('script');
774      newScript.src = proxyUrl;
775      // Do not copy defer/async onto dynamically inserted scripts. After a CDN
776      // failure, DOMContentLoaded may already have fired; deferred dynamic
777      // scripts can fail to execute (seen on Firefox ISP/CDN-fail clients).
778      newScript.setAttribute('data-retried', 'true');
779      newScript.onload = function() {
780        trackResource(proxyUrl, 'script', 'proxy-success', { originalUrl: originalSrc });
781      };
782      newScript.onerror = function() {
783        trackResource(proxyUrl, 'script', 'proxy-failed', { originalUrl: originalSrc });
784        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
785        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
786        trackResource(storageUrl, 'script', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
787        newScript.src = storageUrl;
788        newScript.onload = function() {
789          trackResource(storageUrl, 'script', 'storage-success', { originalUrl: originalSrc });
790        };
791      };
792      element.parentNode.replaceChild(newScript, element);
793    } else if (element.tagName === 'LINK') {
794      const newLink = document.createElement('link');
795      newLink.rel = element.rel;
796      newLink.href = proxyUrl;
797      newLink.setAttribute('data-retried', 'true');
798      newLink.onload = function() {
799        trackResource(proxyUrl, 'link', 'proxy-success', { originalUrl: originalSrc });
800      };
801      newLink.onerror = function() {
802        trackResource(proxyUrl, 'link', 'proxy-failed', { originalUrl: originalSrc });
803        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
804        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
805        trackResource(storageUrl, 'link', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
806        newLink.href = storageUrl;
807        newLink.onload = function() {
808          trackResource(storageUrl, 'link', 'storage-success', { originalUrl: originalSrc });
809        };
810      };
811      element.parentNode.replaceChild(newLink, element);
812    }
813  }
814  
815  // Monitor script and link tags for errors
816  document.addEventListener('error', function(e) {
817    const target = e.target;
818    if ((target.tagName === 'SCRIPT' || target.tagName === 'LINK') && 
819        target.src && target.src.includes('cdn.neonsky.app')) {
820      retryViaProxy(target, target.src, e);
821    } else if (target.tagName === 'LINK' && target.href && target.href.includes('cdn.neonsky.app')) {
822      retryViaProxy(target, target.href, e);
823    }
824  }, true);
825  
826  // Also check existing scripts/links that may have failed before this script loaded
827  // For ISP-affected clients, we assume CDN failures are network/SSL errors
828  setTimeout(function() {
829    document.querySelectorAll('script[src*="cdn.neonsky.app"], link[href*="cdn.neonsky.app"]').forEach(function(el) {
830      if (el.tagName === 'SCRIPT' && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
831        el.addEventListener('error', function(errorEvent) {
832          // For ISP-affected clients, assume any CDN failure is a network/SSL error
833          // The proxy can handle both network errors and 404s
834          retryViaProxy(el, el.src, errorEvent);
835        });
836      } else if (el.tagName === 'LINK' && !el.sheet && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
837        // CSS link failed if it has no stylesheet - check after a delay
838        setTimeout(function() {
839          if (!el.sheet && !el.getAttribute('data-retried')) {
840            // For ISP-affected clients, assume any CDN failure is a network/SSL error
841            retryViaProxy(el, el.href, null);
842          }
843        }, 2000);
844      }
845    });
846  }, 100);
847})();
848</script>
848
849
850<!-- Log Capture System - S+J to download logs (works even if gallery doesn't load) -->
851<script>
852(function() {
853  'use strict';
854  
855  // Only initialize if not already initialized
856  if (window._indexLogCaptureInitialized) {
857    return;
858  }
859  window._indexLogCaptureInitialized = true;
860  
861  // Wait for DOM to be ready
862  function initLogCapture() {
863    // Store original console methods
864    const originalConsole = {
865      log: console.log,
866      warn: console.warn,
867      error: console.error,
868      info: console.info
869    };
870
871    // Array to store all console messages
872    window._capturedLogs = window._capturedLogs || [];
873
874    // Override console methods to capture logs
875    ['log', 'warn', 'error', 'info'].forEach(method => {
876      console[method] = function(...args) {
877        // Call original method
878        originalConsole[method].apply(console, args);
879        
880        // Capture the log with timestamp
881        window._capturedLogs.push({
882          timestamp: new Date().toISOString(),
883          level: method.toUpperCase(),
884          message: args.map(function(arg) {
885            if (arg !== null && arg !== undefined && Object(arg) === arg) {
886              try {
887                return JSON.stringify(arg, null, 2);
888              } catch (e) {
889                return String(arg);
890              }
891            }
892            return String(arg);
893          }).join(' ')
894        });
895        
896        // Keep only last 2000 logs to prevent memory issues
897        if (window._capturedLogs.length > 2000) {
898          window._capturedLogs.shift();
899        }
900      };
901    });
902
903    // Add keyboard shortcut to download logs (S+J)
904    let pressedKeys = new Set();
905    
906    document.addEventListener('keydown', function(event) {
907      const key = (event.key || '').toLowerCase();
908      
909      if (key === 's' || key === 'j') {
910        pressedKeys.add(key);
911        
912        // Check if both S and J are pressed
913        if (pressedKeys.has('s') && pressedKeys.has('j')) {
914          event.preventDefault();
915          console.log('S+J detected - downloading logs...');
916          
917          // Show a brief visual indicator
918          const indicator = document.createElement('div');
919          indicator.style.cssText = 
920            'position: fixed; top: 20px; right: 20px; z-index: 10000; ' +
921            'background: #4CAF50; color: white; padding: 10px 20px; ' +
922            'border-radius: 5px; font-family: Arial, sans-serif; ' +
923            'font-size: 14px; font-weight: bold; ' +
924            'box-shadow: 0 2px 10px rgba(0,0,0,0.3);';
925          indicator.textContent = '📥 Downloading logs...';
926          document.body.appendChild(indicator);
927          
928          setTimeout(() => {
929            if (indicator.parentNode) {
930              indicator.parentNode.removeChild(indicator);
931            }
932          }, 2000);
933          
934          downloadLogs();
935          // Clear the set after triggering
936          pressedKeys.clear();
937        }
938      }
939    });
940    
941    document.addEventListener('keyup', function(event) {
942      const key = (event.key || '').toLowerCase();
943      if (key === 's' || key === 'j') {
944        pressedKeys.delete(key);
945      }
946    });
947
948    console.log('📋 Log capture system initialized. Press S+J simultaneously to download logs.');
949  }
950
951  // Download captured logs as a text file
952  function downloadLogs() {
953    try {
954      // Get all scripts and links, categorized
955      const allScripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
956      const allLinks = Array.from(document.querySelectorAll('link[href]')).map(l => l.href);
957      
958      // Get additional debug info
959      const debugInfo = {
960        url: window.location.href,
961        userAgent: navigator.userAgent,
962        timestamp: new Date().toISOString(),
963        useCdnProxy: window.useCdnProxy || false,
964        windowLocation: window.location.toString(),
965        documentReadyState: document.readyState,
966        // All scripts/links (not just CDN)
967        allScriptsLoaded: allScripts,
968        allLinksLoaded: allLinks,
969        // CDN-specific (for backward compatibility and CDN debugging)
970        scriptsLoaded: allScripts.filter(src => src.includes('cdn.neonsky.app')),
971        linksLoaded: allLinks.filter(href => href.includes('cdn.neonsky.app')),
972        // Gallery scripts (loaded from Worker)
973        galleryScriptsLoaded: allScripts.filter(src => src.includes('/gallery-scripts/')),
974        galleryLinksLoaded: allLinks.filter(href => href.includes('/gallery-scripts/')),
975        // Failed resources
976        failedScripts: Array.from(document.querySelectorAll('script[src*="cdn.neonsky.app"][data-retried]')).map(s => s.src),
977        failedLinks: Array.from(document.querySelectorAll('link[href*="cdn.neonsky.app"][data-retried]')).map(l => l.href),
978        // Proxy resources
979        proxyScriptsLoaded: allScripts.filter(src => src.includes('/cdn-proxy/')),
980        proxyLinksLoaded: allLinks.filter(href => href.includes('/cdn-proxy/')),
981        // Resource tracking data
982        resourceTracking: window._resourceTracking || {
983          resources: [],
984          proxyFlagChanges: [],
985          imageLoads: [],
986          networkErrors: []
987        },
988        // Performance timing
989        performanceTiming: performance.timing ? {
990          navigationStart: performance.timing.navigationStart,
991          domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
992          loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart,
993          domInteractive: performance.timing.domInteractive - performance.timing.navigationStart
994        } : null,
995        // Style / logo diagnostics (layout/color bug triage)
996        styleDiagnostics: (function() {
997          try {
998            var rootStyle = getComputedStyle(document.documentElement);
999            var logoNodes = Array.from(document.querySelectorAll('.sidebar img, .sidebar-element img, img[src*="robert-vizzini"], .horizontal-header-logo img'));
1000            return {
1001              siteBackgroundColor: rootStyle.getPropertyValue('--site-background-color').trim(),
1002              menuBackgroundColor: rootStyle.getPropertyValue('--menu-background-color').trim(),
1003              menuColor: rootStyle.getPropertyValue('--menu-color').trim(),
1004              inlineSiteBg: document.documentElement.style.getPropertyValue('--site-background-color'),
1005              inlineMenuBg: document.documentElement.style.getPropertyValue('--menu-background-color'),
1006              hasMenuStyleCustomizer: !!window.MenuStyleCustomizer,
1007              menuStyleCustomizerInitialized: !!(window.MenuStyleCustomizer && window.MenuStyleCustomizer._initialized),
1008              menuStyleSettingsBg: window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
1009                ? {
1010                    siteBackgroundColor: window.MenuStyleCustomizer.settings.siteBackgroundColor,
1011                    menuBackgroundColor: window.MenuStyleCustomizer.settings.menuBackgroundColor,
1012                    color: window.MenuStyleCustomizer.settings.color
1013                  }
1014                : null,
1015              hasSidebarManager: !!window.SidebarManager,
1016              sidebarElementCount: window.SidebarManager && window.SidebarManager.elements
1017                ? window.SidebarManager.elements.length
1018                : 0,
1019              hasPageManager: !!window.PageManager,
1020              pageManagerHasLoadPage: !!(window.PageManager && typeof window.PageManager.loadPage === 'function'),
1021              pageElementCount: (function() {
1022                try {
1023                  return document.querySelectorAll('.gallery-container .page-element, .page-container .page-element').length;
1024                } catch (e) { return -1; }
1025              })(),
1026              pageContainerHtmlLength: (function() {
1027                try {
1028                  var pc = document.querySelector('.gallery-container .page-container, .page-container');
1029                  return pc ? (pc.innerHTML || '').length : 0;
1030                } catch (e) { return -1; }
1031              })(),
1032              activeContentType: window._activeContentType || null,
1033              logoImageCount: logoNodes.length,
1034              logoImages: logoNodes.slice(0, 5).map(function(img) {
1035                return {
1036                  src: img.currentSrc || img.src || '',
1037                  naturalWidth: img.naturalWidth || 0,
1038                  display: getComputedStyle(img).display,
1039                  complete: !!img.complete
1040                };
1041              }),
1042              earlyCdnRetries: window.__hydraCdnRetryLog || []
1043            };
1044          } catch (e) {
1045            return { error: String(e && e.message ? e.message : e) };
1046          }
1047        })()
1048      };
1049
1050      // Create log content
1051      const logContent = [
1052        '=== INDEX.TS DEBUG LOGS ===',
1053        'Generated: ' + debugInfo.timestamp,
1054        'URL: ' + debugInfo.url,
1055        'User Agent: ' + debugInfo.userAgent,
1056        'useCdnProxy Flag: ' + debugInfo.useCdnProxy,
1057        'Document Ready State: ' + debugInfo.documentReadyState,
1058        '================================',
1059        'TIP: Search for [AUTH_SAVE] in CONSOLE LOGS below for authentication/save failure details.',
1060        'TIP: Search STYLE DIAGNOSTICS for layout/color/logo triage (backgrounds, MenuStyleCustomizer, SidebarManager, logo).',
1061        '================================',
1062        '',
1063        '=== DEBUG INFO ===',
1064        JSON.stringify(debugInfo, null, 2),
1065        '',
1066        '=== STYLE DIAGNOSTICS ===',
1067        JSON.stringify(debugInfo.styleDiagnostics, null, 2),
1068        '',
1069        '=== RESOURCE TRACKING SUMMARY ===',
1070        'Total Resources Tracked: ' + (debugInfo.resourceTracking.resources.length || 0),
1071        'Proxy Flag Changes: ' + (debugInfo.resourceTracking.proxyFlagChanges.length || 0),
1072        'Image Loads Tracked: ' + (debugInfo.resourceTracking.imageLoads.length || 0),
1073        'Network Errors Tracked: ' + (debugInfo.resourceTracking.networkErrors.length || 0),
1074        '',
1075        '=== PROXY FLAG CHANGES ===',
1076        JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2),
1077        '',
1078        '=== RESOURCE LOADING TRACKING ===',
1079        JSON.stringify(debugInfo.resourceTracking.resources, null, 2),
1080        '',
1081        '=== IMAGE LOADING TRACKING ===',
1082        JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2),
1083        '',
1084        '=== NETWORK ERRORS ===',
1085        JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2),
1086        '',
1087        '=== CONSOLE LOGS ===',
1088        ...(window._capturedLogs || []).map(log => 
1089          '[' + log.timestamp + '] ' + log.level + ': ' + log.message
1090        )
1091      ].join('\n');
1092
1093      // Create and download file
1094      const blob = new Blob([logContent], { type: 'text/plain' });
1095      const url = URL.createObjectURL(blob);
1096      const a = document.createElement('a');
1097      a.href = url;
1098      a.download = 'index-debug-logs-' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt';
1099      document.body.appendChild(a);
1100      a.click();
1101      document.body.removeChild(a);
1102      URL.revokeObjectURL(url);
1103
1104      console.log('✅ Debug logs downloaded successfully!');
1105    } catch (error) {
1106      console.error('❌ Failed to download logs:', error);
1107      alert('Failed to download logs. Error: ' + (error.message || 'Unknown error'));
1108    }
1109  }
1110
1111  // Initialize when DOM is ready
1112  if (document.readyState === 'loading') {
1113    document.addEventListener('DOMContentLoaded', initLogCapture);
1114  } else {
1115    // DOM already ready, initialize immediately
1116    setTimeout(initLogCapture, 100);
1117  }
1118})();
1119</script>
1119
1120
1121<!-- Add auth token if available -->
1122
1123
1124<style>
1125/* Override Pico.css body padding to prevent jumping between pages */
1126body {
1127  padding: 0 !important;
1128  margin: 0 !important;
1129  display: block !important;
1130}
1131
1132/* Scope Pico.css to only affect elements in edit mode */
1133:root:not([data-theme="pico"]) {
1134--pico-applied: dark;
1135}
1136
1137/* Add data-theme="pico" to body when in edit mode */
1138body.edit-mode-active {
1139data-theme: dark;
1140}
1141
1142/* Custom styles to ensure Pico only affects editor UI, not content */
1143.sidebar.editing button,
1144.edit-controls button,
1145.element-header button,
1146.text-editor button,
1147.image-uploader button {
1148--pico-applied: dark;
1149}
1150
1151/* Hide default browser-styling when Pico is active */
1152[data-theme="pico"] button.btn {
1153appearance: dark;
1154-webkit-appearance: dark;
1155}
1156
1157/* Ensure content isn't affected by Pico styles */
1158.gallery-container,
1159.page-text,
1160.sidebar-text {
1161--pico-applied: none !important;
1162}
1163
1164/* Conflict Indicator Styles */
1165#conflictIndicator {
1166  background: #d32f2f !important;
1167  border: 1px solid #d32f2f !important;
1168  border-radius: 4px;
1169  padding: 8px;
1170  cursor: pointer;
1171  transition: opacity 0.2s;
1172  margin-left: 8px;
1173  display: flex; /* Use flex for centering, but hidden by default via inline style */
1174  align-items: center;
1175  justify-content: center;
1176}
1177
1178#conflictIndicator:hover {
1179  background: #e53935 !important;
1180  border-color: #e53935 !important;
1181  opacity: 0.9;
1182}
1183
1184#conflictIndicator svg {
1185  display: block;
1186  width: 24px;
1187  height: 24px;
1188  margin: 0 auto;
1189  color: white !important;
1190}
1191
1192/* Tooltip styles for conflict indicator - allow wrapping */
1193#tooltip-container .tooltip[data-tooltip-for="conflictIndicator"],
1194#tooltip-container .tooltip {
1195  max-width: 200px;
1196  word-wrap: break-word;
1197  white-space: normal;
1198  line-height: 1.4;
1199}
1200
1201.global-header-controls {
1202  display: flex;
1203  align-items: center;
1204  gap: 8px;
1205}
1206
1207.sidebar-header {
1208  display: flex;
1209  align-items: center;
1210  gap: 8px;
1211}
1212
1213/* Cookie Notice — paper UI (public-safe; does not depend on edit-mode pe-* scopes) */
1214.cookie-notice {
1215  --cn-paper: #f6f5f1;
1216  --cn-paper-elevated: #fdfcfa;
1217  --cn-ink: #2c2a27;
1218  --cn-ink-muted: #6b6760;
1219  --cn-ink-faint: #9b968d;
1220  --cn-rule: rgba(44, 42, 39, 0.1);
1221  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1222  --cn-shadow: 0 -8px 28px rgba(44, 42, 39, 0.1);
1223  --cn-radius: 3px;
1224  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1225
1226  position: fixed;
1227  bottom: 0;
1228  left: 0;
1229  right: 0;
1230  z-index: 10000;
1231  display: flex;
1232  align-items: center;
1233  justify-content: space-between;
1234  flex-wrap: wrap;
1235  gap: 0.85rem 1.25rem;
1236  padding: 1.1rem 1.35rem 1.2rem;
1237  font-family: var(--cn-font);
1238  font-size: 14px;
1239  line-height: 1.45;
1240  color: var(--cn-ink);
1241  background: var(--cn-paper-elevated);
1242  border-top: 1px solid var(--cn-rule);
1243  box-shadow: var(--cn-shadow);
1244}
1245
1246.cookie-notice.hidden {
1247  display: none;
1248}
1249
1250.cookie-notice-text {
1251  flex: 1 1 220px;
1252  min-width: 0;
1253  margin: 0;
1254  color: var(--cn-ink-muted);
1255  font-size: 13px;
1256}
1257
1258.cookie-notice-text a.cookie-notice-policy {
1259  color: var(--cn-ink);
1260  font-weight: 600;
1261  text-decoration: underline;
1262  text-underline-offset: 2px;
1263}
1264
1265.cookie-notice-text a.cookie-notice-policy:hover {
1266  color: var(--cn-ink-muted);
1267}
1268
1269.cookie-notice-buttons {
1270  display: flex;
1271  flex-wrap: wrap;
1272  align-items: center;
1273  justify-content: flex-end;
1274  gap: 0.5rem;
1275}
1276
1277.cookie-notice .pe-btn {
1278  display: inline-flex;
1279  align-items: center;
1280  justify-content: center;
1281  gap: 0.4rem;
1282  margin: 0;
1283  padding: 0.45rem 0.85rem;
1284  font-family: var(--cn-font);
1285  font-size: 11px;
1286  font-weight: 600;
1287  letter-spacing: 0.1em;
1288  text-transform: uppercase;
1289  color: var(--cn-ink-muted);
1290  background: var(--cn-paper);
1291  border: 1px solid var(--cn-rule-strong);
1292  border-radius: var(--cn-radius);
1293  box-shadow: none;
1294  cursor: pointer;
1295  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1296}
1297
1298.cookie-notice .pe-btn:hover {
1299  color: var(--cn-ink);
1300  background: var(--cn-paper-elevated);
1301  border-color: rgba(44, 42, 39, 0.22);
1302}
1303
1304.cookie-notice .pe-btn--primary {
1305  color: var(--cn-ink);
1306  background: var(--cn-paper);
1307  border-color: var(--cn-rule-strong);
1308}
1309
1310.cookie-notice .pe-btn--primary:hover {
1311  color: var(--cn-ink);
1312  background: var(--cn-paper-elevated);
1313  border-color: rgba(44, 42, 39, 0.22);
1314}
1315
1316#hydraDoNotSellLink {
1317  --cn-paper: #f6f5f1;
1318  --cn-paper-elevated: #fdfcfa;
1319  --cn-ink: #2c2a27;
1320  --cn-ink-muted: #6b6760;
1321  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1322  --cn-radius: 3px;
1323  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1324
1325  position: fixed;
1326  bottom: 10px;
1327  left: 12px;
1328  z-index: 9999;
1329  display: inline-flex;
1330  align-items: center;
1331  justify-content: center;
1332  margin: 0;
1333  padding: 0.45rem 0.85rem;
1334  font-family: var(--cn-font);
1335  font-size: 11px;
1336  font-weight: 600;
1337  letter-spacing: 0.1em;
1338  text-transform: uppercase;
1339  color: var(--cn-ink-muted);
1340  background: var(--cn-paper-elevated);
1341  border: 1px solid var(--cn-rule-strong);
1342  border-radius: var(--cn-radius);
1343  box-shadow: 0 2px 8px rgba(44, 42, 39, 0.06);
1344  cursor: pointer;
1345}
1346
1347#hydraDoNotSellLink.hidden {
1348  display: none;
1349}
1350
1351#hydraDoNotSellLink:hover {
1352  color: var(--cn-ink);
1353  background: var(--cn-paper);
1354  border-color: rgba(44, 42, 39, 0.22);
1355}
1356
1357/* FOUC reveal (paired with early <head> html:not(.hydra-ready) { opacity: 0 }) */
1358html.hydra-ready {
1359  opacity: 1;
1360  transition: opacity 0.2s ease;
1361}
1362/* Keep body class for any legacy selectors; visibility no longer used (submenu CSS pierces it) */
1363body.hydra-ready {
1364  visibility: visible;
1365}
1366
1367/* Admin panel typography */
1368.sidebar-element-form,
1369.sidebar-element-form *,
1370.add-form,
1371.add-form *,
1372.metadata-editor,
1373.metadata-editor * {
1374  font-family: "Hanken Grotesk", sans-serif;
1375  font-weight: 200;
1376  letter-spacing: 0.02em;
1377}
1378
1379.sidebar-element-form h3,
1380.sidebar-element-form label,
1381.add-form label,
1382.metadata-editor h3,
1383.metadata-editor label {
1384  text-transform: uppercase;
1385  letter-spacing: 0.04em;
1386  font-weight: 300;
1387}
1388
1389.sidebar-element-form .form-actions .btn,
1390.sidebar-element-form button,
1391.add-form .form-actions .btn,
1392.add-form button,
1393.metadata-editor .form-actions .btn,
1394.metadata-editor button {
1395  text-transform: uppercase;
1396  letter-spacing: 0.08em;
1397  font-weight: 300;
1398}
1399
1400.sidebar-element-form input,
1401.sidebar-element-form select,
1402.sidebar-element-form textarea,
1403.sidebar-element-form button,
1404.add-form input,
1405.add-form select,
1406.add-form textarea,
1407.add-form button,
1408.metadata-editor input,
1409.metadata-editor select,
1410.metadata-editor textarea,
1411.metadata-editor button {
1412  font-family: "Hanken Grotesk", sans-serif;
1413  font-weight: 200;
1414  letter-spacing: 0.02em;
1415  border-radius: 0;
1416}
1417</style>
1418
1419<script>
1420// Flag to prevent duplicate initialization
1421window.hydraInitialized = false;
1422
1423function ensureAdminPanelFont() {
1424  const head = document.head || document.getElementsByTagName('head')[0];
1425  if (!head) {
1426    return;
1427  }
1428
1429  const linkDefinitions = [
1430    {
1431      id: 'admin-panels-font-preconnect-googleapis',
1432      rel: 'preconnect',
1433      href: 'https://fonts.googleapis.com'
1434    },
1435    {
1436      id: 'admin-panels-font-preconnect-gstatic',
1437      rel: 'preconnect',
1438      href: 'https://fonts.gstatic.com',
1439      crossOrigin: 'anonymous'
1440    },
1441    {
1442      id: 'admin-panels-font-stylesheet',
1443      rel: 'stylesheet',
1444      href: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,200;1,200&display=swap'
1445    },
1446    {
1447      id: 'admin-panels-font-barlow',
1448      rel: 'stylesheet',
1449      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'
1450    }
1451  ];
1452
1453  for (const definition of linkDefinitions) {
1454    if (!document.getElementById(definition.id)) {
1455      const link = document.createElement('link');
1456      link.id = definition.id;
1457      link.rel = definition.rel;
1458      link.href = definition.href;
1459
1460      if (definition.crossOrigin) {
1461        link.crossOrigin = definition.crossOrigin;
1462      }
1463
1464      head.appendChild(link);
1465    }
1466  }
1467}
1468
1469ensureAdminPanelFont();
1470
1471 window.Parameters = window.Parameters || {};
1472  window.Parameters.siteId = "f21s3uk4";
1473  window.Parameters.SiteId = "f21s3uk4"; // Also set capitalized version
1474  window.siteId = "f21s3uk4";
1475  window.TURNSTILE_SITE_KEY = "0x4AAAAAADyHXlv6RTUIaDOp";
1476  window.NEON_SKY_FILES = window.NEON_SKY_FILES || {};
1477  window.NEON_SKY_FILES.QUILL_INTEGRATION = 'quill-integration-v260923-002.js';
1478  window.NEON_SKY_FILES.SITE_THEMES = 'site-themes-v260923-002.css';
1479  window.NEON_SKY_FILES.SITE_THEMES_MANIFEST = 'site-themes-v260923-002.json';
1480
1481// Initialize menu styles from server
1482document.addEventListener('DOMContentLoaded', function() {
1483// Apply initial menu styles from server
1484const initialMenuStyles = {"fontFamily":"'Hanken Grotesk', sans-serif","fontSize":"16px","color":"#333","activeColor":"#8f8f8f","hoverColor":"#9e9e9e","verticalSpacing":"2px","menuBackgroundColor":"#ffffff","menuBackgroundHidden":false,"siteBackgroundColor":"#ffffff","siteBackgroundUseGradient":false,"siteGradientRandomizeOnLoad":false,"artPaletteSelection":null,"menuBackgroundImageUrl":"","siteBackgroundImageUrl":"","mobileHeaderHeight":"180px","sidebarWidth":"15vw","sidebarPaddingTop":"0","sidebarPaddingRight":"0px","sidebarPaddingBottom":"0px","sidebarPaddingLeft":"0px","globalPagePaddingTopEnabled":false,"globalPagePaddingLeftEnabled":false,"globalPagePaddingRightEnabled":false,"globalPagePaddingBottomEnabled":false,"globalPagePaddingTop":"","globalPagePaddingLeft":"","globalPagePaddingRight":"","globalPagePaddingBottom":"","menuLayout":"horizontal","horizontalMenuAlignment":"center","horizontalHeaderLogoLayout":"stacked","horizontalHeaderShadow":false,"disableRightClick":false,"showCookieNotice":false,"cookieNoticeText":"This website uses cookies to ensure you get the best experience on our website.","privacyPolicyUrl":"","alignElementsAtBottom":false,"customCSS":"","customJS":"","siteThemeId":null,"siteThemeOptions":{},"fontName":"Hanken Grotesk"};
1485window.__hydraStyleFromQuery = null;
1486const initialSiteThemeRuntimeVars = null;
1487
1488// Set CSS custom properties
1489// ?hydra-style= preview: apply merged menuStyles (skip site-theme runtime vars that would override).
1490if (initialSiteThemeRuntimeVars && !window.__hydraStyleFromQuery) {
1491  Object.keys(initialSiteThemeRuntimeVars).forEach(function(key) {
1492    document.documentElement.style.setProperty(key, initialSiteThemeRuntimeVars[key]);
1493  });
1494  if (initialMenuStyles && initialMenuStyles.verticalSpacing) {
1495    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1496  }
1497  // Match menu chrome / Gallery Options overview preview (light weight)
1498  document.documentElement.style.setProperty('--menu-font-weight', '200');
1499  if (initialMenuStyles && typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1500    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1501  }
1502} else if (initialMenuStyles) {
1503  if (initialMenuStyles.fontFamily) {
1504    document.documentElement.style.setProperty('--menu-font-family', initialMenuStyles.fontFamily);
1505  }
1506  if (initialMenuStyles.fontSize) {
1507    document.documentElement.style.setProperty('--menu-font-size', initialMenuStyles.fontSize);
1508  }
1509  // Only set when Secondary was explicitly saved; otherwise descriptions follow --menu-font-size
1510  if (typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1511    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1512  }
1513  document.documentElement.style.setProperty('--menu-font-weight', '200');
1514  if (initialMenuStyles.color) {
1515    document.documentElement.style.setProperty('--menu-color', initialMenuStyles.color);
1516  }
1517  if (initialMenuStyles.activeColor) {
1518    document.documentElement.style.setProperty('--menu-active-color', initialMenuStyles.activeColor);
1519  }
1520  if (initialMenuStyles.hoverColor) {
1521    document.documentElement.style.setProperty('--menu-hover-color', initialMenuStyles.hoverColor);
1522  }
1523  // Background colors must be applied here too: MenuStyleCustomizer may load late/fail
1524  // after CDN→proxy retry, and CSS defaults are white/#fafafa.
1525  if (initialMenuStyles.siteBackgroundColor) {
1526    document.documentElement.style.setProperty('--site-background-color', initialMenuStyles.siteBackgroundColor);
1527  }
1528  if (initialMenuStyles.menuBackgroundColor && !initialMenuStyles.menuBackgroundHidden) {
1529    document.documentElement.style.setProperty('--menu-background-color', initialMenuStyles.menuBackgroundColor);
1530  }
1531  if (initialMenuStyles.verticalSpacing) {
1532    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1533  }
1534  if (initialMenuStyles.menuBackgroundImageUrl && String(initialMenuStyles.menuBackgroundImageUrl).trim() !== '') {
1535    var menuBgImg = String(initialMenuStyles.menuBackgroundImageUrl).trim();
1536    document.documentElement.style.setProperty('--menu-background-image', 'url(' + JSON.stringify(menuBgImg) + ')');
1537  }
1538  var resolvedSiteBg = '';
1539  if (window.SiteGradientGenerator && typeof window.SiteGradientGenerator.resolveSiteBackgroundImage === 'function') {
1540    resolvedSiteBg = window.SiteGradientGenerator.resolveSiteBackgroundImage(initialMenuStyles, { allowRandomize: true });
1541  } else if (initialMenuStyles.siteBackgroundImageUrl) {
1542    resolvedSiteBg = String(initialMenuStyles.siteBackgroundImageUrl).trim();
1543  }
1544  if (resolvedSiteBg) {
1545    var peIsCssGradient = String(resolvedSiteBg).toLowerCase().indexOf('gradient(') !== -1;
1546    var siteBgImgCss = peIsCssGradient
1547      ? resolvedSiteBg
1548      : 'url(' + JSON.stringify(resolvedSiteBg) + ')';
1549    document.documentElement.style.setProperty('--site-background-image', siteBgImgCss);
1550    var siteBgSize = 'cover';
1551    if (peIsCssGradient) {
1552      siteBgSize = window.SiteGradientGenerator && window.SiteGradientGenerator.gradientBackgroundSize
1553        ? window.SiteGradientGenerator.gradientBackgroundSize(resolvedSiteBg)
1554        : '100% 100%';
1555      document.body.classList.add('has-site-background-gradient');
1556    }
1557    document.documentElement.style.setProperty('--site-background-size', siteBgSize);
1558  }
1559  if (initialMenuStyles.mobileHeaderHeight && String(initialMenuStyles.mobileHeaderHeight).trim() !== '') {
1560    var mhh0 = String(initialMenuStyles.mobileHeaderHeight).trim();
1561    /* 0 / 0vh / 0px collapse logo max-height calcs; use the same floor as getUserHorizontalHeaderHeightPx */
1562    var mhhParsed = parseInt(mhh0, 10);
1563    if (Number.isNaN(mhhParsed) || mhhParsed < 1) {
1564      mhh0 = '60px';
1565    }
1566    document.documentElement.style.setProperty('--mobile-header-height', mh
1566h0);
1567    document.documentElement.style.setProperty('--content-margin-top', mhh0);
1568    document.documentElement.style.setProperty('--top-bar-height-setting', mhh0);
1569  }
1570}
1571
1572if (initialMenuStyles && initialMenuStyles.menuBackgroundHidden) {
1573  document.body.classList.add('menu-background-hidden');
1574  document.documentElement.style.setProperty('--menu-background-color', 'transparent');
1575  document.documentElement.style.setProperty('--menu-background-image', 'none');
1576}
1577
1578if (initialMenuStyles && initialMenuStyles.menuTextUppercase) {
1579  document.body.classList.add('menu-text-uppercase');
1580  if (!document.getElementById('menu-text-uppercase-style')) {
1581    var menuUpperStyle = document.createElement('style');
1582    menuUpperStyle.id = 'menu-text-uppercase-style';
1583    menuUpperStyle.textContent = '.menu-item,.horizontal-menu-label,a.horizontal-menu-label{text-transform:uppercase!important;font-weight:200!important;}';
1584    document.head.appendChild(menuUpperStyle);
1585  }
1586}
1587
1588// Store initial styles in MenuStyleCustomizer if it exists
1589if (window.MenuStyleCustomizer) {
1590  window.MenuStyleCustomizer.settings = {
1591    ...window.MenuStyleCustomizer.defaults,
1592    ...initialMenuStyles
1593  };
1594}
1595});
1596</script>
vendor: 1 bytes, line 1596
1596
1597<script>
1598let magic = null;
1599let userMetadata = null;
1600let isAuthenticated = false;
1601let isAdmin = false;
1602let didToken = null;
1603let siteId = "f21s3uk4";
1604
1605/** Clear Hydra auth tokens/flags from this browser (used by logout button and ?logout=1 links). */
1606function clearHydraClientAuthStorage() {
1607  try {
1608    localStorage.removeItem('hydra_is_admin');
1609    localStorage.removeItem('hydra_auth_token');
1610    localStorage.removeItem('hydra_auth_email');
1611    localStorage.removeItem('hydra_did_token');
1612    localStorage.removeItem('hydra_auth_site');
1613    localStorage.removeItem('hydra_auth_last_refresh');
1614    localStorage.removeItem('hydra_auth_auto_refresh');
1615  } catch (e) {}
1616  if (window.TokenManager && typeof window.TokenManager.clearToken === 'function') {
1617    try { window.TokenManager.clearToken(); } catch (e) {}
1618  }
1619  isAuthenticated = false;
1620  isAdmin = false;
1621  didToken = null;
1622  userMetadata = null;
1623  window.isAuthenticated = false;
1624  window.isAdmin = false;
1625  window.didToken = null;
1626}
1627
1628function hydraUrlRequestsLogout() {
1629  try {
1630    var params = new URLSearchParams(window.location.search);
1631    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1632    if (params.get('logout') === '1' || params.get('logout') === 'true') return true;
1633    if (params.get('hydra_logout') === '1' || params.get('hydra_logout') === 'true') return true;
1634    return path === '/_manage/logout';
1635  } catch (e) {
1636    return false;
1637  }
1638}
1639
1640function hydraCleanLogoutFromUrl() {
1641  try {
1642    var params = new URLSearchParams(window.location.search);
1643    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1644    var hadQuery = params.has('logout') || params.has('hydra_logout');
1645    params.delete('logout');
1646    params.delete('hydra_logout');
1647    var nextPath = path === '/_manage/logout' ? '/_manage' : window.location.pathname;
1648    var qs = params.toString();
1649    var next = nextPath + (qs ? '?' + qs : '') + (window.location.hash || '');
1650    if (hadQuery || path === '/_manage/logout') {
1651      history.replaceState({}, '', next);
1652    }
1653  } catch (e) {}
1654}
1655
1656(function hydraLogoutFromUrlEarly() {
1657  if (!hydraUrlRequestsLogout()) return;
1658  clearHydraClientAuthStorage();
1659  try { sessionStorage.setItem('hydra_logout_cleared', '1'); } catch (e) {}
1660  hydraCleanLogoutFromUrl();
1661  console.log('[Hydra] Auth cleared via logout link/query.');
1662})();
1663
1664function hydraUrlRequestsRefresh() {
1665  try {
1666    var params = new URLSearchParams(window.location.search);
1667    var v = params.get('refresh');
1668    return v === '1' || v === 'true';
1669  } catch (e) {
1670    return false;
1671  }
1672}
1673
1674function hydraClearGalleryClientCache() {
1675  try {
1676    Object.keys(localStorage).filter(function (key) {
1677      return key.includes('gallery') || key.includes('neon') || key.includes('lightbox');
1678    }
1678).forEach(function (key) {
1679      localStorage.removeItem(key);
1680    });
1681  } catch (e) {}
1682  try {
1683    delete window.__neonGalleryMainBootstrapWipeDone;
1684  } catch (e) {
1685    window.__neonGalleryMainBootstrapWipeDone = false;
1686  }
1687  window._forceGalleryReload = true;
1688}
1689
1690function hydraCleanRefreshFromUrl() {
1691  try {
1692    var params = new URLSearchParams(window.location.search);
1693    if (!params.has('refresh')) return;
1694    params.delete('refresh');
1695    var qs = params.toString();
1696    var next = window.location.pathname + (qs ? '?' + qs : '') + (window.location.hash || '');
1697    history.replaceState({}, '', next);
1698  } catch (e) {}
1699}
1700
1701(function hydraRefreshFromUrlEarly() {
1702  if (!hydraUrlRequestsRefresh()) return;
1703  clearHydraClientAuthStorage();
1704  hydraClearGalleryClientCache();
1705  try { sessionStorage.setItem('hydra_refresh_cleared', '1'); } catch (e) {}
1706  hydraCleanRefreshFromUrl();
1707  console.log('[Hydra] Gallery cache/auth cleared via ?refresh=1 — forcing fresh load.');
1708})();
1709
1710// State Management — site-wide KV metadata for the editor (not per-page SEO)
1711window.siteMetadata = {"title":"People - Adena Stevens Photography","description":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and higher education photography.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""};
1712console.log('Client Initialized site metadata:', window.siteMetadata);
1713
1714/** Match SSR title format: "{Gallery Title} | {Site Title}" */
1715function formatGalleryDocumentTitle(galleryTitle, win) {
1716  win = win || window;
1717  var meta = win.siteMetadata;
1718  var siteTitle = (meta && meta.title && String(meta.title).trim()) || win.location.hostname;
1719  return (galleryTitle || 'Gallery') + ' | ' + siteTitle;
1720}
1721
1722console.log('Client Google Analytics from metadataToUse:', '');
1723console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics);
1724console.log('Client Content Language:', window.siteMetadata.contentLanguage);
1725
1726// State Management
1727let galleries = [{"id":1776787399607,"title":"PEOPLE","parentId":null,"visible":true,"isIntegrated":true,"pageId":"aqipsifn","slug":"people","url":"/people","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"aqipsifn","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"aqipsifn","initialPageAlias":"people","timestamp":1784745905380,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":3,"spacing":0.5,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":0.5,"useRowsLayout":true,"itemsPerRow":3,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"nIcc-lvFuaURuazmETyFzghLEpUWRqkT-z_CJ0J-z7Q","txId":"nIcc-lvFuaURuazmETyFzghLEpUWRqkT-z_CJ0J-z7Q","flyIoSaveTimestamp":1784745032781,"irysSaveTimestamp":1776787802103,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"isHydra":true},"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"metaTitle":"People - Adena Stevens Photography","metaDescription":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and education photography.","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isH
1727omePage":true},{"id":1776787908728,"title":"ATHLETES","parentId":null,"visible":true,"isIntegrated":true,"pageId":"an5m6dbk","slug":"athletes","url":"/athletes","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"an5m6dbk","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"an5m6dbk","initialPageAlias":"athletes","timestamp":1784746002720,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":0.5,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":0.5,"useRowsLayout":true,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"eNdQ_l2SwodRuaXsr-jTuxxa96R9AW6sy06luw5O8f0","txId":"eNdQ_l2SwodRuaXsr-jTuxxa96R9AW6sy06luw5O8f0","flyIoSaveTimestamp":1784746004956,"irysSaveTimestamp":1776891304144,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"isHydra":true},"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776788654891,"title":"HIGHER ED","parentId":null,"visible":true,"isIntegrated":true,"pageId":"4munwywc","slug":"higher-ed","url":"/higher-ed","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"4munwywc","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"4munwywc","initialPageAlias":"higher-ed","timestamp":1784750131910,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":3,"spacing":0.5,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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":"
1727center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":0.5,"useRowsLayout":true,"itemsPerRow":3,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"PQBERRwMcq5Oz5RTDOsGPowGUVADiXCA15aU429WwfA","txId":"PQBERRwMcq5Oz5RTDOsGPowGUVADiXCA15aU429WwfA","flyIoSaveTimestamp":1784750134352,"irysSaveTimestamp":1776789026027,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"isHydra":true,"cdnNdjsonId":"d58486801774441798b792ef1337e7ec","cdnNdjsonUrl":"https://cdn.neonsky.app/d58486801774441798b792ef1337e7ec.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/f21s3uk4_4munwywc.json","cdnSaveTimestamp":1784750138993},"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"metaTitle":"Higher Ed - Adena Stevens Photography","metaDescription":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and education photography.","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776790218826,"title":"BRANDING","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1776790218826","visible":true,"parentId":null,"siteId":"f21s3uk4","slug":"branding","url":"/branding","pageElements":[{"id":1776790221711,"type":"metadata","visible":true,"position":0,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vh","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"},{"id":1776790228823,"type":"column-container","title":"Columns","visible":true,"position":1,"columns":[{"id":1776790231928,"hAlign":"left","vAlign":"top","elements":[{"id":1776790255465,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776794266298_Branding-001_yoga_branding_02-1920w.jpeg","imageKey":"f21s3uk4/1776794266298_Branding-001_yoga_branding_02-1920w.jpeg","imageFilename":"Branding-001_yoga_branding_02-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776790228781,"hAlign":"left","vAlign":"top","elements":[{"id":1776790256151,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><br></p><p><span style=\"font-size: 24px;\">Sam</span></p><p><span style=\"font-size: 24px;\">Bloom with Sam</span></p><p><br></p><p><span style=\"font-size: 16px;\">Sam is a yoga instructor in Jersey City and Hoboken. Her IG handle is bloomwithsam so I knew we had to include flowers in some of her photos. She wanted a combination of portraits and yoga poses to be able to use on social media as well as marketing for workshops. </span></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818922606,"type":"spacer","title":"Spacer Copy","visible":true,"position":2,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776793979457,"type":"column-container","title":"Columns Copy","visible":true,"position":3,"columns":[{"id":1776793987904,"hAlign":"left","vAlign":"top","elements":[{"id":1776793983652,"type":"text","t
1727itle":"Text Block","visible":true,"position":0,"textContent":"<p><span style=\"font-size: 24px;\">Dana</span></p><p><span style=\"font-size: 24px;\">Brooklyn Resume Studio</span></p><p><br></p><p><span style=\"font-size: 16px;\">Every few years, Dana refreshes her headshots and this time we did them at The Yard in Gowanus where her business is located. The shoot was a combination of portraits and also photos showing Dana working and interacting with clients. </span></p>","textWidth":100}],"widthPercent":30},{"id":1776793986478,"hAlign":"left","vAlign":"top","elements":[{"id":1776793983215,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776790406434_Branding-002_brooklyn_branding-1920w.jpeg","imageKey":"f21s3uk4/1776790406434_Branding-002_brooklyn_branding-1920w.jpeg","imageFilename":"Branding-002_brooklyn_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818911697,"type":"spacer","title":"Spacer Copy","visible":true,"position":4,"spacerHeightVh":4.5,"spacerWidthVw":100},{"id":1776794700387,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":5,"columns":[{"id":1776794692712,"hAlign":"left","vAlign":"top","elements":[{"id":1776794698621,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776790949606_Branding-003_qigong_fall_01-1920w.jpeg","imageKey":"f21s3uk4/1776790949606_Branding-003_qigong_fall_01-1920w.jpeg","imageFilename":"Branding-003_qigong_fall_01-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776794693084,"hAlign":"left","vAlign":"top","elements":[{"id":1776794693608,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><span style=\"font-size: 24px;\">Erin Kumpf</span></p><p><span style=\"font-size: 24px;\">Fall shoot - QiGong</span></p><p><br></p><p><span style=\"font-size: 16px;\">Erin Kumpf is not only an amazing acupuncturist, but also practices and teaches QiGong. We did 4 seasonal shoots - starting with fall.</span></p><p><br></p><p><span style=\"font-size: 16px;\">QiGong (pronounced “Chee gong”) is a branch of Chinese Medicine (along with acupuncture, herbs, nutrition, tui na) with roots in Taoism that incorporates gentle stretches, strengthening postures, flowing movement, breath work and meditation. </span></p>","textWidth":100,"fadeInOnScroll":false}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1784750541377,"type":"spacer","title":"Spacer Copy Copy","visible":true,"position":6,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776797627294,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":7,"columns":[{"id":1776797634311,"hAlign":"left","vAlign":"top","elements":[{"id":1776797630209,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Litzky PR</span></h2><p><span style=\"font-size: 16px;\">Litzky PR needed to update their website. We did individual headshots with a portable studio on site that I set up in a conference room, plus a team photo. </span></p><p><br></p>","textWidth":100,"fadeInOnScroll":false}],"widthPercent":30},{"id":1776797632058,"hAlign":"left","vAlign":"top","elements":[{"id":1776797635205,"type":"image","t
1727itle":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1784746790007_litzkyPR.webp","imageKey":"f21s3uk4/1784746790007_litzkyPR.webp","imageFilename":"litzkyPR.webp","imageType":"image/webp","altText":"","linkPageId":"","linkTarget":"_self","linkAutoAdvance":false,"linkAdvanceDuration":5,"fadeInOnScroll":false,"imageFit":"contain","imageAspectRatio":"auto","imageFrameHeight":"","imageBorderColor":"#1c1814","imageBorderOpacity":0,"imageBorderWidthPx":1,"imageBorderRadiusPx":0}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1784750524758,"type":"spacer","title":"Spacer Copy Copy","visible":false,"position":8,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818882765,"type":"spacer","title":"Spacer","visible":true,"position":9,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776794214081,"type":"column-container","title":"Columns Copy","visible":true,"position":10,"columns":[{"id":1776794216867,"hAlign":"left","vAlign":"top","elements":[{"id":1776794220215,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776794156190_Branding-010_acupuncture_herbs_branding-1920w.jpeg","imageKey":"f21s3uk4/1776794156190_Branding-010_acupuncture_herbs_branding-1920w.jpeg","imageFilename":"Branding-010_acupuncture_herbs_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776794221664,"hAlign":"left","vAlign":"top","elements":[{"id":1776794214755,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><span style=\"font-size: 24px;\">Nurturing Life Acupuncture &amp; Wellness</span></p><p><br></p><p><span style=\"font-size: 16px;\">Client was updating website and needed photos of products for online store as well as photos of their space in Hoboken. </span></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776868577370,"type":"spacer","title":"Spacer Copy","visible":false,"position":11,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818876115,"type":"spacer","title":"Spacer","visible":true,"position":12,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776790942163,"type":"column-container","title":"Columns Copy","visible":true,"position":13,"columns":[{"id":1776790935131,"hAlign":"left","vAlign":"top","elements":[{"id":1776790937957,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Kure Pilates</span></h2><p><span style=\"font-size: 16px;\">Photos were for a rebranding of their website. Client needed photos of the instructors and the client space.&nbsp;</span></p><p><br></p>","textWidth":100}],"widthPercent":30},{"id":1776790941706,"hAlign":"left","vAlign":"top","elements":[{"id":1776790939982,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776794784457_Branding-004_kure_pilates_branding-1920w.jpeg","imageKey":"f21s3uk4/1776794784457_Branding-004_kure_pilates_branding-1920w.jpeg","imageFilename":"Branding-004_kure_pilates_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1784750493885,"type":"spacer","title":"Spacer Copy","visible":true,"position":14,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776797555900,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":15,"columns":[{"id":1776797554306,"hAlign":"left","vAlign":"top","elements":[{"id":1776797557772,"type":"image","t
1727itle":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776797562874_Branding-006_utica_plumbing-1920w.jpeg","imageKey":"f21s3uk4/1776797562874_Branding-006_utica_plumbing-1920w.jpeg","imageFilename":"Branding-006_utica_plumbing-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776797554697,"hAlign":"left","vAlign":"top","elements":[{"id":1776797558714,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Utica Avenue Plumbing Supply</span></h2><p><span style=\"font-size: 16px;\">When Utica Avenue Plumbing Supply decided to refresh their website, they needed headshots of everyone on staff. I was able to bring a mini studio to them and photograph everyone against the same neutral backdrop, which made adding the new photos to the website easy for their designer.&nbsp;</span></p><p><br></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818851299,"type":"spacer","title":"Spacer","visible":true,"position":16,"spacerHeightVh":5,"spacerWidthVw":100},{"id":1776795167767,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":17,"columns":[{"id":1776795163538,"hAlign":"left","vAlign":"top","elements":[{"id":1776795170591,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Marsh &amp; McLennan</span></h2><p><span style=\"font-size: 16px;\">Client celebrated the integration of their company with JLT. They needed office interior photographs that illustrated specific signage and the office spaces.&nbsp;</span></p><p><br></p>","textWidth":100}],"widthPercent":30},{"id":1776795163478,"hAlign":"left","vAlign":"top","elements":[{"id":1776795164737,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776797659640_Branding-007_nyc_office_branding-1920w.jpeg","imageKey":"f21s3uk4/1776797659640_Branding-007_nyc_office_branding-1920w.jpeg","imageFilename":"Branding-007_nyc_office_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818828216,"type":"spacer","title":"Spacer","visible":true,"position":18,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818599600,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":19,"columns":[{"id":1776818595572,"hAlign":"left","vAlign":"top","elements":[{"id":1776818600362,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776818639782_Branding-009_yoga_branding_01-1920w.jpeg","imageKey":"f21s3uk4/1776818639782_Branding-009_yoga_branding_01-1920w.jpeg","imageFilename":"Branding-009_yoga_branding_01-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776818592802,"hAlign":"left","vAlign":"top","elements":[{"id":1776818599089,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Lena</span></h2><p><span style=\"font-size: 16px;\">Lena was recently certified in teaching yoga and needed photos to share with the yoga studios where she would be teaching.&nbsp;</span></p><p><br></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818824001,"type":"spacer","title":"Spacer","visible":true,"position":20,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818687062,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":21,"columns":[{"id":1776818686103,"hAlign":"left","vAlign":"top","elements":[{"id":1776818693758,"type":"text","t
1727itle":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Princeton University Economics Department</span></h2><p><span style=\"font-size: 16px;\">Headshots of Princeton University Economics Department's faculty and staff. The courtyard next to their building lent itself to a nice soft background that showed the campus but the focus remained on the individuals.&nbsp;</span></p><p><br></p>","textWidth":100}],"widthPercent":30},{"id":1776818694725,"hAlign":"left","vAlign":"top","elements":[{"id":1776818692788,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776818779413_Branding-008_princeton_university_econ-1920w.jpeg","imageKey":"f21s3uk4/1776818779413_Branding-008_princeton_university_econ-1920w.jpeg","imageFilename":"Branding-008_princeton_university_econ-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}}],"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776789594485,"title":"PLACES + THINGS","parentId":null,"visible":true,"isIntegrated":true,"pageId":"roh6f7ad","slug":"places--things","url":"/places--things","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"roh6f7ad","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"roh6f7ad","initialPageAlias":"places--things","timestamp":1784750891268,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":3,"spacing":1,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":1,"useRowsLayout":true,"itemsPerRow":3,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"I9aliryvb2GghRsEWtlLHtqq8UZVRrNuvLPoZv9ckvg","txId":"I9aliryvb2GghRsEWtlLHtqq8UZVRrNuvLPoZv9ckvg","flyIoSaveTimestamp":1776789866535,"irysSaveTimestamp":1776789866535,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false},"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"isH
1727omePage":false},{"id":1776819147538,"title":"ABOUT","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"4kuqohzp","visible":true,"parentId":null,"siteId":"f21s3uk4","slug":"about","url":"/about","pageElements":[{"id":1776819153165,"type":"metadata","visible":true,"position":1,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"10vh"},{"id":1776819149731,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p style=\"line-height: 1.5;\" class=\"ql-align-center\"><br></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\"><span class=\"ql-cursor\"></span>Adena Stevens is a portrait and corporate event photographer based in Hoboken, New Jersey. With over two decades of experience, she specializes in portraits, corporate events, branding and lifestyle, and higher education photography in New Jersey and NYC.&nbsp;</span></p><p style=\"line-height: 1.5;\"><br></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\">Adena earned a Bachelor of Science degree in photojournalism from the S.I. Newhouse School of Public Communications at Syracuse University. Her past experience includes staff photographer at both Asbury Park Press in Neptune, NJ and The Day in New London, CT, and digital media manager at Yeshiva University in New York City.&nbsp;</span></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\"></span></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\">Partial client list includes Princeton University, Yeshiva University, Ramapo College, Friends Seminary, Robert Half, and Brooklyn Resume Studio.&nbsp;</span></p><p><br></p>","textWidth":50,"fadeInOnScroll":false}],"isHomePage":false,"position":6,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false},{"id":1776819067063,"title":"CONTACT","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1776819067063","visible":true,"parentId":null,"siteId":"f21s3uk4","slug":"contact","url":"/contact","pageElements":[{"id":1776819071298,"type":"metadata","visible":true,"position":1,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"10vh"},{"id":1776819076552,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p style=\"line-height: 1.5;\" class=\"ql-align-center\"><br></p><h2 class=\"ql-align-center\" style=\"line-height: 0.5;\"><span class=\"ql-cursor\"></span><span style=\"font-size: 16px;\">Adena Stevens Photography&nbsp;</span></h2><h2 class=\"ql-align-center\" style=\"line-height: 0.5;\"><span style=\"font-size: 16px;\">315.559.7631&nbsp;</span></h2><h2 class=\"ql-align-center\" style=\"line-height: 0.5;\"><a href=\"mailto:[email protected]\" target=\"_blank\" style=\"color: rgb(147, 147, 147); font-size: 16px;\">[email protected]</a><span style=\"font-size: 16px;\">&nbsp;</span></h2><p><br></p><h2 style=\"line-height: 0.5;\" class=\"ql-align-center\"><span style=\"font-family: &quot;Hanken Grotesk&quot;, sans-serif; font-size: 16px;\">New Jersey &amp; NYC</span></h2><p><br></p>","textWidth":63,"fadeInOnScroll":false}],"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776891434268,"title":"test","parentId":null,"visible":false,"isIntegrated":true,"pageId":"waa2k0e0","slug":"test","url":"/test","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"waa2k0e0","siteId":"f21s3uk4","siteAlias":"adenastevens.com","initialPageUuid":"waa2k0e0","initialPageAlias":"test","timestamp":1784752742345,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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":"
1727center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"isHydra":true,"flyIoSaveTimestamp":1784752744958,"irysSaveTimestamp":1776891467155,"allTimestampsMatch":false,"timestampsMatch":false,"txId":"djDv4jUjWAiA9FJrQHUsuOL9t-SVRbhqiKSpt_fSfsM","permaURL":"djDv4jUjWAiA9FJrQHUsuOL9t-SVRbhqiKSpt_fSfsM","fileSize":2377,"originalFileSize":10320,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"6d8e35677b4741f19746c26104040278","cdnNdjsonUrl":"https://cdn.neonsky.app/6d8e35677b4741f19746c26104040278.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/f21s3uk4_waa2k0e0.json","embedPointerUrl":"https://embed.neonsky.app/ndjson/f21s3uk4_waa2k0e0.json","cdnSaveTimestamp":1784752746601,"uploadedTo":["fly","cdn"]},"isPage":false}];
1728window.galleries = galleries;
1729
1730function getSiteGalleriesArray() {
1731  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
1732    return galleries;
1733  }
1734  if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
1735    return window.galleries;
1736  }
1737  if (window.GALLERY_DATA_CACHE &&
1738      window.GALLERY_DATA_CACHE.initialized &&
1739      Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
1740    return window.GALLERY_DATA_CACHE.galleries;
1741  }
1742  return [];
1743}
1744window.getSiteGalleriesArray = getSiteGalleriesArray;
1745
1746let activeGalleryId = null;
1747let sortableInstance = null;
1748let isEditing = false;
1749let initialPageInfo = {"id":1776787908728,"type":"gallery","pageId":"an5m6dbk","slug":"athletes","url":"/athletes"};
1750
1751
1752// Store the current menu layout type (must be JSON-encoded — bare sidebar/horizontal is invalid JS)
1753let currentMenuLayout = null;
1754
1755/** True when this document is loaded in the chromeless iframe (?neon_embed=1 or body.neon-page-embed). */
1756function isNeonPageEmbedShell() {
1757  try {
1758    if (typeof document !== 'undefined' && document.body && document.body.classList.contains('neon-page-embed'))
1759      return true;
1760    var _spEmbed = new URLSearchParams(window.location.search);
1761    return _spEmbed.get('neon_embed') === '1';
1762  } catch (_eEmbed) { return false; }
1763}
1764
1765/**
1766 * Mirrors "hide menu on this page": in embed shells always hide menus (unless editing),
1767 * otherwise use each gallery/page flag.
1768 */
1769function shouldApplyMenuHiddenForEmbedOrGallery(gallery) {
1770  var editing = typeof window.isInEditMode === 'function' && window.isInEditMode();
1771  if (editing)
1772    return !!(gallery && gallery.hideMenuOnPage);
1773  return !!(gallery && gallery.hideMenuOnPage) || isNeonPageEmbedShell();
1774}
1775
1776function shouldApplyMenuOverlayOnPage(gallery) {
1777  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery))
1778    return false;
1779  return !!(gallery && gallery.overlayMenuOnPage);
1780}
1781
1782function isOverlayMenuChromeActive() {
1783  return document.body.classList.contains('menu-overlay-on-page')
1784    && !document.body.classList.contains('edit-mode-active');
1785}
1786
1787/** Style Customizer paints sidebar with inline background-color !important; beat that only while overlay is active. */
1788function syncOverlayMenuInlineChrome() {
1789  var sidebar = document.querySelector('.sidebar');
1790  var header = document.querySelector('.mobile-header');
1791  if (isOverlayMenuChromeActive()) {
1792    if (sidebar) {
1793      sidebar.style.setProperty('background-color', 'transparent', 'important');
1794      sidebar.style.setProperty('background-image', 'none', 'important');
1795    }
1796    if (header) {
1797      header.style.setProperty('background-color', 'transparent', 'important');
1798      header.style.setProperty('background-image', 'none', 'important');
1799    }
1800    return;
1801  }
1802  if (sidebar) {
1803    var menuBg = (getComputedStyle(document.documentElement).getPropertyValue('--menu-background-color') || '').trim() || '#fafafa';
1804    var hidden = document.body.classList.contains('menu-background-hidden');
1805    sidebar.style.setProperty('background-color', hidden ? 'transparent' : menuBg, 'important');
1806    if (hidden) {
1807      sidebar.style.setProperty('background-image', 'none', 'important');
1808    } else {
1809      sidebar.style.removeProperty('background-image');
1810    }
1811  }
1812  if (header) {
1813    header.style.removeProperty('background-color');
1814    header.style.removeProperty('background-image');
1815  }
1816}
1817
1818function applyMenuOverlayClassForGallery(gallery) {
1819  if (shouldApplyMenuOverlayOnPage(gallery)) {
1820    document.body.classList.add('menu-overlay-on-page');
1821  } else {
1822    document.body.classList.remove('menu-overlay-on-page');
1823  }
1824  syncOverlayMenuInlineChrome();
1825}
1826
1827function hookMenuStyleCustomizerForOverlay() {
1828  var customizer = window.MenuStyleCustomizer;
1829  if (!customizer || customizer.__overlayBgHooked || typeof customizer.applyStyles !== 'function')
1830    return;
1831  var original = customizer.applyStyles.bind(customizer);
1832  customizer.applyStyles = function() {
1833    original.apply(this, arguments);
1834    if (isOverlayMenuChromeActive())
1835      syncOverlayMenuInlineChrome();
1836  };
1837  customizer.__overlayBgHooked = true;
1838}
1839window.applyMenuOverlayClassForGallery = applyMenuOverlayClassForGallery;
1840
1841document.addEventListener('DOMContentLoaded', () => {
1842  if (window.hydraInitialized) {
1843    console.log("Hydra already initialized, skipping");
1844    return;
1845  }
1846  window.hydraInitialized = true;
1847  document.body.classList.add('hydra-initialized');
1848  try {
1849    if (sessionStorage.getItem('hydra_refresh_cleared') === '1') {
1850      sessionStorage.removeItem('hydra_refresh_cleared');
1851      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
1852        window.EnhancedGallerySystem.resetInitializationState();
1853      }
1854      console.log('[Hydra] Applied ?refresh=1 — gallery framework reset for clean load.');
1855    }
1856  } catch (e) {}
1857  try {
1858    var neonEmbedParams = new URLSearchParams(window.location.search);
1859    if (neonEmbedParams.get('neon_embed') === '1') {
1860      document.body.classList.add('neon-page-embed', 'menu-hidden-on-page');
1861    }
1862  } catch (_neonEmbedErr) {}
1863  hookMenuStyleCustomizerForOverlay();
1864  setTimeout(hookMenuStyleCustomizerForOverlay, 0);
1865  setTimeout(hookMenuStyleCustomizerForOverlay, 500);
1866  if (isOverlayMenuChromeActive())
1867    syncOverlayMenuInlineChrome();
1868  window.Parameters = window.Parameters || {};
1869  if (localStorage.getItem('hydra_is_admin') === 'true') {
1870    document.body.classList.add('hydra-admin', 'hydra-authenticated');
1871    isAdmin = true;
1872    isAuthenticated = true;
1873    console.log('Restored admin status from localStorage');
1874    
1875    // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins
1876    // This ensures admins always see fresh data when opening new tabs
1877    try {
1878      console.log('Admin detected - clearing all caches except auth tokens...');
1879      
1880      // Save auth tokens first
1881      const savedAuthToken = localStorage.getItem('hydra_auth_token');
1882      const savedAuthEmail = localStorage.getItem('hydra_auth_email');
1883      const savedIsAdmin = localStorage.getItem('hydra_is_admin');
1884      const savedDidToken = localStorage.getItem('hydra_did_token');
1885      
1886      // Clear ALL localStorage
1887      localStorage.clear();
1888      
1889      // Restore auth tokens
1890      if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken);
1891      if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail);
1892      if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin);
1893      if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken);
1894      
1895      // Clear ALL sessionStorage (auth tokens are not stored here)
1896      sessionStorage.clear();
1897      
1898      console.log('Cleared all caches except auth tokens for admin user');
1899    } catch (cacheError) {
1900      console.warn('Error clearing caches on admin page load:', cacheError);
1901      // Don't fail page load if cache clearing fails
1902    }
1903  }
1904
1905  function ensureSidebarElementsAndRender() {
1906  console.log('Ensuring sidebar elements are available before rendering...');
1907  
1908  // Restore from siteConfig when hydrate missed or init invented a default menu
1909  if (window.SidebarManager) {
1910    var current = window.SidebarManager.elements;
1911    var missingOrDefault = !current || current.length === 0 || window.SidebarManager._createdDefaultElements;
1912    if (missingOrDefault && typeof siteConfig !== 'undefined' && siteConfig.sidebarElements && siteConfig.sidebarElements.length > 0) {
1913      console.log('Initializing SidebarManager elements from siteConfig');
1914      window.SidebarManager.elements = siteConfig.sidebarElements;
1915      window.SidebarManager._elementsReady = true;
1916      window.SidebarManager._createdDefaultElements = false;
1917      if (typeof window.SidebarManager.renderElements === 'function') {
1918        window.SidebarManager.renderElements();
1919      }
1920    }
1921  }
1922  
1923  // Determine layout and render
1924  const currentMenuLayout = getEffectiveMenuLayout();
1925  
1926  if (currentMenuLayout === 'horizontal') {
1927    console.log('Rendering horizontal menu with ensured sidebar elements');
1928    renderHorizontalMenu();
1929  } else {
1930    renderGalleries();
1931  }
1932}
1933
1934 setTimeout(() => {
1935  console.log("Initializing galleries:", galleries.length);
1936  // Collapse folders before first paint so nested items never flash as expanded plain text
1937  alignFolderStates();
1938  
1939  // CHANGED: Use the new function instead of direct rendering
1940  setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init
1941  
1942  initMobileMenu();
1943  
1944  /** If true, deep-link timeouts below must not call handleURLNavigation again (duplicate loadPage / blink). */
1945  let bootstrapRouteAttempted = false;
1946
1947  // Handle direct URL navigation
1948  if (initialPageInfo) {
1949    console.log("Loading initial page from direct URL:", initialPageInfo);
1950    
1951    // Find the gallery by ID
1952    const gallery = galleries.find(g => g.id === initialPageInfo.id);
1953    
1954    if (gallery) {
1955      // Set active gallery ID
1956      activeGalleryId = gallery.id;
1957      
1958      if (initialPageInfo.type === 'page') {
1959        console.log("Loading page directly:", gallery.title);
1960        loadPage(gallery.id);
1961      } else {
1962        console.log("Loading gallery directly:", gallery.title);
1963        loadGallery(gallery.id);
1964      }
1965      bootstrapRouteAttempted = true;
1966    }
1967  } else if (window.location.pathname === '/') {
1968    // No specific path, try to load home page
1969    console.log("No specific path, checking for home page");
1970    if (!loadHomePage() && galleries.length > 0) {
1971      // Default to first gallery if no home page is set
1972      loadGallery(galleries[0].id);
1973    }
1974    bootstrapRouteAttempted = true;
1975  } else if (galleries.length > 0) {
1976    // Path exists but no matching initialPageInfo, try to handle URL path
1977    handleURLNavigation();
1978    bootstrapRouteAttempted = true;
1979  }
1980  window.__hydraBootstrapRouteAttempted = bootstrapRouteAttempted;
1981  
1982  // Log what was rendered
1983  console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found");
1984  
1985    // Start with edit controls properly set
1986    toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active'));
1987    
1988    // Explicitly hide the import classic form too
1989    const importForm = document.getElementById('importClassicForm');
1990    if (importForm) {
1991      importForm.style.display = 'none';
1992      importForm.classList.remove('visible');
1993    }
1994    
1995    // Try to restore token from localStorage
1996    if (!didToken) {
1997      didToken = localStorage.getItem('hydra_auth_token');
1998      if (didToken) {
1999        console.log('Restored auth token from localStorage');
2000      }
2001    }
2002    
2003    // Initialize Magic and check authentication
2004    
2005    
2006    // Initialize sidebar elements if available
2007    if (window.SidebarManager) {
2008      // Pass the initial elements from server config if available
2009      if (true) {
2010        window.SidebarManager.elements = [{"id":1776787283176,"type":"menu","title":"Menu","visible":true,"position":1},{"id":1776819336850,"type":"image","title":"Image","visible":true,"position":0,"imageData":"https://storage.neonsky.app/f21s3uk4/1776867536436_adena-stevens-photography-p.png","imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","linkAutoAdvance":false,"hideFromMobileMenu":true},{"id":1776878709559,"type":"social","title":"Social","visible":true,"position":2,"socialIconSize":16,"socialIcons":[{"type":"email","url":"mailto:[email protected]"},{"type":"instagram","url":"https://www.instagram.com/adenastevensphoto"}],"socialAlignment":"center"}];
2011        if (window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2012          window.SidebarManager._elementsReady = true;
2013          window.SidebarManager._createdDefaultElements = false;
2014        }
2015      }
2016      window.SidebarManager.init();
2017    }
2018
2019
2020    // Initialize metadata from server config (site-wide only — not current page SEO)
2021window.siteMetadata = {"title":"People - Adena Stevens Photography","description":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and higher education photography.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""};
2022console.log('Client Initialized site metadata (second init):', window.siteMetadata);
2023console.log('Client Google Analytics from metadataToUse (second init):', '');
2024console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics);
2025console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage);
2026
2027    // Update copyright footer after metadata is set 
2027(important for main domain)
2028    if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
2029      window.SidebarManager.updateMetadataFooter();
2030    }
2031
2032
2033
2034    const submenuCheck = document.getElementById('createSubmenu');
2035    const submenuTitleField = document.getElementById('submenuTitle');
2036    const submenuTitleGroup = submenuTitleField?.parentElement;
2037    
2038    if (submenuCheck && submenuTitleGroup) {
2039      submenuCheck.addEventListener('change', function() {
2040        submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
2041      });
2042    }
2043    
2044    // Check authentication state after initialization
2045    if (isAuthenticated && isAdmin) {
2046      console.log('Already authenticated and admin, showing edit UI');
2047      forceShowEditUI();
2048    } else if (didToken) {
2049      console.log('Have token, checking admin status');
2050      checkAuth();
2051    }
2052  }, 100); // Small delay to ensure everything is loaded
2053
2054  setTimeout(() => {
2055    if (window.location.pathname === '/' ||
2056        window.location.pathname === '/_manage' || window.location.pathname === '/_manage/' ||
2057        (window.location.hostname === 'preview.neonsky.app' && 
2058         window.location.pathname.split('/').length <= 2)) {
2059      // Root URL or preview URL with only GUID - check for home page
2060      // Skip if bootstrap already routed (themed sites boot slower and would hit a duplicate load)
2061      if (!initialPageInfo && !window.__hydraBootstrapRouteAttempted) {
2062        loadHomePage();
2063      }
2064    }
2065  }, 500);
2066
2067  // Dedicated editor URL: open edit tools or OTP login without requiring + key
2068  setTimeout(function hydraManageEditorEntry() {
2069    var p = window.location.pathname;
2070    if (p !== '/_manage' && p !== '/_manage/') return;
2071    try {
2072      if (sessionStorage.getItem('hydra_logout_cleared') === '1') {
2073        sessionStorage.removeItem('hydra_logout_cleared');
2074        console.log('[Hydra] Logout link cleared auth; skipping auto-login on /_manage.');
2075        return;
2076      }
2077    } catch (e) {}
2078    if (document.body.classList.contains('edit-mode-active')) return;
2079    try {
2080      var token = window.didToken || localStorage.getItem('hydra_auth_token');
2081      var admin = window.isAdmin === true || localStorage.getItem('hydra_is_admin') === 'true';
2082      if (token && admin) {
2083        if (typeof window.toggleEditMode === 'function') window.toggleEditMode();
2084        return;
2085      }
2086      if (token && typeof checkAdminStatus === 'function') {
2087        checkAdminStatus(token).then(function() {
2088          if (window.isAdmin && typeof window.toggleEditMode === 'function' && !document.body.classList.contains('edit-mode-active')) {
2089            window.toggleEditMode();
2090          }
2091        }).catch(function() {});
2092        return;
2093      }
2094      if (typeof startOTPLogin === 'function') {
2095        var pr = startOTPLogin();
2096        if (pr && typeof pr.catch === 'function') pr.catch(function() {});
2097      }
2098    } catch (e) {
2099      console.error('hydraManageEditorEntry:', e);
2100    }
2101  }, 2200);
2102});
2103
2104/**
2105 * Accessible name for header/sidebar brand images (no visual change).
2106 * Prefers element title or site metadata over generic "Image" / filenames.
2107 */
2108function resolveBrandLogoAlt(element) {
2109  if (window.SidebarManager && typeof window.SidebarManager._resolveBrandImageAlt === 'function') {
2110    return window.SidebarManager._resolveBrandImageAlt(element || {});
2111  }
2112  const siteTitle =
2113    (window.siteMetadata && String(window.siteMetadata.title || '').trim()) ||
2114    (document.title && document.title.includes('|') ? document.title.split('|').pop().trim() : '') ||
2115    String(location.hostname || '').replace(/^www\./i, '');
2116  const title = element && element.title ? String(element.title).trim() : '';
2117  if (title && !/^image(\s*\d*)?$/i.test(title)) return title;
2118  return siteTitle || 'Site logo';
2119}
2120
2121/**
2122 * Gets the first visible sidebar element (image or text) to be used as a logo or title.
2123 * @returns {object|null} The first suitable sidebar element or null.
2124 */
2125
2126function getFirstSidebarElementForHeader() {
2127    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2128        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
2129        if (visibleElements.length > 0) {
2130            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
2131            
2132            // Debug: Log all element types found
2133            const elementTypes = sortedElements.map(el => ({ type: el.type, id: el.id, title: el.title }));
2134            console.log('[getFirstSidebarElementForHeader] Visible elements found:', elementTypes);
2135            
2136            for (let el of sortedElements) {
2137                if (el.type === 'image' || el.type === 'text') { // Filter 2: type
2138                    console.log('[getFirstSidebarElementForHeader] Found matching element:', { type: el.type, id: el.id, title: el.title, linkType: el.linkType, linkUrl: el.linkUrl, linkPageId: el.linkPageId });
2139                    return el;
2140                }
2141            }
2142            // If loop finishes, no visible image or text found among visible elements
2143            console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text. Element types found:', elementTypes.map(e => e.type));
2144            return null;
2145        } else {
2146            console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements. Total elements:', window.SidebarManager.elements.length);
2147            return null;
2148        }
2149    }
2150    console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
2151    return null;
2152}
2153
2154/**
2155 * Gets the first visible social icon element from sidebar elements.
2156 * @returns {object|null} The first suitable social element or null.
2157 */
2158function getFirstSocialElementForHeader() {
2159    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2160        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
2161        if (visibleElements.length > 0) {
2162            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
2163            for (let el of sortedElements) {
2164                if (el.type === 'social') { // Filter for social type
2165                    return el;
2166                }
2167            }
2168            // If loop finishes, no visible social element found
2169            console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.');
2170            return null;
2171        } else {
2172            console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.');
2173            return null;
2174        }
2175    }
2176    console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
2177    return null;
2178}
2179
2180function isSpaceColonySiteTheme() {
2181  const themeId = document.body && document.body.dataset.siteTheme;
2182  return themeId === 'space-colony';
2183}
2184
2185/**
2186 * Apply horizontal bar logo placement (inline vs stacked) from Menu Style > Bar logo & menu.
2187 * Sets body.horizontal-header-logo-stacked and flex on .logo-menu-container; updates --mobile-header-height when stacked.
2188 */
2189function applyHorizontalLogoMenuLayoutStyles() {
2190  const sc = window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings;
2191  const mode = (sc && sc.horizontalHeaderLogoLayout) || 'inline';
2192  const isSpaceColonyTheme = isSpaceColonySiteTheme();
2193  document.body.classList.toggle('horizontal-header-logo-stacked', mode === 'stacked');
2194  const w = document.querySelector('.logo-menu-container');
2195  if (w) {
2196    const isMobile = window.innerWidth <= 768;
2197    w.style.display = 'flex';
2198    w.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : 'center';
2199    if (mode === 'stacked') {
2200      w.style.flexDirection = 'column';
2201      w.style.gap = isMobile ? '8px' : '12px';
2202      w.style.width = '100%';
2203      w.style.boxSizing = 'border-box';
2204      w.style.flexGrow = isMobile ? '0' : '1';
2205      w.style.minWidth = '0';
2206      w.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : 'flex-start';
2207    } else {
2208      w.style.flexDirection = 'row';
2209      w.style.gap = '20px';
2210      w.style.width = '';
2211      w.style.boxSizing = '';
2212      w.style.minWidth = '';
2213      if (isMobile) {
2214        w.style.flexGrow = '0';
2215        w.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
2216      } else {
2217        w.style.flexGrow = '';
2218        w.style.justifyContent = '';
2219      }
2220    }
2221  }
2222  const mhc = document.querySelector('.mobile-header-content');
2223  if (mhc) {
2224    const isMobile = window.innerWidth <= 768;
2225    if (isSpaceColonyTheme && !isMobile) {
2226      mhc.style.alignItems = 'flex-end';
2227      mhc.style.flexGrow = '0';
2228      mhc.style.width = '';
2229    } else if (isSpaceColonyTheme && isMobile) {
2230      mhc.style.alignItems = 'flex-start';
2231      mhc.style.justifyContent = 'flex-start';
2232      mhc.style.flexGrow = '';
2233      mhc.style.width = '100%';
2234    } else if (!isMobile && mode === 'stacked') {
2235      mhc.style.alignItems = 'flex-start';
2236      mhc.style.flexGrow = '';
2237      mhc.style.width = '';
2238    } else {
2239      mhc.style.alignItems = '';
2240      mhc.style.flexGrow = '';
2241      mhc.style.width = '';
2242    }
2243  }
2244  if (typeof window.syncContentMarginToHorizontalHeader === 'function') {
2245    window.syncContentMarginToHorizontalHeader();
2246  }
2247}
2248window.applyHorizontalLogoMenuLayoutStyles = applyHorizontalLogoMenuLayoutStyles;
2249
2250function parseHorizontalHeaderHeightPx(value, fallback) {
2251  if (value === null || value === undefined) {
2252    return fallback;
2253  }
2254  const parsed = parseInt(String(value).trim(), 10);
2255  if (Number.isNaN(parsed) || parsed < 1) {
2256    return fallback;
2257  }
2258  return parsed;
2259}
2260
2261function getUserHorizontalHeaderHeightPx() {
2262  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2263    const fromSetting = parseHorizontalHeaderHeightPx(window.MenuStyleCustomizer.settings.mobileHeaderHeight, 0);
2264    if (fromSetting > 0) {
2265      return fromSetting;
2266    }
2267  }
2268  const fromVar = getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height-setting').trim();
2269  const parsedVar = parseHorizontalHeaderHeightPx(fromVar, 0);
2270  if (parsedVar > 0) {
2271    return parsedVar;
2272  }
2273  const themeMin = getComputedStyle(document.documentElement).getPropertyValue('--st-header-height').trim();
2274  const parsedTheme = parseHorizontalHeaderHeightPx(themeMin, 0);
2275  if (parsedTheme > 0) {
2276    return parsedTheme;
2277  }
2278  return 60;
2279}
2280
2281function setHorizontalHeaderHeightVars(headerPx, contentMarginPx) {
2282  const headerVal = headerPx + 'px';
2283  const marginVal = contentMarginPx + 'px';
2284  const currentHeader = getComputedStyle(document.documentElement).getPropertyValue('--mobile-header-height').trim();
2285  const currentMargin = getComputedStyle(document.documentElement).getPropertyValue('--content-margin-top').trim();
2286  if (currentHeader === headerVal && currentMargin === marginVal) {
2287    return;
2288  }
2289  document.documentElement.style.setProperty('--mobile-header-height', headerVal);
2290  document.documentElement.style.setProperty('--content-margin-top', marginVal);
2291}
2292
2293function measureHorizontalHeaderNaturalHeight(header) {
2294  const el = header;
2295  const prevHeight = el.style.getPropertyValue('height');
2296  const prevMinHeight = el.style.getPropertyValue('min-height');
2297  /* Beat CSS min-height: var(--mobile-header-height) !important so a prior wrap
2298     doesn’t floor the measurement when the viewport widens back to one row. */
2299  el.style.setProperty('height', 'auto', 'important');
2300  el.style.setProperty('min-height', '0', 'important');
2301  const measured = Math.ceil(el.getBoundingClientRect().height);
2302  if (prevHeight) {
2303    el.style.setProperty('height', prevHeight);
2304  } else {
2305    el.style.removeProperty('height');
2306  }
2307  if (prevMinHeight) {
2308    el.style.setProperty('min-height', prevMinHeight);
2309  } else {
2310    el.style.removeProperty('min-height');
2311  }
2312  return measured;
2313}
2314
2315function applyHorizontalHeaderContentInset() {
2316  if (!document.body.classList.contains('menu-layout-horizontal')) {
2317    return;
2318  }
2319  if (window._horizontalHeaderInsetApplying) {
2320    return;
2321  }
2322  const header = document.querySelector('.mobile-header');
2323  if (!header) {
2324    return;
2325  }
2326
2327  window._horizontalHeaderInsetApplying = true;
2328  try {
2329    const userH = getUserHorizontalHeaderHeightPx();
2330    /* Keep logo-cap var as a real px height. Sites can save mobileHeaderHeight as 0vh;
2331       leaving that on --top-bar-height-setting makes calc(var(...) - 8px) → 0 and hides the logo. */
2332    document.documentElement.style.setProperty('--top-bar-height-setting', userH + 'px');
2333    const hasExpandedMenu = header.querySelector('.horizontal-menu-item.expanded, .horizontal-menu-item.submenu-open');
2334    const isStackedLogo = document.body.classList.contains('horizontal-header-logo-stacked');
2335    const isPhone = window.innerWidth <= 768;
2336
2337    /*
2338     * Phone hamburger chrome: fixed Style Customizer height (link row is hidden).
2339     * Desktop inline may wrap beside the logo — measure like stacked so the bar can grow.
2340     * Stacked / open submenu: always measure.
2341     */
2342    if (!hasExpandedMenu && !isStackedLogo && isPhone) {
2343      setHorizontalHeaderHeightVars(userH, userH);
2344      return;
2345    }
2346
2347    /* Desktop inline (wrap-capable), stacked logo, or open submenu */
2348    const measured = measureHorizontalHeaderNaturalHeight(header);
2349    if (!measured || measured < 1) {
2350      setHorizontalHeaderHeightVars(userH, userH);
2351      return;
2352    }
2353
2354    let maxH = Math.max(200, Math.round(window.innerHeight * 0.5));
2355    const clamped = Math.min(Math.max(measured, userH), maxH);
2356    /* Extra content margin only when bar grew past the user setting (wrapped / stacked / open menu) */
2357    const contentMargin = clamped > userH ? clamped + 8 : clamped;
2358    setHorizontalHeaderHeightVars(clamped, contentMargin);
2359  } finally {
2360    window._horizontalHeaderInsetApplying = false;
2361  }
2362}
2363
2364function ensureHorizontalHeaderHeightObserver() {
2365  if (typeof ResizeObserver === 'undefined') {
2366    return;
2367  }
2368  const header = document.querySelector('.mobile-header');
2369  if (!header) {
2370    return;
2371  }
2372  if (window._horizontalHeaderResizeObserver) {
2373    window._horizontalHeaderResizeObserver.disconnect();
2374  }
2375  window._horizontalHeaderResizeObserver = new ResizeObserver(function() {
2376    applyHorizontalHeaderContentInset();
2377  });
2378  window._horizontalHeaderResizeObserver.observe(header);
2379}
2380
2381function syncContentMarginToHorizontalHeader() {
2382  if (!document.body.classList.contains('menu-layout-horizontal')) {
2383    return;
2384  }
2385  requestAnimationFrame(function() {
2386    requestAnimationFrame(function() {
2387      applyHorizontalHeaderContentInset();
2388      ensureHorizontalHeaderHeightObserver();
2389    });
2390  });
2391}
2392window.syncContentMarginToHorizontalHeader = syncContentMarginToHorizontalHeader;
2393window.applyHorizontalHeaderContentInset = applyHorizontalHeaderContentInset;
2394
2395function getEffectiveMenuLayout() {
2396  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.getEffectiveMenuLayout === 'function') {
2397    return window.MenuStyleCustomizer.getEffectiveMenuLayout();
2398  }
2399  if (document.body.classList.contains('menu-layout-horizontal')) return 'horizontal';
2400  if (document.body.classList.contains('menu-layout-top')) return 'top';
2401  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2402    return window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2403  }
2404  return 'sidebar';
2405}
2406
2407function ensureCorrectLayoutApplied() {
2408  // Check if MenuStyleCustomizer and its necessary parts exist
2409  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2410    // Prefer in-editor settings (e.g. Style History preview) over theme/body heuristics
2411    const fromSettings = window.MenuStyleCustomizer.settings.menuLayout;
2412    const currentLayout = fromSettings || getEffectiveMenuLayout();
2413    console.log(`Ensuring layout is applied: ${currentLayout}`);
2414
2415    // Apply the layout styles (CSS classes, display properties)
2416    window.MenuStyleCustomizer._applyMenuLayout(currentLayout);
2417
2418    // --- Force Header Structure Rebuild for Horizontal Layout ---
2419    if (currentLayout === 'horizontal') {
2420      const mobileHeader = document.querySelector('.mobile-header');
2421      if (mobileHeader) {
2422        let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2423        // Ensure the main content container exists
2424        if (!mobileHeaderContent) {
2425          mobileHeaderContent = document.createElement('div');
2426          mobileHeaderContent.className = 'mobile-header-content';
2427          const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn');
2428          if (hamburgerBtn) {
2429            mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn);
2430          } else {
2431            mobileHeader.appendChild(mobileHeaderContent);
2432          }
2433        }
2434        // Ensure specific sub-containers exist, creating them if necessary
2435        // This guarantees the structure is present before renderHorizontalMenu fills them.
2436        if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) {
2437            const logoContainer = document.createElement('div');
2438            logoContainer.className = 'horizontal-header-logo';
2439            mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild);
2440        }
2441        if (!mobileHeaderContent.querySelector('.horizontal-menu
2441-container')) {
2442            const menuContainer = document.createElement('div');
2443            menuContainer.className = 'horizontal-menu-container';
2444            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2445            if (logoContainer) {
2446                logoContainer.insertAdjacentElement('afterend', menuContainer);
2447            } else { // Fallback if logo container wasn't found/created
2448                 mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild);
2449            }
2450        }
2451        // Apply flexbox styling to position social container at far right
2452        mobileHeaderContent.style.display = 'flex';
2453        const isSpaceColonyTheme = isSpaceColonySiteTheme();
2454        const isMobile = window.innerWidth <= 768;
2455        mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2456        mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2457        if (isSpaceColonyTheme && !isMobile) {
2458          mobileHeaderContent.style.flexGrow = '0';
2459          mobileHeaderContent.style.width = '';
2460        } else {
2461          mobileHeaderContent.style.flexGrow = '';
2462          mobileHeaderContent.style.width = '100%';
2463        }
2464        
2465        // Create a flex container for logo and menu items
2466        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2467        if (!logoMenuContainer) {
2468            console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...');
2469            const logoMenuWrapper = document.createElement('div');
2470            logoMenuWrapper.className = 'logo-menu-container';
2471            // Move logo and menu into the wrapper
2472            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2473            const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
2474            
2475            console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer);
2476            console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer);
2477            
2478            if (logoContainer && menuContainer) {
2479                // Insert wrapper before the logo
2480                mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2481                // Move both containers into the wrapper
2482                logoMenuWrapper.appendChild(logoContainer);
2483                logoMenuWrapper.appendChild(menuContainer);
2484                console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper');
2485            } else {
2486                console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper');
2487            }
2488        } else {
2489            console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists');
2490        }
2491        if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2492          applyHorizontalLogoMenuLayoutStyles();
2493        }
2494        
2495        // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created)
2496        if (!mobileHeaderContent.querySelector('.horizontal-social-container')) {
2497            const socialContainer = document.createElement('div');
2498            socialContainer.className = 'horizontal-social-container';
2499            // Insert at the end (far right) of the mobile header content
2500            mobileHeaderContent.appendChild(socialContainer);
2501        }
2502      }
2503    }
2504     // --- End Header Structure Rebuild ---
2505
2506
2507    // Re-render the correct menu structure based on the applied layout
2508    if (currentLayout === 'horizontal') {
2509      // If horizontal layout, render the horizontal menu
2510      if (typeof renderHorizontalMenu === 'function') {
2511        renderHorizontalMenu();
2512      } else {
2513        console.warn("renderHorizontalMenu function not found.");
2514      }
2515      
2516      // After rendering horizontal menu, update mobile header content
2517      // This ensures mobile header shows logo instead of menu items on mobile
2518      setTimeout(() => {
2519        if (typeof updateMobileHeaderContent === 'function') {
2520          updateMobileHeaderContent();
2521        }
2522      }, 100);
2523    } else {
2524      // For sidebar or top layouts, render the standard gallery tree
2525      if (typeof renderGalleries === 'function') {
2526        renderGalleries();
2527      } else {
2528        console.warn("renderGalleries function not found.");
2529      }
2530      
2531      // Update mobile header content for other layouts too
2532      setTimeout(() => {
2533        if (typeof updateMobileHeaderContent === 'function') {
2534          updateMobileHeaderContent();
2535        }
2536      }, 100);
2537    }
2538  } else {
2539    // Log a warning if the necessary components aren't available
2540    console.warn("MenuStyleCustomizer or its methods not available to re-apply layout.");
2541  }
2542}
2543
2544function applyLayoutStylesOnly() {
2545    // Check if MenuStyleCustomizer and its necessary parts exist
2546    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2547        // Get the current layout setting, default to 'sidebar' if not set
2548        const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2549        console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`);
2550        // Apply the layout styles (CSS classes, display properties)
2551        window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles
2552    } else {
2553        console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available.");
2554    }
2555}
2556
2557// Add this function to ensure sidebar content for mobile
2558function ensureSidebarContentForMobile() {
2559    const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
2560    const isMobileView = window.innerWidth <= 768;
2561    
2562    if (isHorizontalLayout && isMobileView) {
2563        console.log('Ensuring sidebar content for mobile horizontal layout');
2564        
2565        // Make sure the sidebar tree is populated
2566        const galleryTreeContainer = document.getElementById('galleryTree');
2567        if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) {
2568            console.log('Sidebar tree is empty, populating with galleries');
2569            
2570            // Render the galleries in the sidebar tree for mobile navigation
2571            if (typeof renderGalleries === 'function') {
2572                renderGalleries();
2573            }
2574        }
2575        
2576        // Also ensure sidebar elements are rendered
2577        if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') {
2578            console.log('Re-rendering sidebar elements for mobile');
2579            window.SidebarManager.renderElements();
2580        }
2581    }
2582}
2583
2584function renderTextLogo(container, htmlContentFromSidebarElement) {
2585  if (!container) {
2586    console.error("[RenderTextLogo] Error: Logo container not found.");
2587    return;
2588  }
2589
2590  container.innerHTML = '';
2591
2592  const raw = (htmlContentFromSidebarElement || '').trim();
2593  if (!raw) {
2594    container.innerHTML = '<span class="site-title-text">Menu</span>';
2595  } else if (!/<[a-z][sS]*>/i.test(raw)) {
2596    // Plain text — wrap so theme CSS can target .site-title-text
2597    container.innerHTML = '<span class="site-title-text">' + raw + '</span>';
2598  } else {
2599    container.innerHTML = raw;
2600    const titleEl = container.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
2601    if (titleEl && !titleEl.classList.contains('site-title-text')) {
2602      titleEl.classList.add('site-title-text');
2603    }
2604  }
2605
2606  // Do not force heading/paragraph margins to 0 — menu-styles.css matches
2607  // .sidebar-text rhythm (h1 margin-bottom 0.5em). Zeroing made the bar tighter.
2608
2609  console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`);
2610}
2611
2612function renderHorizontalMenu() {
2613  console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined');
2614  
2615  // CRITICAL: Check if we're actually in horizontal layout (theme overlay counts)
2616  const currentLayout = getEffectiveMenuLayout();
2617  
2618  const isHorizontalLayout = currentLayout === 'horizontal';
2619  console.log("[RenderHorizontalMenu] Current layout:", currentLayout, "isHorizontalLayout:", isHorizontalLayout);
2620  
2621  // If not horizontal layout, only handle logo/alignment, then return early
2622  if (!isHorizontalLayout) {
2623    console.log("[RenderHorizontalMenu] Not in horizontal layout, only handling logo/alignment, skipping menu item creation");
2624    // Still handle logo/alignment for sidebar layout
2625    const mobileHeader = document.querySelector('.mobile-header');
2626    if (mobileHeader) {
2627      const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2628      if (logoContainer) {
2629        // Handle logo rendering only (code below will handle this)
2630        // But we'll return before creating menu items
2631      }
2632    }
2633    // Return early - don't create horizontal menu items in sidebar layout
2634    return;
2635  }
2636  
2637  // Wait for sidebar elements to be available before rendering
2638  if (!window.SidebarManager || !window.SidebarManager.elements || window.SidebarManager.elements.length === 0) {
2639    console.log("[RenderHorizontalMenu] Sidebar elements not yet available, waiting...");
2640    const waitForSidebarElements = (attempts = 0) => {
2641      const maxAttempts = 20; // Increased attempts for slower loads
2642      if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2643        console.log("[RenderHorizontalMenu] Sidebar elements now available, proceeding with render.");
2644        renderHorizontalMenu();
2645      } else if (attempts < maxAttempts) {
2646        const attemptNum = attempts + 1;
2647        setTimeout(() => waitForSidebarElements(attempts + 1), 100);
2648      } else {
2649        console.warn("[RenderHorizontalMenu] Sidebar elements not available after waiting, rendering anyway.");
2650        // Continue with render even if sidebar elements aren't loaded
2651      }
2652    };
2653    waitForSidebarElements();
2654    return;
2655  }
2656  
2657  const mobileHeader = document.querySelector('.mobile-header');
2658  if (!mobileHeader) {
2659    console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND.");
2660    return;
2661  }
2662
2663  // Apply flexbox styling to position social container at far right
2664  const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2665  if (mobileHeaderContent) {
2666    mobileHeaderContent.style.display = 'flex';
2667    const isSpaceColonyTheme = isSpaceColonySiteTheme();
2668    const isMobile = window.innerWidth <= 768;
2669    mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2670    mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2671    if (isSpaceColonyTheme && !isMobile) {
2672      mobileHeaderContent.style.flexGrow = '0';
2673      mobileHeaderContent.style.width = '';
2674    } else {
2675      mobileHeaderContent.style.flexGrow = '';
2676      mobileHeaderContent.style.width = '100%';
2677    }
2678    
2679    // Create a flex container for logo and menu items
2680    const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2681    if (!logoMenuContainer) {
2682      console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...');
2683      const logoMenuWrapper = document.createElement('div');
2684      logoMenuWrapper.className = 'logo-menu-container';
2685      // Move logo and menu into the wrapper
2686      const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2687      const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu
2687-container');
2688      
2689      console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer);
2690      console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer);
2691      
2692      if (logoContainer && menuContainer) {
2693        // Insert wrapper before the logo
2694        mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2695        // Move both containers into the wrapper
2696        logoMenuWrapper.appendChild(logoContainer);
2697        logoMenuWrapper.appendChild(menuContainer);
2698        console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper');
2699      } else {
2700        console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper');
2701      }
2702    } else {
2703      console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists');
2704    }
2705    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2706      applyHorizontalLogoMenuLayoutStyles();
2707    }
2708  }
2709
2710  const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2711  if (!logoContainer) {
2712    console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header.");
2713    return;
2714  }
2715  
2716  console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container");
2717  logoContainer.innerHTML = ''; // Clear existing logo content first
2718
2719  // FIXED: Check if we're on mobile - if so, don't render the horizontal logo
2720  const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed
2721  
2722  if (isMobileView) {
2723    console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering");
2724    // On mobile, the logo will be handled by updateMobileHeaderContent()
2725  } else {
2726    console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo");
2727    
2728    // Get the logo element data
2729    let logoElementData = null;
2730    try {
2731      logoElementData = getFirstSidebarElementForHeader();
2732      console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData));
2733    } catch (error) {
2734      console.error('[RenderHorizontalMenu] Error getting sidebar element:', error);
2735      logoElementData = getFirstSidebarElementFallback();
2736    }
2737
2738     if (logoElementData) {
2739      if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) {
2740        console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl);
2741        
2742        const img = document.createElement('img');
2743        img.src = logoElementData.imageUrl || logoElementData.imageData;
2744        img.alt = resolveBrandLogoAlt(logoElementData);
2745        img.className = 'sidebar-image';
2746        
2747        // Apply comprehensive styling to ensure visibility
2748        img.style.display = 'block';
2749        
2750        // --- FIX ---
2751        // The line below was removed. It was applying a fixed max-height of 50px,
2752        // overriding your CSS. Removing it allows your stylesheet to take control.
2753        // img.style.maxHeight = '50px'; 
2754        
2755        img.style.objectFit = 'contain';
2756        img.style.minWidth = '1px';
2757        img.style.minHeight = '0';
2758
2759        let logoDisplayNode = img;
2760        if (logoElementData.colorizeWithSiteColors &&
2761            window.SidebarManager &&
2762            typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
2763          const buildData = Object.assign({}, logoElementData);
2764          if (!buildData.imageWidth) {
2765            buildData.imageWidth = 'auto';
2766          }
2767          logoDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
2768        } else {
2769          img.style.width = 'auto';
2770          img.style.visibility = 'visible';
2771          img.style.opacity = '1';
2772        }
2773
2774        // Opt-in per-image padding wrapper (no-op when unset; does not alter .horizontal-header-logo CSS)
2775        if (window.SidebarManager &&
2776            typeof window.SidebarManager._wrapWithImagePadding === 'function') {
2777          logoDisplayNode = window.SidebarManager._wrapWithImagePadding(logoDisplayNode, logoElementData);
2778        }
2779
2780        // Enhanced event listeners for debugging
2781        img.onload = () => {
2782          console.log('[RenderHorizontalMenu] Image successfully LOADED');
2783        };
2784        
2785        img.onerror = (error) => {
2786          console.error('[RenderHorizontalMenu] Image FAILED to load:', error);
2787          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
2788        };
2789
2790        try {
2791          // Handle linking (similar to sidebar-manager.js)
2792          // Only add links in view mode, not edit mode
2793          // Explicit edit toggle only — Parameters.isInEditor is also true for logged-in admins
2794          const isEditing = document.body.classList.contains('edit-mode-active') ||
2795                           (typeof window.isInEditMode === 'function' && window.isInEditMode());
2796          
2797          // Debug: Log link properties and element data
2798          console.log('[RenderHorizontalMenu] Full logoElementData:', logoElementData);
2799          console.log('[RenderHorizontalMenu] Image link properties:', {
2800            linkType: logoElementData.linkType,
2801            linkUrl: logoElementData.linkUrl,
2802            linkPageId: logoElementData.linkPageId,
2803            linkTarget: logoElementData.linkTarget,
2804            isEditing: isEditing,
2805            hasLinkType: !!logoElementData.linkType,
2806            linkTypeValue: logoElementData.linkType,
2807            linkTypeNotNone: logoElementData.linkType && logoElementData.linkType !== 'none',
2808            notEditing: !isEditing,
2809            bodyHasEditClass: document.body.classList.contains('edit-mode-active'),
2810            isInEditMode: typeof window.isInEditMode === 'function' ? window.isInEditMode() : 'N/A',
2811            ParametersIsInEditor: window.Parameters ? window.Parameters.isInEditor : 'N/A'
2812          });
2813          
2814          // Check for link - handle undefined, null, empty string, or 'none'
2815          const hasLink = logoElementData.linkType && 
2816                         logoElementData.linkType !== 'none' && 
2817                         logoElementData.linkType !== '' &&
2818                         !isEditing;
2819          
2820          console.log('[RenderHorizontalMenu] Has link?', hasLink, 'linkType:', logoElementData.linkType);
2821          
2822          if (hasLink) {
2823            const link = document.createElement('a');
2824            link.target = logoElementData.linkTarget || '_self';
2825            let finalLinkHref = '#';
2826            let isValidLink = false;
2827            
2828            if (logoElementData.linkType === 'external' && logoElementData.linkUrl) {
2829              finalLinkHref = logoElementData.linkUrl;
2830              if (link.target === '_blank') {
2831                link.rel = 'noopener noreferrer';
2832              }
2833              isValidLink = true;
2834            } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2835              const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2836              const linkPageIdNum = parseInt(logoElementData.linkPageId);
2837              console.log('[RenderHorizontalMenu] Looking for internal page:', {
2838                linkPageId: logoElementData.linkPageId,
2839                linkPageIdNum: linkPageIdNum,
2840                allGalleriesLength: allGalleries ? allGalleries.length : 0,
2841                galleryIds: allGalleries ? allGalleries.map(g => ({ id: g.id, title: g.title })).slice(0, 5) : []
2842              });
2843              
2844              const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2845              
2846              console.log('[RenderHorizontalMenu] Target page found:', targetPage ? { id: targetPage.id, title: targetPage.title, slug: targetPage.slug, url: targetPage.url } : null);
2847              
2848              if (targetPage) {
2849                // Handle preview mode URLs
2850                const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
2851                console.log('[RenderHorizontalMenu] Preview mode:', isPreviewMode);
2852                
2853                if (isPreviewMode) {
2854                  const pathParts = window.location.pathname.split('/').filter(Boolean);
2855                  const siteGuid = pathParts[0];
2856                  console.log('[RenderHorizontalMenu] Preview mode path parts:', pathParts, 'siteGuid:', siteGuid, 'targetPage.slug:', targetPage.slug);
2857                  
2858                  if (siteGuid && targetPage.slug) {
2859                    finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
2860                    isValidLink = true;
2861                    console.log('[RenderHorizontalMenu] Set preview href:', finalLinkHref);
2862                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2863                    finalLinkHref = targetPage.url;
2864                    isValidLink = true;
2865                    console.log('[RenderHorizontalMenu] Set preview href from url:', finalLinkHref);
2866                  }
2867                } else {
2868                  if (targetPage.slug) {
2869                    finalLinkHref = '/' + targetPage.slug;
2870                    isValidLink = true;
2871                    console.log('[RenderHorizontalMenu] Set non-preview href:', finalLinkHref);
2872                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2873                    finalLinkHref = targetPage.url;
2874                    isValidLink = true;
2875                    console.log('[RenderHorizontalMenu] Set non-preview href from url:', finalLinkHref);
2876                  }
2877                }
2878              } else {
2879                console.warn('[RenderHorizontalMenu] Target page not found for linkPageId:', logoElementData.linkPageId);
2880              }
2881            }
2882            
2883            if (isValidLink) {
2884              link.href = finalLinkHref;
2885              // Ensure link is clickable
2886              link.style.pointerEvents = 'auto';
2887              link.style.cursor = 'pointer';
2888              link.style.textDecoration = 'none';
2889              link.style.display = 'flex';
2890              link.style.alignItems = 'center';
2891              link.style.height = 'auto';
2892              link.style.maxHeight = '100%';
2893              link.style.minHeight = '0';
2894              link.style.lineHeight = '0';
2895              console.log('[RenderHorizontalMenu] Link created with href:', finalLinkHref, 'target:', link.target, 'styles applied');
2896              
2897              // For internal links, add click handler to use site navigation
2898              if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2899                // Store linkPageId in a variable that won't be affected by closure
2900                const linkPageId = logoElementData.linkPageId;
2901                const linkHref = finalLinkHref;
2902                
2903                // Add click handler with both capture and bubble phases to ensure it fires
2904                const clickHandler = (e) => {
2905                  console.log('[RenderHorizontalMenu] Link clicked! linkPageId:', linkPageId, 'event:', e);
2906                  e.preventDefault();
2907                  e.stopPropagation();
2908                  e.stopImmediatePropagation();
2909                  
2910                  const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2911                  const linkPageIdNum = parseInt(linkPageId);
2912                  const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2913                  
2914                  console.log('[RenderHorizontalMenu] Click handler - targetPage found:', targetPage ? { id: targetPage.id, title: targetPage.title, isPage: targetPage.isPage } : null);
2915                  console.log('[RenderHorizontalMenu] Click handler - loadPage function:', typeof window.loadPage, 'loadGallery function:', typeof window.loadGallery);
2916                  
2917                  if (targetPage) {
2918                    if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
2919                      console.log('[RenderHorizontalMenu] Calling loadPage with id:', targetPage.id);
2920                      window.loadPage(targetPage.id);
2921                    } else if (typeof window.loadGallery === 'function') {
2922                      console.log('[RenderHorizontalMenu] Calling loadGallery with id:', targetPage.id);
2923                      window.loadGallery(targetPage.id);
2924                    } else {
2925                      // Fallback to standard navigation
2926                      console.log('[RenderHorizontalMenu] Fallback to window.location.href:', linkHref);
2927                      window.location.href = linkHref;
2928                    }
2929                  } else {
2930                    // Fallback to standard navigation if page not found
2931                    console.warn('[RenderHorizontalMenu] Target page not found in click handler, using fallback href:', linkHref);
2932                    window.location.href = linkHref;
2933                  }
2934                  
2935                  return false;
2936                };
2937                
2938                // Add handler in capture phase to catch event early
2939                link.addEventListener('click', clickHandler, true);
2940                // Also add in bubble phase as backup
2941                link.addEventListener('click', clickHandler, false);
2942                
2943                // Also add mousedown as backup in case click is being prevented
2944                link.addEventListener('mousedown', (e) => {
2945                  if (e.button === 0) { // Left click only
2946                    console.log('[RenderHorizontalMenu] MouseDown detected on link, triggering click handler');
2947                    clickHandler(e);
2948                  }
2949                });
2950                
2951                console.log('[RenderHorizontalMenu] Click handlers attached to link');
2952              }
2953              
2954              link.appendChild(logoDisplayNode);
2955              logoContainer.appendChild(link);
2956              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer. Link element:', link, 'href:', link.href, 'parent:', logoContainer);
2957              
2958              // Verify link is in DOM and clickable
2959              setTimeout(() => {
2960                const verifyLink = logoContainer.querySelector('a');
2961                if (verifyLink) {
2962                  console.log('[RenderHorizontalMenu] Link verification - Found in DOM:', {
2963                    href: verifyLink.href,
2964                    pointerEvents: window.getComputedStyle(verifyLink).pointerEvents,
2965                    cursor: window.getComputedStyle(verifyLink).cursor,
2966                    zIndex: window.getComputedStyle(verifyLink).zIndex,
2967                    position: window.getComputedStyle(verifyLink).position
2968                  });
2969                } else {
2970                  console.warn('[RenderHorizontalMenu] Link verification - Link NOT found in DOM!');
2971                }
2972              }, 100);
2973            } else {
2974              console.warn('[RenderHorizontalMenu] Link is not valid, appending image without link. isValidLink:', isValidLink);
2975              logoContainer.appendChild(logoDisplayNode);
2976              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
2977            }
2978          } else {
2979            logoContainer.appendChild(logoDisplayNode);
2980            console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
2981          }
2982        } catch (e) {
2983          console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e);
2984          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
2985        }
2986        
2987      } else if (logoElementData.type === 'text' && logoElementData.textContent) {
2988        console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent);
2989        renderTextLogo(logoContainer, logoElementData.textContent);
2990        
2991      } else {
2992        console.log('[RenderHorizontalMenu] Using title as fallback logo');
2993        renderTextLogo(logoContainer, logoElementData.title || 'Menu');
2994      }
2995    } else {
2996      console.log("[RenderHorizontalMenu] No logoElementData available. Using default text.");
2997      renderTextLogo(logoContainer, 'Menu');
2998    }
2999  }
3000
3001  // Render menu items (only on desktop, not on mobile)
3002  const menuContainer = mobileHeader.querySelector('.horizontal-menu-container');
3003  if (!menuContainer) {
3004    console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header.");
3005  } else {
3006    menuContainer.innerHTML = '';
3007    
3008    // Apply alignment class from settings
3009    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
3010      const rawAlign = window.MenuStyleCustomizer.settings.horizontalMenuAlignment || 'left';
3011      const alignment = ['left', 'center', 'right'].indexOf(rawAlign) >= 0 ? rawAlign : 'left';
3012      menuContainer.classList.remove('menu-align-left', 'menu-align-right', 'menu-align-center');
3013      menuContainer.classList.add('menu-align-' + alignment);
3014      console.log('[RenderHorizontalMenu] Applied alignment class: menu-align-' 
3014+ alignment);
3015    }
3016    
3017    // Only populate menu items on desktop, not on mobile
3018    if (!isMobileView) {
3019      let galleryTree = [];
3020      if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') {
3021        galleryTree = createGalleryTree(galleries);
3022      } else {
3023        console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined.");
3024      }
3025      
3026      galleryTree.forEach(item => {
3027        const menuItemElement = createHorizontalMenuItem(item);
3028        if (menuItemElement) {
3029          menuContainer.appendChild(menuItemElement);
3030        }
3031      });
3032      if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
3033    } else {
3034      console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population");
3035    }
3036  }
3037
3038  // Render social icons (only on desktop, not on mobile)
3039  let socialContainer = mobileHeader.querySelector('.horizontal-social-container');
3040  if (!socialContainer) {
3041    // Fallback: create the social container if it doesn't exist
3042    console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it...");
3043    const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
3044    if (mobileHeaderContent) {
3045      socialContainer = document.createElement('div');
3046      socialContainer.className = 'horizontal-social-container';
3047      mobileHeaderContent.appendChild(socialContainer);
3048      console.log("[RenderHorizontalMenu] Created .horizontal-social-container");
3049    } else {
3050      console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container");
3051    }
3052  }
3053  
3054  if (socialContainer) {
3055    socialContainer.innerHTML = '';
3056    
3057    // Only populate social icons on desktop, not on mobile
3058    if (!isMobileView) {
3059      const socialElement = getFirstSocialElementForHeader();
3060      if (socialElement && socialElement.type === 'social') {
3061        console.log('[RenderHorizontalMenu] Found social element, rendering social icons');
3062        renderSocialIcons(socialContainer, socialElement);
3063      } else {
3064        console.log('[RenderHorizontalMenu] No social element found or element is not social type');
3065        // Container remains empty - no impact on layout
3066      }
3067    } else {
3068      console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population");
3069    }
3070  }
3071
3072
3073
3074  if (typeof updateActiveStatesHorizontal === 'function') {
3075    updateActiveStatesHorizontal();
3076  }
3077
3078  if (!isMobileView && mobileHeaderContent) {
3079    mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title').forEach(function(el) {
3080      if (!el.closest('.horizontal-header-logo')) {
3081        el.remove();
3082      }
3083    });
3084  }
3085  
3086setTimeout(() => {
3087  ensureSidebarContentForMobile();
3088}, 100);
3089
3090  if (typeof syncContentMarginToHorizontalHeader === 'function') {
3091    setTimeout(function() { syncContentMarginToHorizontalHeader(); }, 200);
3092  }
3093
3094  console.log("[RenderHorizontalMenu] Function COMPLETED.");
3095}
3096
3097/**
3098 * Renders social icons in the horizontal header container.
3099 * @param {HTMLElement} container - The container to render social icons in
3100 * @param {object} socialElement - The social element data from sidebar
3101 */
3102function renderSocialIcons(container, socialElement) {
3103  console.log('[RenderSocialIcons] Rendering social icons:', socialElement);
3104  
3105  try {
3106    // Safety check: ensure we have a valid social element
3107    if (!socialElement || socialElement.type !== 'social') {
3108      console.log('[RenderSocialIcons] Invalid social element - skipping rendering');
3109      return;
3110    }
3111
3112    /* Placeholders without URLs still need menu color (CSS also sets this). */
3113    container.style.color = 'var(--menu-color, #333)';
3114    
3115    // Get icon size with proper fallbacks
3116    let iconSize = 30;
3117    if (typeof socialElement.socialIconSize === 'number' && !isNaN(socialElement.socialIconSize)) {
3118      iconSize = socialElement.socialIconSize;
3119    } else if (typeof socialElement.socialIconSize === 'string' && !isNaN(parseInt(socialElement.socialIconSize))) {
3120      iconSize = parseInt(socialElement.socialIconSize);
3121    }
3122    
3123    // Render icons directly from element data
3124    if (socialElement.socialIcons && Array.isArray(socialElement.socialIcons)) {
3125      socialElement.socialIcons.forEach(icon => {
3126        // Create icon wrapper (color via CSS --menu-color so placeholders without URLs stay visible)
3127        const iconWrapper = document.createElement('div');
3128        iconWrapper.className = 'social-icon-wrapper';
3129        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;';
3130
3131        const applySvgSizing = (host) => {
3132          const svg = host.querySelector('svg');
3133          if (!svg) return;
3134          svg.style.width = '100%';
3135          svg.style.height = '100%';
3136          svg.setAttribute('width', '100%');
3137          svg.setAttribute('height', '100%');
3138          svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
3139          svg.setAttribute('aria-hidden', 'true');
3140          svg.setAttribute('focusable', 'false');
3141          svg.style.color = 'inherit';
3142          svg.style.fill = 'currentColor';
3143          svg.style.stroke = 'currentColor';
3144        };
3145
3146        const hasUrl = !!(icon.url && String(icon.url).trim());
3147
3148        // Add link if URL exists; otherwise still render the glyph (placeholder icons)
3149        if (hasUrl) {
3150          const link = document.createElement('a');
3151          link.href = icon.url;
3152          link.target = '_blank';
3153          link.rel = 'noopener noreferrer';
3154          link.classList.add('horizontal-social-link');
3155          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;';
3156          const socialLabel = (icon.alt && String(icon.alt).trim())
3157            || (window.SidebarManager && typeof window.SidebarManager._socialIconTypeLabel === 'function'
3158              ? window.SidebarManager._socialIconTypeLabel(icon.type)
3159              : (icon.type && icon.type !== 'custom' ? String(icon.type).replace(/_/g, ' ') : ''))
3160            || 'Social link';
3161          const socialLabelPretty = socialLabel.charAt(0).toUpperCase() + socialLabel.slice(1);
3162          link.setAttribute('aria-label', socialLabelPretty + ' (opens in new tab)');
3163          
3164          if (icon.type === 'custom' && icon.iconUrl) {
3165            // Render uploaded image
3166            const img = document.createElement('img');
3167            img.src = icon.iconUrl;
3168            img.alt = '';
3169            img.setAttribute('aria-hidden', 'true');
3170            img.style.width = '100%';
3171            img.style.height = '100%';
3172            img.style.objectFit = 'contain';
3173            link.appendChild(img);
3174          } else {
3175            // Add SVG - use SidebarManager's method if available, otherwise create basic SVG
3176            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
3177              link.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
3178            } else {
3179              // Fallback: create a simple SVG placeholder
3180              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>';
3181            }
3182            applySvgSizing(link);
3183          }
3184          iconWrapper.appendChild(link);
3185        } else {
3186          // Placeholder: no URL — still paint with menu color via container inheritance
3187          iconWrapper.setAttribute('aria-hidden', 'true');
3188          if (icon.type === 'custom' && icon.iconUrl) {
3189            const img = document.createElement('img');
3190            img.src = icon.iconUrl;
3191            img.alt = '';
3192            img.style.width = '100%';
3193            img.style.height = '100%';
3194            img.style.objectFit = 'contain';
3195            iconWrapper.appendChild(img);
3196          } else {
3197            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
3198              iconWrapper.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
3199            } else {
3200              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>';
3201            }
3202            applySvgSizing(iconWrapper);
3203          }
3204        }
3205        
3206        container.appendChild(iconWrapper);
3207        console.log('[RenderSocialIcons] Rendered social icon:', icon.type, icon.url);
3208      });
3209    } else {
3210      console.log('[RenderSocialIcons] No social icons found in element');
3211    }
3212  } catch (error) {
3213    console.error('[RenderSocialIcons] Error rendering social icons:', error);
3214  }
3215}
3216
3217/**
3218 * Gets the appropriate icon text or class for a social platform.
3219 * @param {string} platform - The social platform name
3220 * @returns {string} The icon text or class
3221 */
3222function getSocialIconText(platform) {
3223  const iconMap = {
3224    'facebook': 'f',
3225    'twitter': 't',
3226    'instagram': 'i',
3227    'linkedin': 'in',
3228    'youtube': 'yt',
3229    'pinterest': 'p',
3230    'tiktok': 'tt',
3231    'snapchat': 'sc',
3232    'whatsapp': 'wa',
3233    'telegram': 'tg',
3234    'discord': 'd',
3235    'github': 'gh',
3236    'email': '@',
3237    'phone': '📞',
3238    'website': '🌐'
3239  };
3240  
3241  return iconMap[platform.toLowerCase()] || '•';
3242}
3243
3244
3245
3246
3247/**
3248 * Creates a single top-level horizontal menu item and its submenu if it has children.
3249 * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal)
3250 * @param {object} galleryItem - The gallery item data.
3251 * @returns {HTMLElement | null} The created menu item element, or null if not rendered.
3252 */
3253function createHorizontalMenuItem(galleryItem) {
3254  if (galleryItem.visible === false || galleryItem.isSpacer) {
3255      return null;
3256  }
3257
3258  const menuItem = document.createElement('div');
3259  menuItem.className = 'horizontal-menu-item';
3260  menuItem.dataset.id = String(galleryItem.id);
3261  menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3262
3263  const hasVisibleChildren = !!(galleryItem.children && galleryItem.children.filter(c => c.visible !== false && !c.isSpacer).length);
3264  const isNavigableParent = isNavigableMenuParent(galleryItem);
3265  let titleEl;
3266  if (hasVisibleChildren && !isNavigableParent) {
3267    titleEl = document.createElement('span');
3268    titleEl.className = 'horizontal-menu-label';
3269    titleEl.textContent = galleryItem.title;
3270    menuItem.setAttribute('aria-haspopup', 'true');
3271    menuItem.setAttribute('aria-expanded', 'false');
3272  } else {
3273    titleEl = document.createElement('a');
3274    titleEl.className = 'horizontal-menu-label';
3275    titleEl.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3276    titleEl.textContent = galleryItem.title;
3277    if (galleryItem.isExternal) {
3278      titleEl.target = '_blank';
3279      titleEl.rel = 'noopener noreferrer';
3280    } else {
3281      titleEl.addEventListener('click', (e) => { e.preventDefault(); });
3282    }
3283  }
3284  menuItem.appendChild(titleEl);
3285
3286  // Ensure activeGalleryId is treated as a number for comparison during initial render
3287  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3288  if (galleryItem.id === currentGlobalActiveId) {
3289      menuItem.classList.add('active');
3290  }
3291
3292  let leaveTimer; // For mouseleave timeout to close dropdown
3293
3294  if (galleryItem.children && galleryItem.children.length > 0) {
3295      const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer);
3296
3297      if (visibleChildren.length > 0) {
3298          menuItem.classList.add('has-children');
3299          if (isNavigableParent) {
3300              menuItem.setAttribute('aria-haspopup', 'true');
3301              menuItem.setAttribute('aria-expanded', 'false');
3302          }
3303
3304          const toggleIconContainer = document.createElement(isNavigableParent ? 'button' : 'span');
3305          toggleIconContainer.className = 'submenu-toggle-horizontal';
3306          if (isNavigableParent) {
3307              toggleIconContainer.type = 'button';
3308              toggleIconContainer.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3309              toggleIconContainer.setAttribute('aria-expanded', 'false');
3310              toggleIconContainer.addEventListener('click', function(event) {
3311                  event.preventDefault();
3312                  event.stopPropagation();
3313                  const willExpand = !menuItem.classList.contains('expanded');
3314                  menuItem.classList.toggle('expanded', willExpand);
3315                  menuItem.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3316                  toggleIconContainer.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3317                  const icon = toggleIconContainer.querySelector('.icon.toggle-icon');
3318                  if (icon) icon.classList.toggle('rotated', willExpand);
3319              });
3320          }
3321          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3322          svgIcon.setAttribute("class", "icon toggle-icon");
3323          svgIcon.setAttribute("viewBox", "0 0 24 24");
3324          svgIcon.setAttribute("fill", "none");
3325          svgIcon.setAttribute("stroke", "currentColor");
3326          svgIcon.setAttribute("stroke-width", "2");
3327          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3328          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3329          svgIcon.appendChild(polyline);
3330          toggleIconContainer.appendChild(svgIcon);
3331          menuItem.appendChild(toggleIconContainer);
3332
3333          const subMenu = document.createElement('div');
3334          subMenu.className = 'horizontal-dropdown';
3335          const subMenuList = document.createElement('ul');
3336          subMenuList.className = 'horizontal-dropdown-list';
3337
3338          visibleChildren.forEach((childItem) => {
3339              const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call
3340              if (subMenuItemElement) {
3341                  subMenuList.appendChild(subMenuItemElement);
3342              }
3343          });
3344
3345          if (subMenuList.hasChildNodes()) {
3346              subMenu.appendChild(subMenuList);
3347              menuItem.appendChild(subMenu);
3348
3349              menuItem.addEventListener('mouseenter', function() {
3350                  clearTimeout(leaveTimer);
3351                  document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3352                      if (openItem !== this) {
3353                          openItem.classList.remove('expanded');
3354                          openItem.setAttribute('aria-expanded', 'false');
3355                          const otherIcon = openItem.querySelector('.icon.toggle-icon');
3356                          if (otherIcon) otherIcon.classList.remove('rotated');
3357                      }
3358                  });
3359                  this.classList.add('expanded');
3360                  this.setAttribute('aria-expanded', 'true');
3361                  if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'true');
3362                  const currentIcon = this.querySelector('.icon.toggle-icon');
3363                  if (currentIcon) currentIcon.classList.add('rotated');
3364              });
3365
3366              menuItem.addEventListener('mouseleave', function(event) {
3367                  // Check if we're moving to the dropdown or a flyout
3368                  const relatedTarget = event.relatedTarget;
3369                  const isMovingToDropdown = relatedTarget && (
3370                      relatedTarget.closest('.horizontal-dropdown') ||
3371                      relatedTarget.classList.contains('horizontal-submenu-item') ||
3372                      relatedTarget.closest('.horizontal-submenu-item')
3373                  );
3374                  const isMovingToFlyout = relatedTarget && (
3375                      relatedTarget.classList.contains('horizontal-nested-dropdown') ||
3376                      relatedTarget.closest('.horizontal-nested-dropdown')
3377                  );
3378                  
3379                  if (!isMovingToDropdown && !isMovingToFlyout) {
3380                      console.log('Main menu item mouseleave - starting timer');
3381                      leaveTimer = setTimeout(() => {
3382                          // Check if there's an active flyout - if so, keep parent open
3383                          // Use the same multiple detection methods as the submenu handler
3384                          const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3385                                             document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3386                                             document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3387                          console.log('Main menu timer fired - active flyout:', activeFlyout);
3388                          if (!activeFlyout) {
3389                              console.log('Main menu closing dropdown - no active flyout');
3390                              this.classList.remove('expanded');
3391                              this.setAttribute('aria-expanded', 'false');
3392                              if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'false');
3393                              const currentIcon = this.querySelector('.icon.toggle-icon');
3394                              if (currentIcon) currentIcon.classList.remove('rotated');
3395                          } else {
3396                              console.log('Main menu keeping dropdown open - flyout is active');
3397                          }
3398                      }, 200);
3399                  } else {
3400                      console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)');
3401                  }
3402              });
3403
3404              subMenu.addEventListener('mouseenter', function() {
3405                  const parentMenuItem = this.closest('.horizontal-menu
3405-item');
3406                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3407                      clearTimeout(parentMenuItem.leaveTimer);
3408                      parentMenuItem.leaveTimer = null;
3409                  }
3410              });
3411
3412              subMenu.addEventListener('mouseleave', function(event) {
3413                  const parentMenuItem = this.closest('.horizontal-menu-item');
3414                  if (parentMenuItem) {
3415                      // Check if we're moving to another element within the same dropdown
3416                      const relatedTarget = event.relatedTarget;
3417                      console.log('SubMenu mouseleave - relatedTarget:', relatedTarget);
3418                      console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null');
3419                      
3420                      // Check if we're moving to another element within the same dropdown
3421                      const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown');
3422                      console.log('Current dropdown found:', !!currentDropdown);
3423                      console.log('Current dropdown element:', currentDropdown);
3424                      console.log('RelatedTarget:', relatedTarget);
3425                      console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null');
3426                      
3427                      // Check if relatedTarget is within the same dropdown container
3428                      const isMovingWithinDropdown = relatedTarget && currentDropdown && (
3429                          relatedTarget.closest('.horizontal-dropdown') === currentDropdown ||
3430                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3431                          relatedTarget.closest('.horizontal-submenu-item') ||
3432                          currentDropdown.contains(relatedTarget)
3433                      );
3434                      
3435                      // Alternative approach: check if relatedTarget is within the same parent menu item
3436                      const parentMenuContainer = this.closest('.horizontal-menu-item');
3437                      const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget);
3438                      console.log('Is moving within parent menu item:', isMovingWithinParent);
3439                      console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown');
3440                      console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null');
3441                      
3442                      // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside)
3443                      // Get the parent menu item's dropdown container
3444                      const parentMenuItem = this.closest('.horizontal-menu-item');
3445                      console.log('Parent menu item found:', !!parentMenuItem);
3446                      const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null;
3447                      console.log('Dropdown container found:', !!dropdownContainer);
3448                      if (dropdownContainer) {
3449                          console.log('Dropdown container classes:', dropdownContainer.className);
3450                          console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display);
3451                          console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position);
3452                      }
3453                      
3454                      // Get bounds from the visible dropdown (not the hidden one)
3455                      let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 };
3456                      if (dropdownContainer) {
3457                          const computedStyle = window.getComputedStyle(dropdownContainer);
3458                          if (computedStyle.display !== 'none') {
3459                              dropdownRect = dropdownContainer.getBoundingClientRect();
3460                          } else {
3461                              // If dropdown is hidden, try to get bounds from the parent menu item
3462                              const parentRect = parentMenuItem.getBoundingClientRect();
3463                              dropdownRect = parentRect;
3464                          }
3465                      }
3466                      const mouseX = event.clientX;
3467                      const mouseY = event.clientY;
3468                      const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && 
3469                                                     mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom;
3470                      
3471                      console.log('Dropdown bounds:', {
3472                          left: dropdownRect.left,
3473                          right: dropdownRect.right,
3474                          top: dropdownRect.top,
3475                          bottom: dropdownRect.bottom,
3476                          mouseX,
3477                          mouseY,
3478                          mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom
3479                      });
3480                      
3481                      // If there's an active flyout, always keep the parent dropdown open
3482                      // Check multiple ways to detect active flyouts
3483                      const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3484                                         document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3485                                         document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3486                      console.log('Active flyout found:', !!activeFlyout);
3487                      if (activeFlyout) {
3488                          console.log('Active flyout display style:', activeFlyout.style.display);
3489                          console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display);
3490                          console.log('Active flyout visibility style:', activeFlyout.style.visibility);
3491                      }
3492                      const hasActiveFlyout = !!activeFlyout;
3493                      
3494                      // If the dropdown is hidden (display: none), we can't get accurate bounds
3495                      // So let's use a simpler approach - if we're moving to another submenu item, keep open
3496                      const isMovingToSubmenuItem = relatedTarget && (
3497                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3498                          relatedTarget.closest('.horizontal-submenu-item')
3499                      );
3500                      console.log('Is moving to submenu item:', isMovingToSubmenuItem);
3501                      console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null');
3502                      console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null');
3503                      
3504                      // If we're moving to another submenu item within the same dropdown, al
3504ways keep open
3505                      // OR if there's an active flyout (regardless of where the mouse is), keep open
3506                      // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open
3507                      const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem;
3508                      
3509                      console.log('Is moving within dropdown:', isMovingWithinDropdown);
3510                      console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds);
3511                      console.log('Has active flyout:', hasActiveFlyout);
3512                      console.log('Should keep open:', shouldKeepOpen);
3513                      
3514                      if (!shouldKeepOpen) {
3515                          console.log('SubMenu mouseleave - starting timer (moving outside dropdown)');
3516                          parentMenuItem.leaveTimer = setTimeout(() => {
3517                              // Check if there's an active flyout - if so, keep parent open
3518                              const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3519                              console.log('Timer fired - active flyout:', activeFlyout);
3520                              if (!activeFlyout) {
3521                                  console.log('Closing parent dropdown - no active flyout');
3522                                  parentMenuItem.classList.remove('expanded');
3523                                  const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3524                                  if (currentIcon) currentIcon.classList.remove('rotated');
3525                              } else {
3526                                  console.log('Keeping parent dropdown open - flyout is active');
3527                              }
3528                          }, 200);
3529                      } else {
3530                          console.log('SubMenu mouseleave - not starting timer (moving within dropdown)');
3531                      }
3532                  }
3533              });
3534          } else {
3535              menuItem.classList.remove('has-children');
3536              if (toggleIconContainer.parentNode) {
3537                  toggleIconContainer.remove();
3538              }
3539          }
3540      }
3541  }
3542
3543  // Click listener for items that are direct navigation links (not folders with children)
3544  if (!menuItem.classList.contains('has-children') || isNavigableParent) {
3545      menuItem.addEventListener('click', async (event) => {
3546          if (isNavigableParent && hasVisibleChildren && !titleEl.contains(event.target)) {
3547              return;
3548          }
3549          event.stopPropagation();
3550          const clickedItemId = galleryItem.id; // Capture the ID of the clicked item
3551
3552          // Check if it's an external URL first
3553          if (galleryItem.isExternal && galleryItem.url) {
3554              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
3555              return;
3556          }
3557
3558          if (isPageMenuItem(galleryItem)) {
3559              if (typeof loadPage === 'function') {
3560                  loadPage(clickedItemId, event); // loadPage is synchronous
3561                  if (typeof updateActiveStatesHorizontal === 'function') {
3562                      console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
3563                      // Use a minimal delay even for sync operations if page rendering has its own async microtasks
3564                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
3565                  }
3566              }
3567          } else { // It's a gallery
3568              if (typeof loadGallery === 'function') {
3569                  console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
3570                  await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery
3571                  console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
3572                  if (typeof updateActiveStatesHorizontal === 'function') {
3573                      setTimeout(() => {
3574                          console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
3575                          updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId
3576                      }, 100); // Delay to allow gallery system to settle
3577                  }
3578              }
3579          }
3580
3581          // Close any other open dropdowns and flyouts
3582          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3583              // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click)
3584              if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) {
3585                 openItem.classList.remove('expanded');
3586                 openItem.setAttribute('aria-expanded', 'false');
3587                 const icon = openItem.querySelector('.icon.toggle-icon');
3588                 if (icon) icon.classList.remove('rotated');
3589              }
3590          });
3591          
3592          // Close all flyouts
3593          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
3594              flyout.style.display = 'none';
3595              flyout.style.opacity = '0';
3596              flyout.style.visibility = 'hidden';
3597          });
3598          
3599          // Remove expanded class from all submenu items
3600          document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => {
3601              subItem.classList.remove('expanded');
3602          });
3603      });
3604  } else {
3605      // Keyboard: Enter/Space toggles hover-style dropdown without changing mouse behavior
3606      menuItem.setAttribute('tabindex', '0');
3607      menuItem.addEventListener('keydown', function(event) {
3608          if (event.key !== 'Enter' && event.key !== ' ') return;
3609          event.preventDefault();
3610          const isOpen = this.classList.contains('expanded');
3611          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3612              if (openItem !== this) {
3613                  openItem.classList.remove('expanded');
3614                  openItem.setAttribute('aria-expanded', 'false');
3615                  const otherIcon = openItem.querySelector('.icon.toggle-icon');
3616                  if (otherIcon) otherIcon.classList.remove('rotated');
3617              }
3618          });
3619          this.classList.toggle('expanded', !isOpen);
3620          this.setAttribute('aria-expanded', !isOpen ? 'true' : 'false');
3621          const currentIcon = this.querySelector('.icon.toggle-icon');
3622          if (currentIcon) currentIcon.classList.toggle('rotated', !isOpen);
3623      });
3624  }
3625  return menuItem;
3626}
3627
3628/**
3629* Creates a single horizontal submenu item.
3630* (UPDATED to handle nested children and async loadGallery)
3631* @param {object} galleryItem - The gallery item data for the submenu.
3632* @returns {HTMLElement | null} The created submenu item element, or null if not rendered.
3633*/
3634function createHorizontalSubMenuItem(galleryItem) {
3635  if (galleryItem.visible === false || galleryItem.isSpacer) {
3636      return null;
3637  }
3638
3639  const subMenuItem = document.createElement('li');
3640  subMenuItem.className = 'horizontal-submenu-item';
3641  subMenuItem.dataset.id = String(galleryItem.id);
3642  subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3643
3644  const subVisibleChildren = (galleryItem.children || []).filter(child => child.visible !== false && !child.isSpacer);
3645  const isNavigableParent = isNavigableMenuParent(galleryItem);
3646  let titleSpan;
3647  if (subVisibleChildren.length > 0 && !isNavigableParent) {
3648      titleSpan = document.createElement('span');
3649      titleSpan.className = 'horizontal-menu-label';
3650      titleSpan.textContent = galleryItem.title;
3651  } else {
3652      titleSpan = document.createElement('a');
3653      titleSpan.className = 'horizontal-menu-label';
3654      titleSpan.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3655      titleSpan.textContent = galleryItem.title;
3656      if (galleryItem.isExternal) {
3657          titleSpan.target = '_blank';
3658          titleSpan.rel = 'noopener noreferrer';
3659      } else {
3660          titleSpan.addEventListener('click', (e) => { e.preventDefault(); });
3661      }
3662  }
3663  subMenuItem.appendChild(titleSpan);
3664
3665  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3666  if (galleryItem.id === currentGlobalActiveId) {
3667      subMenuItem.classList.add('active');
3668      if (titleSpan.tagName === 'A') titleSpan.setAttribute('aria-current', 'page');
3669  }
3670
3671  // Check if this submenu item has children (nested sub-sub items)
3672  if (galleryItem.children && galleryItem.children.length > 0) {
3673      const visibleChildren = subVisibleChildren;
3674      
3675      if (visibleChildren.length > 0) {
3676          subMenuItem.classList.add('has-children');
3677          
3678          // Add arrow indicator for nested items (same as top-level menu)
3679          const arrowSpan = document.createElement(isNavigableParent ? 'button' : 'span');
3680          arrowSpan.className = 'submenu-arrow';
3681          if (isNavigableParent) {
3682              arrowSpan.type = 'button';
3683              arrowSpan.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3684              arrowSpan.setAttribute('aria-expanded', 'false');
3685              arrowSpan.addEventListener('click', function(event) {
3686                  event.preventDefault();
3687                  event.stopPropagation();
3688                  const isOpen = subMenuItem.classList.contains('expanded');
3689                  if (isOpen) {
3690                      closeHorizontalNestedFlyout(subMenuItem);
3691                  } else {
3692                      openHorizontalNestedFlyout(subMenuItem);
3693                  }
3694              });
3695          }
3696          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3697          svgIcon.setAttribute("class", "icon toggle-icon");
3698          svgIcon.setAttribute("viewBox", "0 0 24 24");
3699          svgIcon.setAttribute("fill", "none");
3700          svgIcon.setAttribute("stroke", "currentColor");
3701          svgIcon.setAttribute("stroke-width", "2");
3702          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3703          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3704          svgIcon.appendChild(polyline);
3705          arrowSpan.appendChild(svgIcon);
3706          subMenuItem.appendChild(arrowSpan);
3707          
3708          // Create nested submenu as a true flyout
3709          const nestedSubMenu = document.createElement('div');
3710          nestedSubMenu.className = 'horizontal-nested-dropdown';
3711          
3712          // Create a list container for the nested items
3713          const nestedList = document.createElement('ul');
3714          nestedList.className = 'horizontal-nested-list';
3715          
3716          visibleChildren.forEach((childItem) => {
3717              const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call
3718              if (nestedSubMenuItem) {
3719                  nestedList.appendChild(nestedSubMenuItem);
3720              }
3721          });
3722          
3723          nestedSubMenu.appendChild(nestedList);
3724          
3725          if (nestedSubMenu.hasChildNodes()) {
3726              // Append to document body for true flyout behavior
3727              document.body.appendChild(nestedSubMenu);
3728              // Store reference to the flyout for cleanup
3729              const flyoutId = 'flyout-' + Date.now() + '-' + Math.random();
3730              subMenuItem.setAttribute('data-flyout-id', flyoutId);
3731              nestedSubMenu.setAttribute('data-flyout-id', flyoutId);
3732              
3733              console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug
3734              console.log('Flyout element:', nestedSubMenu); // Debug
3735              
3736              // Add hover events for nested submenu
3737              let nestedLeaveTimer;
3738
3739              // Shared open/close used by hover AND keyboard — mouse path unchanged
3740              function openHorizontalNestedFlyout(hostItem) {
3741                  if (!hostItem) return;
3742                  if (hostItem.nestedLeaveTimer) {
3743                      clearTimeout(hostItem.nestedLeaveTimer);
3744                  }
3745                  const parentMenuItem = hostItem.closest('.horizontal-menu-item');
3746                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3747                      clearTimeout(parentMenuItem.leaveTimer);
3748                  }
3749                  document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => {
3750                      if (openItem !== hostItem) {
3751                          openItem.classList.remove('expanded');
3752                          openItem.setAttribute('aria-expanded', 'false');
3753                          const otherFlyoutId = openItem.getAttribute('data-flyout-id');
3754                          if (otherFlyoutId) {
3755                              const otherFlyout = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + otherFlyoutId + '"]');
3756                              if (otherFlyout) {
3757                                  otherFlyout.style.display = 'none';
3758                                  otherFlyout.style.opacity = '0';
3759                                  otherFlyout.style.visibility = 'hidden';
3760                              }
3761                          }
3762                      }
3763                  });
3764                  hostItem.classList.add('expanded');
3765                  hostItem.setAttribute('aria-expanded', 'true');
3766                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3767                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'true');
3768                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3769                  if (!hostFlyoutId) return;
3770                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3771                  if (!nestedDropdown) return;
3772                  const rect = hostItem.getBoundingClientRect();
3773                  const parentDropdown = hostItem.closest('.horizontal-dropdown');
3774                  const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect;
3775                  const leftPos = parentRect.right - 1;
3776                  const topPos = rect.top;
3777                  nestedDropdown.style.left = leftPos + 'px';
3778                  nestedDropdown.style.top = topPos + 'px';
3779                  nestedDropdown.style.display = 'block';
3780                  nestedDropdown.style.opacity = '1';
3781                  nestedDropdown.style.visibility = 'visible';
3782                  nestedDropdown.style.position = 'fixed';
3783                  nestedDropdown.style.zIndex = '1251';
3784                  setTimeout(() => {
3785                      const dropdownRect = nestedDropdown.getBoundingClientRect();
3786                      if (dropdownRect.right > window.innerWidth) {
3787                          nestedDropdown.style.left = (parentRect.left - dropdownRect.width - 5) + 'px';
3788                      }
3789                  }, 10);
3790              }
3791
3792              function closeHorizontalNestedFlyout(hostItem) {
3793                  if (!hostItem) return;
3794                  hostItem.classList.remove('expanded');
3795                  hostItem.setAttribute('aria-expanded', 'false');
3796                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3797                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'false');
3798                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3799                  if (!hostFlyoutId) return;
3800                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3801                  if (nestedDropdown) {
3802                      nestedDropdown.style.display = 'none';
3803                      nestedDropdown.style.opacity = '0';
3804                      nestedDropdown.style.visibility = 'hidden';
3805                  }
3806              }
3807
3808              // Keyboard opens the same flyout hover shows — attrs on label so mouse click focus stays natural
3809              if (titleSpan && !isNavigableParent) {
3810                  titleSpan.setAttribute('tabindex', '0');
3811                  titleSpan.setAttribute('role', 'button');
3812                  titleSpan.setAttribute('aria-haspopup', 'true');
3813                  titleSpan.setAttribute('aria-expanded', 'false');
3814                  titleSpan.addEventListener('keydown', function(event) {
3815                      if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowRight') {
3816                          event.preventDefault();
3817                          event.stopPropagation();
3818                          openHorizontalNestedFlyout(subMenuItem);
3819                          titleSpan.setAttribute('aria-expanded', 'true');
3820                          const hostFlyoutId = subMenuItem.getAttribute('data-flyout-id');
3821                          const nestedDropdown = hostFlyoutId
3822                              ? document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]')
3823                              : null;
3824                          const firstLink = nestedDropdown && nestedDropdown.querySelector('a.horizontal-menu
3824-label');
3825                          if (firstLink && typeof firstLink.focus === 'function') {
3826                              try { firstLink.focus(); } catch (e) {}
3827                          }
3828                      } else if (event.key === 'Escape' || event.key === 'ArrowLeft') {
3829                          event.preventDefault();
3830                          event.stopPropagation();
3831                          closeHorizontalNestedFlyout(subMenuItem);
3832                          titleSpan.setAttribute('aria-expanded', 'false');
3833                      }
3834                  });
3835              }
3836              
3837              subMenuItem.addEventListener('mouseenter', function() {
3838                  openHorizontalNestedFlyout(this);
3839                  if (titleSpan) titleSpan.setAttribute('aria-expanded', 'true');
3840              });
3841              
3842              subMenuItem.addEventListener('mouseleave', function(event) {
3843                  // Check if we're moving to the flyout
3844                  const flyoutId = this.getAttribute('data-flyout-id');
3845                  const relatedTarget = event.relatedTarget;
3846                  const isMovingToFlyout = relatedTarget && flyoutId && 
3847                      relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]');
3848                  
3849                  // Also check if we're moving within the dropdown
3850                  const parentMenuItem = this.closest('.horizontal-menu-item');
3851                  const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget);
3852                  
3853                  console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout);
3854                  console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown);
3855                  
3856                  if (!isMovingToFlyout && !isMovingWithinDropdown) {
3857                      this.nestedLeaveTimer = setTimeout(() => {
3858                          closeHorizontalNestedFlyout(this);
3859                          if (titleSpan) titleSpan.setAttribute('aria-expanded', 'false');
3860                          
3861                          // Now that the flyout is closed, allow the parent dropdown to close
3862                          const parentMenuItem = this.closest('.horizontal-menu-item');
3863                          if (parentMenuItem) {
3864                              setTimeout(() => {
3865                                  const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3866                                  if (!activeFlyout) {
3867                                      parentMenuItem.classList.remove('expanded');
3868                                      const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3869                                      if (currentIcon) currentIcon.classList.remove('rotated');
3870                                  }
3871                              }, 50);
3872                          }
3873                      }, 200);
3874                  } else {
3875                      console.log('Submenu item mouseleave - not starting timer (moving to flyout)');
3876                  }
3877              });
3878              
3879              nestedSubMenu.addEventListener('mouseenter', function() {
3880                  console.log('Flyout mouseenter - clearing timers');
3881                  clearTimeout(nestedLeaveTimer);
3882                  
3883                  // Clear the parent dropdown's leave timer to keep it open
3884                  const flyoutId = this.getAttribute('data-flyout-id');
3885                  if (flyoutId) {
3886                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3887                      if (parentSubItem) {
3888                          const parentMenuItem = parentSubItem.closest('.horizontal-menu-item');
3889                          if (parentMenuItem && parentMenuItem.leaveTimer) {
3890                              console.log('Clearing parent menu item leave timer');
3891                              clearTimeout(parentMenuItem.leaveTimer);
3892                              parentMenuItem.leaveTimer = null;
3893                          } else {
3894                              console.log('No parent menu item leave timer to clear');
3895                          }
3896                          if (parentSubItem.nestedLeaveTimer) {
3897                              console.log('Clearing submenu item nested leave timer');
3898                              clearTimeout(parentSubItem.nestedLeaveTimer);
3899                              parentSubItem.nestedLeaveTimer = null;
3900                          }
3901                      }
3902                  }
3903              });
3904              
3905              nestedSubMenu.addEventListener('mouseleave', function() {
3906                  const flyoutId = this.getAttribute('data-flyout-id');
3907                  if (flyoutId) {
3908                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3909                      if (parentSubItem) {
3910                          nestedLeaveTimer = setTimeout(() => {
3911                              closeHorizontalNestedFlyout(parentSubItem);
3912                              const label = parentSubItem.querySelector('.horizontal-menu
3912-label');
3913                              if (label) label.setAttribute('aria-expanded', 'false');
3914                          }, 200);
3915                      }
3916                  }
3917              });
3918          }
3919      }
3920  }
3921
3922  // Click listener for navigation (only if no children or if it's a direct link)
3923  if (!subMenuItem.classList.contains('has-children') || isPageMenuItem(galleryItem) || isNavigableParent) {
3924      subMenuItem.addEventListener('click', async (event) => {
3925          if (isNavigableParent && subVisibleChildren.length > 0 && !titleSpan.contains(event.target)) {
3926              return;
3927          }
3928          event.stopPropagation();
3929          const clickedItemId = galleryItem.id;
3930
3931          // Check if it's an external URL first
3932          if (galleryItem.isExternal && galleryItem.url) {
3933              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
3934              return;
3935          }
3936
3937          if (isPageMenuItem(galleryItem)) {
3938              if (typeof loadPage === 'function') {
3939                  loadPage(clickedItemId, event);
3940                  if (typeof updateActiveStatesHorizontal === 'function') {
3941                      console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
3942                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
3943                  }
3944              }
3945          } else { // It's a gallery
3946              if (typeof loadGallery === 'function') {
3947                  console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
3948                  await loadGallery(clickedItemId, event);
3949                  console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
3950                  if (typeof updateActiveStatesHorizontal === 'function') {
3951                      setTimeout(() => {
3952                          console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
3953                          updateActiveStatesHorizontal(clickedItemId);
3954                      }, 100);
3955                  }
3956              }
3957          }
3958
3959          // Close all parent dropdowns and flyouts after navigation
3960          document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => {
3961              openItem.classList.remove('expanded');
3962              const icon = openItem.querySelector('.icon.toggle-icon');
3963              if (icon) icon.classList.remove('rotated');
3964          });
3965          
3966          // Close all flyouts
3967          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
3968              flyout.style.display = 'none';
3969              flyout.style.opacity = '0';
3970              flyout.style.visibility = 'hidden';
3971          });
3972      });
3973  }
3974  
3975  return subMenuItem;
3976}
3977
3978/**
3979* Updates the 'active' class for items in the horizontal menu.
3980*/
3981
3982function updateActiveStatesHorizontal(forcedActiveId = null) {
3983  // Prioritize forcedActiveId if provided, otherwise use global state.
3984  // Ensure the ID is treated as a number for comparison.
3985  const targetId = forcedActiveId !== null 
3986                   ? parseInt(String(forcedActiveId)) 
3987                   : parseInt(String(window.activeGalleryId || activeGalleryId));
3988
3989  console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, 
3990              (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), 
3991              "Type:", typeof targetId);
3992
3993  const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item');
3994
3995  if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number
3996    console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId);
3997    return;
3998  }
3999
4000  let activeItemSet = false;
4001  menuItems.forEach(item => {
4002    const itemIdStr = item.dataset.id;
4003    if (!itemIdStr) {
4004      // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item);
4005      return; 
4006    }
4007    const itemId = parseInt(itemIdStr);
4008
4009    if (itemId === targetId) {
4010      if (!item.classList.contains('active')) {
4011        item.classList.add('active');
4012        console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
4013      }
4014      activeItemSet = true;
4015    } else {
4016      if (item.classList.contains('active')) {
4017        item.classList.remove('active');
4018        console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
4019      }
4020    }
4021  });
4022
4023  if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number
4024      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.`);
4025  }
4026
4027  const activeSubItem = document.querySelector('.horizontal-submenu-item.active');
4028  if (activeSubItem) {
4029      const parentTopItem = activeSubItem.closest('.horizontal-menu
4029-item.has-children');
4030      if (parentTopItem && !parentTopItem.classList.contains('active')) {
4031          // parentTopItem.classList.add('active-parent'); // Optional class for styling parent
4032      }
4033  }
4034  console.log("[Horizontal Menu Update] Completed for Target ID:", targetId);
4035}
4036
4037
4038  // Listener for closing dropdowns when clicking outside
4039  document.addEventListener('click', function(event) {
4040      if (currentMenuLayout === 'horizontal') {
4041          const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded');
4042          let clickedInsideSubmenuOrParent = false;
4043
4044          openSubmenus.forEach(submenuContainer => {
4045              // Check if click is on the submenu container OR its direct parent menu item
4046              if (submenuContainer.contains(event.target) || 
4047                  (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) {
4048                  clickedInsideSubmenuOrParent = true;
4049              }
4050          });
4051          
4052          // If click is on another top-level item that is NOT expanded, also don't close
4053          const targetMenuItem = event.target.closest('.horizontal-menu-item');
4054          if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) {
4055               clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it
4056          }
4057
4058
4059          if (!clickedInsideSubmenuOrParent) {
4060              openSubmenus.forEach(submenuContainer => {
4061                  submenuContainer.classList.remove('expanded');
4062              });
4063          }
4064      }
4065  });
4066
4067document.addEventListener('menuLayoutChanged', function(e) {
4068    const newLayout = e.detail.layout;
4069    console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing);
4070
4071    // Update the global currentMenuLayout variable if you have one
4072    if (typeof currentMenuLayout !== 'undefined') {
4073        currentMenuLayout = newLayout;
4074    }
4075
4076    const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected
4077
4078    // Always clear and re-render the appropriate menu structure for the new layout.
4079    // This is important for style changes (like font size) that affect dimensions.
4080    const galleryTreeContainer = document.getElementById('galleryTree');
4081    const mobileHeaderContent = document.querySelector('.mobile-header-content');
4082
4083    if (galleryTreeContainer) {
4084        galleryTreeContainer.innerHTML = ''; // Clear existing tree
4085    }
4086    if (mobileHeaderContent) {
4087        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
4088        if (horizontalMenuContainer) {
4089            horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself
4090        }
4091    }
4092
4093    // Render the correct menu structure based on the new layout
4094    if (newLayout === 'horizontal') {
4095        if (typeof renderHorizontalMenu === 'function') {
4096            renderHorizontalMenu();
4097        }
4098        // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal
4099    } else { // 'sidebar' or 'top'
4100        if (typeof renderGalleries === 'function') {
4101            renderGalleries();
4102        }
4103        // CSS rules will handle sidebar visibility
4104    }
4105
4106    // If currently in edit mode, ensure the UI components for editing are correctly displayed
4107    // for the new layout, without fully toggling the mode off and on.
4108    if (isEditing && typeof window.updateGlobalEditState === 'function') {
4109        console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.');
4110
4111        // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode.
4112        // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar.
4113        window.updateGlobalEditState(true);
4114
4115        // 2. Re-initialize sortables. It's often safer to destroy existing ones first.
4116        if (typeof initializeNestedSortables === 'function') {
4117            if (typeof destroyNestedSortables === 'function') {
4118                destroyNestedSortables();
4119            }
4120            setTimeout(initializeNestedSortables, 100); // Delay for DOM updates
4121        }
4122        // Also reinitialize for SidebarManager if it exists and handles its own sortables
4123        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
4124             setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150);
4125        }
4126
4127        // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible
4128        const editControls = document.querySelector('.edit-controls');
4129        if (editControls) {
4130            editControls.style.display = 'flex'; // Or your default display type for these controls
4131            editControls.classList.add('visible');
4132        }
4133
4134        // 4. If the layout is now horizontal, the sidebar (for editing) should be visible.
4135        // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }`
4136        // should handle this. This log is for confirmation.
4137        if (newLayout === 'horizontal' && sidebar) {
4138            console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.');
4139        }
4140        
4141        // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set.
4142        if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && 
4143            window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) {
4144            window.PageManager.setEditMode(true);
4145        }
4146
4147    } else if (!isEditing) {
4148        // If not in edit mode, CSS should handle the view state.
4149        // This block can be used for any explicit view mode adjustments if CSS isn't sufficient.
4150        console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.');
4151    }
4152
4153    // Update mobile title and close mobile menu if open, regardless of edit mode
4154    if (typeof updateMobileTitle === 'function') {
4155        updateMobileTitle();
4156    }
4157    if (typeof closeMobileMenu === 'function') {
4158        closeMobileMenu();
4159    }
4160
4161    // Re-render the active page so row-band insets and recipe layout match the new menu shell
4162    if (window.PageManager && typeof window.PageManager.renderElements === 'function' && window.PageManager.currentPageId) {
4163        console.log('menuLayoutChanged: Re-rendering page', window.PageManager.currentPageId);
4164        window.PageManager.renderElements(window.PageManager.currentPageId);
4165    }
4166    if (typeof ensureCorrectLayoutApplied === 'function') {
4167        setTimeout(ensureCorrectLayoutApplied, 0);
4168    }
4169});
4170
4171
4172// Make sure getFirstSidebarElementForHeader is available if called from other scripts
4173if (typeof window.getFirstSidebarElementForHeader === 'undefined') {
4174  window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader;
4175}
4176
4177// Make sure getFirstSocialElementForHeader is available if called from other scripts
4178if (typeof window.getFirstSocialElementForHeader === 'undefined') {
4179  window.getFirstSocialElementForHeader = getFirstSocialElementForHeader;
4180}
4181
4182// Make sure renderSocialIcons is available if called from other scripts
4183if (typeof window.renderSocialIcons === 'undefined') {
4184  window.renderSocialIcons = renderSocialIcons;
4185}
4186if (typeof window.renderHorizontalMenu === 'undefined') {
4187    window.renderHorizontalMenu = renderHorizontalMenu;
4188}
4189if (typeof window.createHorizontalMenuItem === 'undefined') {
4190    window.createHorizontalMenuItem = createHorizontalMenuItem;
4191}
4192if (typeof window.createHorizontalSubMenuItem === 'undefined') {
4193    window.createHorizontalSubMenuItem = createHorizontalSubMenuItem;
4194}
4195if (typeof window.updateActiveStatesHorizontal === 'undefined') {
4196    window.updateActiveStatesHorizontal = updateActiveStatesHorizontal;
4197}
4198// Ensure toggleEditMode is globally available if it's the primary one.
4199if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder
4200    window.toggleEditMode = toggleEditMode;
4201}
4202
4203
4204// Helper function to toggle edit controls visibility
4205function toggleEditControlsVisibility(show) {
4206  const editControls = document.querySelector('.edit-controls');
4207  if (editControls) {
4208    if (show) {
4209      editControls.classList.add('visible');
4210      editControls.style.display = 'flex';
4211    } else {
4212      editControls.classList.remove('visible');
4213      editControls.style.display = 'none';
4214    }
4215  }
4216}
4217
4218/** Reliable + shortcut across US/Intl layouts, numpad, and browsers (key/keyCode differs). */
4219function isHydraPlusKey(e) {
4220  if (e.repeat) return false;
4221  if (e.ctrlKey || e.metaKey || e.altKey) return false;
4222  if (e.key === '+' || e.key === 'Add') return true;
4223  if (e.code === 'NumpadAdd') return true;
4224  if ((e.code === 'Equal' || e.key === '=') && e.shiftKey) return true;
4225  if (e.keyCode === 187 && e.shiftKey) return true;
4226  if (e.keyCode === 107) return true;
4227  return false;
4228}
4229
4230function handleHydraPlusShortcut() {
4231  try {
4232    const token = window.didToken || localStorage.getItem('hydra_auth_token');
4233    const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
4234    if (token && isAdmin) {
4235      console.log('User already logged in - toggling edit mode');
4236      if (typeof window.toggleEditMode === 'function') {
4237        window.toggleEditMode();
4238      } else if (typeof toggleEditMode === 'function') {
4239        toggleEditMode();
4240      } else {
4241        console.warn('toggleEditMode function not found');
4242      }
4243    } else {
4244      const p = startOTPLogin();
4245      if (p && typeof p.then === 'function') {
4246        p.catch(function(err) {
4247          console.error('startOTPLogin failed:', err);
4248        });
4249      }
4250    }
4251  } catch (err) {
4252    console.error('Hydra + key handler error:', err);
4253  }
4254}
4255
4256document.addEventListener('keydown', function(e) {
4257  if (!isHydraPlusKey(e)) return;
4258  e.preventDefault();
4259  handleHydraPlusShortcut();
4260}, true);
4261
4262// Custom HTML pages load in a sandboxed iframe; + is captured there unless forwarded via postMessage.
4263function isHydraCustomPageHtmlPlusMessage(e) {
4264  if (!e.data || e.data.source !== 'hydra-custom-page-html' || e.data.type !== 'plus-key') return false;
4265  if (e.origin === window.location.origin) return true;
4266  // Sandboxed iframes (no allow-same-origin) post with origin "null" — accept only from our iframe.
4267  if (e.origin !== 'null' || !e.source) return false;
4268  const iframes = document.querySelectorAll('iframe.page-custom-html-iframe');
4269  for (let i = 0; i < iframes.length; i++) {
4270    if (iframes[i].contentWindow === e.source) return true;
4271  }
4272  return false;
4273}
4274
4275window.addEventListener('message', function(e) {
4276  if (!isHydraCustomPageHtmlPlusMessage(e)) return;
4277  handleHydraPlusShortcut();
4278});
4279
4280/** Option+/ (÷) opens Help → Pathways. Option+? opens Help → Ask (help admin only). */
4281function isHydraHelpKey(e) {
4282  if (e.repeat) return false;
4283  if (e.ctrlKey || e.metaKey) return false;
4284  if (!e.altKey) return false;
4285  if (e.shiftKey) return false;
4286  if (e.key === '÷') return true;
4287  if (e.code === 'Slash' && e.altKey) return true;
4288  return false;
4289}
4290
4291function isHydraHelpAskKey(e) {
4292  if (e.repeat) return false;
4293  if (e.ctrlKey || e.metaKey) return false;
4294  if (!e.altKey || !e.shiftKey) return false;
4295  if (e.key === '?' || e.key === '¿') return true;
4296  if (e.code === 'Slash') return true;
4297  return false;
4298}
4299
4300document.addEventListener('keydown', function(e) {
4301  if (!window.HYDRA_HELP_ENABLED) return;
4302  var t = e.target;
4303  if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
4304  if (isHydraHelpAskKey(e)) {
4305    e.preventDefault();
4306    e.stopPropagation();
4307    openHydraHelp({ initialTab: 'ask' });
4308    return;
4309  }
4310  if (!isHydraHelpKey(e)) return;
4311  e.preventDefault();
4312  e.stopPropagation();
4313  openHydraHelp();
4314}, true);
4315
4316// Email OTP Authentication with Enhanced Debugging
4317
4318
4319// Add this function to the client-side code
4320// Improved client-side backup function with better token handling
4321async function createLoginBackup() {
4322  try {
4323    console.log('Attempting to create login backup...');
4324    
4325    // Get authentication info from multiple possible sources
4326    let token = window.didToken || localStorage.getItem('hydra_auth_token');
4327    const email = localStorage.getItem('hydra_auth_email') || 
4328                  (window.userMetadata ? window.userMetadata.email : '') ||
4329                  '';
4330    
4331    // Check if we have authentication
4332    if (!token) {
4333      console.error('No auth token available for backup');
4334      return;
4335    }
4336    
4337    if (!email) {
4338      console.error('No email available for backup - make sure it was stored during login');
4339      return;
4340    }
4341    
4342    console.log(`Creating backup with token (${token.length} chars) for ${email}`);
4343    
4344    // Format the token correctly - VERY IMPORTANT
4345    // If it's already a hydra token (starts with hydra:), use it as is
4346    // Otherwise, add the hydra: prefix
4347    const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`;
4348    
4349    console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`);
4350    
4351    // Get the API URL with preview handling if needed
4352    const backupUrl = typeof getApiUrl === 'function' ? 
4353      getApiUrl('/api/create-backup') : 
4354      '/api/create-backup';
4355    
4356    console.log(`Sending backup request to: ${backupUrl}`);
4357    
4358    // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header
4359    const response = await fetch(backupUrl, {
4360      method: 'POST',
4361      headers: {
4362        'Authorization': `Bearer ${formattedToken}`,
4363        'Content-Type': 'application/json',
4364        'X-User-Email': email, // Include email as fallback
4365        'X-API-Request': 'true' // Mark as API request
4366      }
4367    });
4368    
4369    // Handle response
4370    if (!response.ok) {
4371      const errorText = await response.text();
4372      let errorData;
4373      try {
4374        // Try to parse as JSON
4375        errorData = JSON.parse(errorText);
4376        console.error('Backup failed:', errorData.error, errorData);
4377      } catch {
4378        // Not JSON, log raw text
4379        console.error('Backup failed with status', response.status, errorText);
4380      }
4381      return;
4382    }
4383    
4384    // Parse successful response
4385    const result = await response.json();
4386    console.log(`✅ Backup created successfully: ${result.backup}`);
4387  } catch (error) {
4388    console.error('Error in backup process:', error);
4389  }
4390}
4391
4392// Initialize Magic with detailed logging
4393function initMagic() {
4394  try {
4395    // If Magic is already initialized, return it
4396    if (window.magic) {
4397      return window.magic;
4398    }
4399    
4400    console.log('Initializing Magic SDK...');
4401    
4402    // Initialize with minimal options
4403    const magic = new Magic('pk_live_E0B68BA6DCA8C75F', {
4404      testMode: false
4405    });
4406    
4407    // Store reference globally
4408    window.magic = magic;
4409    
4410    console.log('Magic SDK initialized successfully');
4411    return magic;
4412  } catch (error) {
4413    console.error('Error initializing Magic:', error);
4414    throw error;
4415  }
4416}
4417
4418// Start OTP login flow with proper event handling
4419async function startOTPLogin() {
4420  try {
4421    // Initialize Magic
4422    const magic = initMagic();
4423    
4424    // Get email from user (styled overlay — same auth path as native prompt)
4425    const email = await showEmailLoginOverlay();
4426    if (!email) return; // User cancelled
4427    
4428    console.log('Starting Email OTP login for:', email);
4429    showLoadingOverlay('Sending verification code...');
4430    
4431    // Check if Magic's event enum types are available - for newer Magic SDK versions
4432    // If not available, fall back to string-based events
4433    const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && 
4434                    typeof LoginWithEmailOTPEventEmit !== 'undefined';
4435    
4436    console.log('Using enum-based events:', useEnums);
4437    
4438    // Create a timeout handler
4439    let emailSentTimeout = setTimeout(() => {
4440      console.log('No email-otp-sent event received, using fallback UI');
4441      hideLoadingOverlay();
4442      showOTPInputOverlay(email);
4443    }, 10000);
4444    
4445    // Create OTP login handle with showUI: false
4446    const handle = magic.auth.loginWithEmailOTP({ 
4447      email, 
4448      showUI: false,
4449      deviceCheckUI: false
4450    });
4451    
4452    // Track authentication state
4453    window.pendingAuth = {
4454      email,
4455      handle
4456    };
4457    
4458    // Listen for events - using both enum-based and string-based event handling
4459    console.log('Setting up OTP event handlers...');
4460    
4461    // Email OTP sent event
4462    if (useEnums) {
4463      handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => {
4464        console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully');
4465        clearTimeout(emailSentTimeout);
4466        hideLoadingOverlay();
4467        showOTPInputOverlay(email);
4468      });
4469    }
4470    
4471    // Also listen for string-based event (for backward compatibility)
4472    handle.on('email-otp-sent', () => {
4473      console.log('EVENT: email-otp-sent - Email was sent successfully');
4474      clearTimeout(emailSentTimeout);
4475      hideLoadingOverlay();
4476      showOTPInputOverlay(email);
4477    });
4478    
4479    // Invalid OTP event
4480    if (useEnums) {
4481      handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => {
4482        console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered');
4483        showErrorMessage('Invalid verification code. Please try again.');
4484        updateOTPInputForRetry();
4485      });
4486    }
4487    
4488    // Also listen for string-based event
4489    handle.on('invalid-email-otp', () => {
4490      console.log('EVENT: invalid-email-otp - Invalid code entered');
4491      showErrorMessage('Invalid verification code. Please try again.');
4492      updateOTPInputForRetry();
4493    });
4494    
4495    async function handleLoginSuccess(result) {
4496  console.log('OTP verification successful, checking admin status...');
4497  hideOTPInputOverlay();
4498  hideLoadingOverlay();
4499
4500  try {
4501    window.didToken = result;
4502    console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none');
4503
4504    const userInfo = await magic.user.getInfo();
4505    window.userMetadata = userInfo;
4506    console.log('User info retrieved:', userInfo);
4507
4508    const isUserAdmin = await checkAdminStatus(result);
4509
4510    if (!isUserAdmin && !window.isAdmin) {
4511      console.log('User is not on admin list, login denied');
4512      showErrorMessage('Login failed');
4513      clearHydraClientAuthStorage();
4514      resetAuthUI();
4515      try {
4516        await magic.user.logout();
4517      } catch (logoutError) {
4518        console.error('Error logging out non-admin user from Magic:', logoutError);
4519      }
4520      window.pendingAuth = null;
4521      return;
4522    }
4523
4524    console.log('Admin status check completed');
4525    window.isAuthenticated = true;
4526    isAuthenticated = true;
4527    showSuccessMessage('Login successful!');
4528
4529    // Store the ID token (JWT) for API calls
4530    try {
4531      const idToken = await magic.user.getIdToken();
4532      if (idToken && idToken.includes('.')) {
4533        console.log('Got proper JWT token from Magic.link');
4534        localStorage.setItem('hydra_auth_token', idToken);
4535      } else if (userInfo && userInfo.email) {
4536        const userToken = {
4537          email: userInfo.email,
4538          iat: Math.floor(Date.now() / 1000),
4539          exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60)
4540        };
4541        const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' }));
4542        const payload = btoa(JSON.stringify(userToken));
4543        const jwtToken = header + '.' + payload + '.nosignature';
4544        localStorage.setItem('hydra_auth_token', jwtToken);
4545        localStorage.setItem('hydra_auth_email', userInfo.email);
4546      }
4547      localStorage.setItem('hydra_did_token', result);
4548    } catch (tokenError) {
4549      console.error('Error getting ID token:', tokenError);
4550      localStorage.setItem('hydra_auth_token', result);
4551    }
4552
4553    // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy
4554
4555    // 1. Add state classes to body
4556    document.body.classList.add('hydra-authenticated');
4557    if (window.isAdmin) {
4558      document.body.classList.add('hydra-admin');
4559    }
4560
4561    // 2. Direct DOM manipulation
4562    const logoutButton = document.getElementById('logoutButton');
4563    const editButton = document.getElementById('editButton');
4564
4565    // Create new buttons if they don't exist
4566    if (!logoutButton) {
4567      console.log('Creating logout button');
4568      const newLogoutButton = document.createElement('button');
4569      newLogoutButton.id = 'logoutButton';
4570      newLogoutButton.className = 'btn';
4571      newLogoutButton.textContent = 'Logout';
4572      newLogoutButton.onclick = logout;
4573      newLogoutButton.style.display = 'block';
4574
4575      const sidebarHeader = document.querySelector('.sidebar-header');
4576      if (sidebarHeader) {
4577        sidebarHeader.appendChild(newLogoutButton);
4578      } else {
4579        document.body.appendChild(newLogoutButton);
4580      }
4581    } else {
4582      logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4583    }
4584
4585    if (!editButton && window.isAdmin) {
4586      console.log('Creating edit button');
4587      const newEditButton = document.createElement('button');
4588      newEditButton.id = 'editButton';
4589      newEditButton.className = 'btn btn-primary';
4590      newEditButton.innerHTML = `
4591        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4592          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
4593          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
4594        </svg>
4595        Edit
4596      `;
4597      newEditButton.onclick = toggleEditMode;
4598      newEditButton.style.display = 'block';
4599
4600      const sidebarHeader = document.querySelector('.sidebar-header');
4601      if (sidebarHeader) {
4602        sidebarHeader.appendChild(newEditButton);
4603      } else {
4604        document.body.appendChild(newEditButton);
4605      }
4606    } else if (editButton && window.isAdmin) {
4607      editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4608    }
4609
4610    // 3. Dispatch event for other listeners
4611    document.dispatchEvent(new CustomEvent('login-complete', {
4612      detail: {
4613        isAdmin: window.isAdmin,
4614        email: window.userMetadata ? window.userMetadata.email : null
4615      }
4616    }));
4617
4618    window.pendingAuth = null;
4619
4620    if (typeof checkAuth === 'function') {
4621      console.log('Running existing checkAuth function');
4622      setTimeout(checkAuth, 500);
4623    }
4624  } catch (error) {
4625    console.error('Error in login success handling:', error);
4626    showErrorMessage('Login failed');
4627    clearHydraClientAuthStorage();
4628    resetAuthUI();
4629    window.pendingAuth = null;
4630  }
4631}
4632
4633
4634    // Completion event
4635    handle.on('done', async (result) => {
4636      await handleLoginSuccess(result);
4637    });
4638    
4639    // Error event
4640    handle.on('error', (error) => {
4641      console.error('EVENT: error - OTP login error:', error);
4642      hideLoadingOverlay();
4643      hideOTPInputOverlay();
4644      showErrorMessage('Login failed: ' + (error.message || 'Unknown error'));
4645      
4646      // Clear timeout if still active
4647      clearTimeout(emailSentTimeout);
4648      
4649      // Clean up
4650      window.pendingAuth = null;
4651    });
4652    
4653    console.log('OTP event handlers set up successfully');
4654    
4655  } catch (error) {
4656    console.error('Error starting OTP login:', error);
4657    hideLoadingOverlay();
4658    hideOTPInputOverlay();
4659    showErrorMessage('Error starting login: ' + error.message);
4660  }
4661}
4662
4663// Submit OTP code with support for enum-based events
4664function submitOTP(code) {
4665  try {
4666    // Validate code input
4667    if (!code || code.trim() === '') {
4668      showErrorMessage('Please enter the verification code.');
4669      return;
4670    }
4671    
4672    // Check if we have a pending authentication
4673    if (!window.pendingAuth || !window.pendingAuth.handle) {
4674      showErrorMessage('No active login session. Please try again.');
4675      hideOTPInputOverlay();
4676      return;
4677    }
4678    
4679    console.log('Submitting OTP code...');
4680    showLoadingOverlay('Verifying code...');
4681    
4682    // Check if enum types are available
4683    const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4684    
4685    // Verify the OTP code
4686    if (useEnums) {
4687      window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code);
4688    } else {
4689      window.pendingAuth.handle.emit('verify-email-otp', code);
4690    }
4691    
4692  } catch (error) {
4693    console.error('Error submitting OTP:', error);
4694    hideLoadingOverlay();
4695    showErrorMessage('Error verifying code: ' + error.message);
4696  }
4697}
4698
4699// Cancel OTP login with support for enum-based events
4700function cancelOTPLogin() {
4701  try {
4702    // Check if we have a pending authentication
4703    if (window.pendingAuth && window.pendingAuth.handle) {
4704      // Check if enum types are available
4705      const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4706      
4707      // Emit cancel event
4708      if (useEnums) {
4709        window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel);
4710      } else {
4711        window.pendingAuth.handle.emit('cancel');
4712      }
4713      
4714      console.log('OTP login cancelled');
4715    }
4716    
4717    // Clean up
4718    window.pendingAuth = null;
4719    
4720    // Hide any overlays
4721    hideEmailLoginOverlay();
4722    hideOTPInputOverlay();
4723    hideLoadingOverlay();
4724  } catch (error) {
4725    console.error('Error cancelling OTP login:', error);
4726  }
4727}
4728
4729// Update OTP input for retry
4730function updateOTPInputForRetry() {
4731  const otpInput = document.getElementById('otp-input');
4732  if (otpInput) {
4733    otpInput.value = '';
4734    otpInput.focus();
4735    
4736    // Add a shake animation for visual feedback
4737    otpInput.classList.add('shake');
4738    
4739    // Remove the animation class after it completes
4740    setTimeout(() => {
4741      otpInput.classList.remove('shake');
4742    }, 500);
4743  }
4744  
4745  // Hide loading overlay if it's visible
4746  hideLoadingOverlay();
4747}
4748
4749function isValidLoginEmail(value) {
4750  const trimmed = String(value || '').trim();
4751  if (!trimmed || trimmed.indexOf(' ') >= 0) return false;
4752  const at = trimmed.indexOf('@');
4753  if (at <= 0 || at !== trimmed.lastIndexOf('@')) return false;
4754  const local = trimmed.slice(0, at);
4755  const domain = trimmed.slice(at + 1);
4756  if (!local || !domain || domain.indexOf('.') < 1) return false;
4757  if (domain.startsWith('.') || domain.endsWith('.')) return false;
4758  return true;
4759}
4760
4761function showEmailLoginOverlay() {
4762  hideEmailLoginOverlay();
4763
4764  return new Promise((resolve) => {
4765    const overlay = document.createElement('div');
4766    overlay.id = 'email-login-overlay';
4767    overlay.className = 'pe-auth-overlay';
4768
4769    const content = document.createElement('div');
4770    content.className = 'pe-auth-shell';
4771
4772    const logo = document.createElement('img');
4773    logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4774    logo.alt = 'Neon Sky Logo';
4775    logo.className = 'pe-auth-shell__logo';
4776
4777    const description = document.createElement('p');
4778    description.className = 'pe-auth-shell__desc';
4779    description.textContent = 'Enter your email address to receive a verification code.';
4780
4781    const form = document.createElement('form');
4782    form.noValidate = true;
4783    form.onsubmit = function(e) {
4784      e.preventDefault();
4785      const raw = input.value.trim();
4786      if (!isValidLoginEmail(raw)) {
4787        input.classList.add('shake');
4788        setTimeout(() => input.classList.remove('shake'), 500);
4789        return;
4790      }
4791      hideEmailLoginOverlay();
4792      resolve(raw);
4793    };
4794
4795    const input = document.createElement('input');
4796    input.id = 'email-login-input';
4797    input.type = 'email';
4798    input.name = 'email';
4799    input.placeholder = '[email protected]';
4800    input.autocomplete = 'email';
4801    input.inputMode = 'email';
4802    input.required = true;
4803    input.className = 'pe-auth-shell__input pe-auth-shell__input--email';
4804    const savedEmail = localStorage.getItem('hydra_auth_email');
4805    if (savedEmail && isValidLoginEmail(savedEmail)) {
4806      input.value = savedEmail.trim();
4807    }
4808    form.appendChild(input);
4809
4810    const buttonContainer = document.createElement('div');
4811    buttonContainer.className = 'pe-auth-actions';
4812
4813    const continueButton = document.createElement('button');
4814    continueButton.type = 'submit';
4815    continueButton.textContent = 'Continue';
4816    continueButton.className = 'pe-btn pe-btn--primary';
4817    buttonContainer.appendChild(continueButton);
4818
4819    const cancelButton = document.createElement('button');
4820    cancelButton.type = 'button';
4821    cancelButton.textContent = 'Cancel';
4822    cancelButton.className = 'pe-btn';
4823    cancelButton.onclick = function() {
4824      hideEmailLoginOverlay();
4825      resolve(null);
4826    };
4827    buttonContainer.appendChild(cancelButton);
4828    form.appendChild(buttonContainer);
4829
4830    content.appendChild(logo);
4831    content.appendChild(description);
4832    content.appendChild(form);
4833    overlay.appendChild(content);
4834    document.body.appendChild(overlay);
4835
4836    setTimeout(() => {
4837      input.focus();
4838      if (input.value) {
4839        input.select();
4840      }
4841    }, 100);
4842  });
4843}
4844
4845function hideEmailLoginOverlay() {
4846  const overlay = document.getElementById('email-login-overlay');
4847  if (overlay && overlay.parentNode) {
4848    overlay.parentNode.removeChild(overlay);
4849  }
4850}
4851
4852// Show OTP input overlay - improved version
4853function showOTPInputOverlay(email) {
4854  hideOTPInputOverlay();
4855  hideEmailLoginOverlay();
4856
4857  const overlay = document.createElement('div');
4858  overlay.id = 'otp-input-overlay';
4859  overlay.className = 'pe-auth-overlay';
4860
4861  const content = document.createElement('div');
4862  content.className = 'pe-auth-shell';
4863
4864  const logo = document.createElement('img');
4865  logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4866  logo.alt = 'Neon Sky Logo';
4867  logo.className = 'pe-auth-shell__logo';
4868
4869  const description = document.createElement('p');
4870  description.className = 'pe-auth-shell__desc';
4871  const descPrefix = document.createTextNode("We've sent a verification code to ");
4872  const descEmail = document.createElement('strong');
4873  descEmail.textContent = String(email || '');
4874  const descSuffix = document.createTextNode('. Please check your email and enter the code below:');
4875  description.appendChild(descPrefix);
4876  description.appendChild(descEmail);
4877  description.appendChild(document.createElement('br'));
4878  description.appendChild(descSuffix);
4879
4880  const form = document.createElement('form');
4881  form.onsubmit = function(e) {
4882    e.preventDefault();
4883    const otpInput = document.getElementById('otp-input');
4884    if (otpInput && otpInput.value) {
4885      submitOTP(otpInput.value);
4886    }
4887  };
4888
4889  const input = document.createElement('input');
4890  input.id = 'otp-input';
4891  input.type = 'text';
4892  input.placeholder = 'Enter code';
4893  input.pattern = '[0-9]*';
4894  input.inputMode = 'numeric';
4895  input.autocomplete = 'one-time-code';
4896  input.className = 'pe-auth-shell__input';
4897  form.appendChild(input);
4898
4899  const buttonContainer = document.createElement('div');
4900  buttonContainer.className = 'pe-auth-actions';
4901
4902  const verifyButton = document.createElement('button');
4903  verifyButton.type = 'submit';
4904  verifyButton.textContent = 'Verify';
4905  verifyButton.className = 'pe-btn pe-btn--primary';
4906  buttonContainer.appendChild(verifyButton);
4907
4908  const cancelButton = document.createElement('button');
4909  cancelButton.type = 'button';
4910  cancelButton.textContent = 'Cancel';
4911  cancelButton.className = 'pe-btn';
4912  cancelButton.onclick = function() {
4913    cancelOTPLogin();
4914  };
4915  buttonContainer.appendChild(cancelButton);
4916  form.appendChild(buttonContainer);
4917
4918  const resendContainer = document.createElement('div');
4919  resendContainer.className = 'pe-auth-resend';
4920
4921  const resendText = document.createElement('span');
4922  resendText.textContent = "Didn't receive the code? ";
4923
4924  const resendLink = document.createElement('a');
4925  resendLink.textContent = 'Resend';
4926  resendLink.href = '#';
4927  resendLink.onclick = function(e) {
4928    e.preventDefault();
4929    cancelOTPLogin();
4930    setTimeout(() => {
4931      startOTPLogin();
4932    }, 500);
4933  };
4934
4935  resendContainer.appendChild(resendText);
4936  resendContainer.appendChild(resendLink);
4937  form.appendChild(resendContainer);
4938
4939  content.appendChild(logo);
4940  content.appendChild(description);
4941  content.appendChild(form);
4942  overlay.appendChild(content);
4943  document.body.appendChild(overlay);
4944
4945  setTimeout(() => {
4946    input.focus();
4947  }, 100);
4948}
4949
4950// Hide OTP input overlay
4951function hideOTPInputOverlay() {
4952  const overlay = document.getElementById('otp-input-overlay');
4953  if (overlay && overlay.parentNode) {
4954    overlay.parentNode.removeChild(overlay);
4955  }
4956}
4957
4958// Utility function to show loading overlay
4959function showLoadingOverlay(message = 'Loading...') {
4960  hideLoadingOverlay();
4961
4962  const overlay = document.createElement('div');
4963  overlay.id = 'loading-overlay';
4964  overlay.className = 'pe-auth-overlay';
4965
4966  const content = document.createElement('div');
4967  content.className = 'pe-auth-shell';
4968
4969  const loaderContainer = document.createElement('div');
4970  loaderContainer.className = 'pe-auth-shell__loader';
4971  loaderContainer.innerHTML = `
4972    <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
4973      <defs>
4974        <filter id="svgSpinnersGooeyBalls20">
4975          <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
4976          <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"/>
4977          <feBlend in="SourceGraphic" in2="z"/>
4978        </filter>
4979      </defs>
4980      <g filter="url(#svgSpinnersGooeyBalls20)">
4981        <circle cx="5" cy="12" r="4" fill="currentColor">
4982          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
4983        </circle>
4984        <circle cx="19" cy="12" r="4" fill="currentColor">
4985          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
4986        </circle>
4987        <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
4988      </g>
4989    </svg>
4990  `;
4991  content.appendChild(loaderContainer);
4992
4993  const messageEl = document.createElement('p');
4994  messageEl.className = 'pe-auth-shell__message';
4995  messageEl.textContent = message;
4996  content.appendChild(messageEl);
4997
4998  overlay.appendChild(content);
4999  document.body.appendChild(overlay);
5000}
5001
5002// Utility function to hide loading overlay
5003function hideLoadingOverlay() {
5004  const overlay = document.getElementById('loading-overlay');
5005  if (overlay && overlay.parentNode) {
5006    overlay.parentNode.removeChild(overlay);
5007  }
5008}
5009
5010// Utility function to show success message
5011function showSuccessMessage(message, duration = 3000) {
5012  const existingToast = document.getElementById('success-toast');
5013  if (existingToast && existingToast.parentNode) {
5014    existingToast.parentNode.removeChild(existingToast);
5015  }
5016
5017  const toast = document.createElement('div');
5018  toast.id = 'success-toast';
5019  toast.className = 'pe-auth-toast';
5020  toast.textContent = message;
5021
5022  document.body.appendChild(toast);
5023
5024  setTimeout(() => {
5025    if (toast.parentNode) {
5026      toast.parentNode.removeChild(toast);
5027    }
5028  }, duration);
5029}
5030
5031// Utility function to show error message
5032function showErrorMessage(message, duration = 5000) {
5033  const existingToast = document.getElementById('error-toast');
5034  if (existingToast && existingToast.parentNode) {
5035    existingToast.parentNode.removeChild(existingToast);
5036  }
5037
5038  const toast = document.createElement('div');
5039  toast.id = 'error-toast';
5040  toast.className = 'pe-auth-toast pe-auth-toast--error';
5041  toast.textContent = message;
5042
5043  document.body.appendChild(toast);
5044
5045  setTimeout(() => {
5046    if (toast.parentNode) {
5047      toast.parentNode.removeChild(toast);
5048    }
5049  }, duration);
5050}
5051
5052// UPDATED: Check authentication status with class-based approach
5053async function checkAuth() {
5054  try {
5055    if (!magic) {
5056      console.log('Magic not initialized, skipping auth check');
5057      return;
5058    }
5059    
5060    // Prevent multiple simultaneous auth checks
5061    if (window._checkingAuth) return;
5062    window._checkingAuth = true;
5063    
5064    console.log('Checking auth status...');
5065    
5066    isAuthenticated = await magic.user.isLoggedIn();
5067    console.log('isLoggedIn check:', isAuthenticated);
5068    
5069    if (isAuthenticated) {
5070      // Add the authenticated class 
5071      document.body.classList.add('hydra-authenticated');
5072      
5073      // Get user metadata
5074      userMetadata = await magic.user.getInfo();
5075      console.log('User info:', userMetadata);
5076      
5077      // Get ID token for API calls (JWT format)
5078      const rawDidToken = await magic.user.getIdToken();
5079      console.log('ID token acquired, length:', rawDidToken.length);
5080      
5081      // Do NOT write the raw DID token to localStorage here — checkAdminStatus will
5082      // receive the server-issued hydra token and store that instead, which is what
5083      // the save function needs. Writing the DID token now would overwrite a valid
5084      // hydra token that may already be stored from the OTP login flow.
5085      
5086      // Check if user is admin; on success this updates both localStorage and didToken
5087      // with a proper hydra token via checkAdminStatus → TokenManager.storeToken
5088      await checkAdminStatus(rawDidToken);
5089    } else {
5090      // Magic has no session — still try Hydra OTP token so /_manage and return visits work
5091      const hydraOnly = localStorage.getItem('hydra_auth_token');
5092      if (hydraOnly) {
5093        try {
5094          await checkAdminStatus(hydraOnly);
5095        } catch (e) {
5096          console.error('Hydra token restore after Magic logout failed:', e);
5097        }
5098      } else {
5099        resetAuthUI();
5100      }
5101    }
5102    
5103    // Release lock
5104    window._checkingAuth = false;
5105  } catch (error) {
5106    console.error('Error checking authentication:', error);
5107    window._checkingAuth = false;
5108    resetAuthUI();
5109  }
5110}
5111
5112
5113async function checkAdminStatus(token) {
5114  try {
5115    console.log('Checking admin status...');
5116
5117    // Add user email to headers if available
5118    let userEmail = null;
5119    if (window.userMetadata && window.userMetadata.email) {
5120      userEmail = window.userMetadata.email;
5121      console.log('Using email from userMetadata:', userEmail);
5122    } else {
5123        userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage
5124        if (userEmail) console.log('Using email from localStorage:', userEmail);
5125    }
5126
5127    if (!token) {
5128      console.log('No token provided to checkAdminStatus');
5129      return false; // Indicate failure
5130    }
5131
5132    // Format the token correctly for the API call
5133    let formattedToken = token;
5134    // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix
5135    if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) {
5136        formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer
5137    } else if (token.startsWith('hydra:')) {
5138        formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens
5139    }
5140    // If it already starts with Bearer, use as is
5141
5142    // Get the API URL, handling preview mode
5143    const apiUrl = typeof getApiUrl === 'function' ?
5144      getApiUrl('/api/check-admin') :
5145      '/api/check-admin';
5146
5147    console.log('Sending admin check request to:', apiUrl);
5148
5149    // Create request headers
5150    const headers = {
5151      'Authorization': formattedToken,
5152      'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow
5153    };
5154
5155    if (userEmail) {
5156      headers['X-User-Email'] = userEmail;
5157    }
5158
5159    console.log('Admin check headers:', {
5160      'Authorization': `${formattedToken.substring(0, 20)}...`,
5161      'X-User-Email': userEmail || 'Not available'
5162    });
5163
5164    // Add timeout to the fetch request
5165    const controller = new AbortController();
5166    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
5167
5168    let data;
5169    try {
5170      const response = await fetch(apiUrl, {
5171        method: 'GET',
5172        headers: headers,
5173        signal: controller.signal
5174      });
5175
5176      clearTimeout(timeoutId); // Clear timeout if fetch succeeds
5177
5178      console.log('Admin check response status:', response.status);
5179
5180      // Get the response as text first for better debugging
5181      const responseText = await response.text();
5182      console.log('Admin check response length:', responseText.length);
5183      console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...');
5184
5185
5186      try {
5187        data = JSON.parse(responseText);
5188        console.log('Admin check data:', data);
5189      } catch (parseError) {
5190        console.error('Error parsing admin check response:', parseError);
5191        // Handle cases where the response might not be JSON (e.g., HTML error page)
5192        // For preview URLs, let's still assume admin to allow editing flow
5193        if (window.location.hostname === 'preview.neonsky.app') {
5194          console.log('Preview URL detected, assuming admin status due to non-JSON response');
5195          isAdmin = true;
5196          document.body.classList.add('hydra-admin');
5197          localStorage.setItem('hydra_is_admin', 'true');
5198          forceShowEditUI(); // Attempt to show UI
5199          return true; // Indicate potential success for preview
5200        }
5201        throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`);
5202      }
5203
5204      if (!response.ok) {
5205          // Throw an error with details from the parsed JSON if available
5206          throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`);
5207      }
5208
5209      // Process the admin status
5210      isAdmin = data.isAdmin;
5211      window.isAdmin = isAdmin; // Update global flag
5212
5213      // Update classes based on admin status
5214      if (isAdmin) {
5215        document.body.classList.add('hydra-admin');
5216        localStorage.setItem('hydra_is_admin', 'true');
5217        // Store email if we got it from the server response
5218        if (data.email) {
5219            localStorage.setItem('hydra_auth_email', data.email);
5220        } else if (userEmail) {
5221            localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used
5222        }
5223      } else {
5224        document.body.classList.remove('hydra-admin');
5225        localStorage.removeItem('hydra_is_admin');
5226        localStorage.removeItem('hydra_auth_email'); // Clear email if not admin
5227      }
5228
5229      // Update siteId if provided
5230      if (data.siteId) {
5231        siteId = data.siteId;
5232        window.siteId = siteId;
5233        if (window.Parameters) window.Parameters.siteId = siteId;
5234      }
5235
5236      // Store new token if provided (Hydra token)
5237      if (data.hydraToken) {
5238        console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...');
5239        const cleanHydraToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken;
5240        // Always update the local didToken variable so saves immediately use the hydra token
5241        didToken = cleanHydraToken;
5242        // Store token globally and in localStorage using TokenManager if available
5243        if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
5244            window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || '');
5245        } else {
5246            // Fallback storage
5247            localStorage.setItem('hydra_auth_token', cleanHydraToken);
5248            if (data.email || userEmail) {
5249                localStorage.setItem('hydra_auth_email', data.email || userEmail);
5250            }
5251        }
5252        console.log('Stored credentials in localStorage');
5253      }
5254
5255      // Update UI based on admin status
5256      if (isAdmin) {
5257        console.log('Admin status confirmed, showing edit UI');
5258        forceShowEditUI(); // Ensure UI elements are visible
5259        // --- MODIFICATION START ---
5260        // If layout is horizontal, re-render the menu to show controls
5261        let currentLayout = 'sidebar'; // Default
5262        if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5263            currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5264        }
5265        if (currentLayout === 'horizontal') {
5266            console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.');
5267            if (typeof renderHorizontalMenu === 'function') {
5268                renderHorizontalMenu();
5269            } else {
5270                console.warn("checkAdminStatus: renderHorizontalMenu function not found.");
5271            }
5272        }
5273        // --- MODIFICATION END ---
5274        createLoginBackup(); // Create backup after successful login/admin check
5275      } else {
5276          console.log('User is not admin, resetting UI.');
5277          resetAuthUI(); // Ensure non-admin UI state
5278      }
5279
5280      return isAdmin; // Return the admin status
5281
5282    } catch (fetchError) {
5283      clearTimeout(timeoutId); // Clear timeout on error as well
5284      console.error('Fetch error during admin check:', fetchError);
5285
5286      // For preview URLs, still allow editing as a fallback
5287      if (window.location.hostname === 'preview.neonsky.app') {
5288        console.log('Preview URL + fetch error, still enabling editing');
5289        isAdmin = true;
5290        window.isAdmin = true;
5291        document.body.classList.add('hydra-admin');
5292        localStorage.setItem('hydra_is_admin', 'true');
5293        forceShowEditUI();
5294        return true; // Indicate potential success for preview
5295      }
5296
5297      // For non-preview, throw the error to indicate failure
5298      throw fetchError;
5299    }
5300  } catch (error) {
5301    console.error('Error in checkAdminStatus:', error);
5302    // Don't show alert here, let the calling function handle UI feedback
5303    // Reset UI to non-admin state on error
5304    resetAuthUI();
5305    return false; // Indicate failure
5306  }
5307}
5308
5309// UPDATED: Force showing edit UI with class-based approach
5310function forceShowEditUI() {
5311  console.log('Forcing admin/auth state for UI');
5312
5313  // Ensure global state flags are set
5314  isAuthenticated = true;
5315  isAdmin = true;
5316  window.isAuthenticated = true;
5317  window.isAdmin = true;
5318
5319  // Add admin/auth classes to body - CSS will handle button visibility
5320  document.body.classList.add('hydra-authenticated', 'hydra-admin');
5321
5322  // Store admin status in localStorage
5323  localStorage.setItem('hydra_is_admin', 'true');
5324
5325  // Show edit controls container ONLY if already in edit mode
5326  if (window.isInEditMode && window.isInEditMode()) {
5327    const editControls = document.querySelector('.edit-controls');
5328    if (editControls) {
5329      editControls.classList.add('visible');
5330      editControls.style.display = 'flex'; // Make sure container is flex
5331    }
5332    // Ensure controls inside items are visible if already editing
5333     document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5334        if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) {
5335             el.style.display = 'flex';
5336        }
5337    });
5338    
5339    // Show logout button in edit controls when in edit mode
5340    const logoutButton = document.getElementById('logoutButton');
5341    if (logoutButton) {
5342      logoutButton.style.display = 'flex';
5343      logoutButton.classList.add('visible');
5344    }
5345  } else {
5346     // Ensure edit controls container is hidden if not in edit mode
5347     const editControls = document.querySelector('.edit-controls');
5348     if (editControls) {
5349        editControls.classList.remove('visible');
5350        editControls.style.display = 'none';
5351     }
5352     
5353     // Hide logout button when not in edit mode
5354     const logoutButton = document.getElementById('logoutButton');
5355     if (logoutButton) {
5356       logoutButton.style.display = 'none';
5357       logoutButton.classList.remove('visible');
5358     }
5359  }
5360
5361  console.log('Admin/Auth classes set. CSS should handle button visibility.');
5362}
5363
5364// UPDATED: Reset auth UI with class-based approach  
5365function resetAuthUI() {
5366  document.body.classList.remove('hydra-authenticated');
5367  document.body.classList.remove('hydra-admin');
5368  
5369  
5370  const logoutButton = document.getElementById('logoutButton');
5371  if (logoutButton) {
5372    logoutButton.style.display = 'none';
5373    logoutButton.classList.remove('visible');
5374  }
5375  
5376  const editButton = document.getElementById('editButton');
5377  if (editButton) {
5378    editButton.style.display = 'none';
5379    editButton.classList.remove('visible');
5380  }
5381  
5382  // Remove admin status from localStorage
5383  localStorage.removeItem('hydra_is_admin');
5384  localStorage.removeItem('hydra_auth_token');
5385  
5386  if (isEditing) {
5387    toggleEditMode(); // Exit edit mode if active
5388  }
5389}
5390
5391// Debug function - add to your code during testing
5392function debugUIClasses() {
5393  console.log('Current body classes:', document.body.className);
5394  console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized'));
5395  console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated'));
5396  console.log('hydra-admin:', document.body.classList.contains('hydra-admin'));
5397  console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active'));
5398}
5399
5400
5401
5402async function logout() {
5403  console.log('Logout initiated');
5404  
5405  try {
5406    // Show loading indication (optional)
5407    showLoadingOverlay('Logging out...');
5408    
5409    // 1. Properly logout from Magic SDK
5410    if (window.magic) {
5411      try {
5412        await window.magic.user.logout();
5413        console.log('Magic SDK logout successful');
5414      } catch (magicError) {
5415        console.error('Error during Magic logout:', magicError);
5416        // Continue with other logout steps even if Magic SDK logout fails
5417      }
5418    }
5419    
5420    // 2. Reset all authentication state variables
5421    clearHydraClientAuthStorage();
5422    
5423    // 3. Reset all UI state - remove authentication classes from body
5424    document.body.classList.remove('hydra-authenticated');
5425    document.body.classList.remove('hydra-admin');
5426    
5427    // 5. If in edit mode, exit it first
5428    if (document.body.classList.contains('edit-mode-active')) {
5429      console.log('Exiting edit mode before logout');
5430      // If toggleEditMode exists, call it to exit edit mode
5431      if (typeof toggleEditMode === 'function') {
5432        try {
5433          toggleEditMode();
5434        } catch (editError) {
5435          console.error('Error exiting edit mode:', editError);
5436        }
5437      }
5438      
5439      // Ensure edit mode class is removed regardless
5440      document.body.classList.remove('edit-mode-active');
5441    }
5442    
5443    // 6. Ensure all edit UI elements are hidden
5444    
5445    // Hide edit and logout buttons
5446    const editButton = document.getElementById('editButton');
5447    if (editButton) {
5448      editButton.classList.remove('visible');
5449      editButton.style.display = 'none';
5450    }
5451    
5452    const logoutButton = document.getElementById('logoutButton');
5453    if (logoutButton) {
5454      logoutButton.classList.remove('visible');
5455      logoutButton.style.display = 'none';
5456    }
5457    
5458    // Hide edit controls
5459    const editControls = document.querySelector('.edit-controls');
5460    if (editControls) {
5461      editControls.style.display = 'none';
5462      editControls.classList.remove('visible');
5463    }
5464    
5465    // Hide sidebar header if it should be hidden when logged out
5466    const sidebarHeader = document.querySelector('.sidebar-header');
5467    if (sidebarHeader) {
5468      sidebarHeader.style.display = 'none';
5469    }
5470    
5471    // Hide all element controls
5472    document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5473      el.style.display = 'none';
5474    });
5475    
5476    // 7. Reset editor state if applicable
5477    if (window.Parameters) {
5478      window.Parameters.isInEditor = false;
5479    }
5480    
5481    // Hide any editing forms that might be open
5482    const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm');
5483    forms.forEach(form => {
5484      form.style.display = 'none';
5485      if (form.classList.contains('visible')) {
5486        form.classList.remove('visible');
5487      }
5488    });
5489    
5490    // Remove editing class from sidebar
5491    const sidebar = document.querySelector('.sidebar');
5492    if (sidebar) {
5493      sidebar.classList.remove('editing');
5494    }
5495    
5496    // 8. Clear any page-specific state
5497    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
5498      window.PageManager.clearCurrentPage();
5499    }
5500    
5501    // 9. Notify any components that need to know about logout
5502    document.dispatchEvent(new CustomEvent('user-logout', {
5503      detail: { timestamp: Date.now() }
5504    }));
5505    
5506    // 10. Show success message
5507    hideLoadingOverlay();
5508    showSuccessMessage('Successfully logged out');
5509    
5510    console.log('Logout complete');
5511    
5512    // 11. Optional: Refresh the page after a short delay for a clean state
5513    // Uncomment the following lines if you want the page to refresh after logout
5514    /*
5515    setTimeout(() => {
5516      window.location.reload();
5517    }, 1500);
5518    */
5519    
5520  } catch (error) {
5521    console.error('Error during logout process:', error);
5522    hideLoadingOverlay();
5523    showErrorMessage('Error during logout. Please refresh the page.');
5524  }
5525}
5526
5527// Ensure the logout button is properly connected to the logout function
5528document.addEventListener('DOMContentLoaded', function() {
5529  const logoutButton = document.getElementById('logoutButton');
5530  if (logoutButton) {
5531    // Remove any existing event listeners to avoid duplicates
5532    const newButton = logoutButton.cloneNode(true);
5533    logoutButton.parentNode.replaceChild(newButton, logoutButton);
5534    
5535    // Add fresh event listener
5536    newButton.addEventListener('click', function(e) {
5537      e.preventDefault();
5538      e.stopPropagation();
5539      logout();
5540    });
5541  }
5542});
5543
5544function stopAutoAdvanceTimer() {
5545  if (window.currentAutoAdvanceTimerId) {
5546    clearTimeout(window.currentAutoAdvanceTimerId);
5547    window.currentAutoAdvanceTimerId = null; 
5548    console.log('Auto-advance timer stopped.');
5549  }
5550}
5551window.stopAutoAdvanceTimer = stopAutoAdvanceTimer;
5552
5553function beginHydraContentLoad(type, itemId, pageId) {
5554  const generation = (Number(window.__hydraContentLoadGeneration) || 0) + 1;
5555  window.__hydraContentLoadGeneration = generation;
5556  window.__neonGalleryInitEpoch = (Number(window.__neonGalleryInitEpoch) || 0) + 1;
5557  window.__hydraContentLoadOwner = {
5558    generation,
5559    type,
5560    itemId: itemId === undefined || itemId === null ? '' : String(itemId),
5561    pageId: pageId === undefined || pageId === null ? '' : String(pageId)
5562  };
5563  return generation;
5564}
5565
5566function isHydraContentLoadCurrent(generation, type, itemId) {
5567  if (generation != null && Number(generation) !== Number(window.__hydraContentLoadGeneration || 0)) {
5568    return false;
5569  }
5570  if (type && window._activeContentType && window._activeContentType !== type) {
5571    return false;
5572  }
5573  if (itemId != null && itemId !== '' && window.activeGalleryId != null &&
5574      String(window.activeGalleryId) !== String(itemId)) {
5575    return false;
5576  }
5577  return true;
5578}
5579
5580window.beginHydraContentLoad = beginHydraContentLoad;
5581window.isHydraContentLoadCurrent = isHydraContentLoadCurrent;
5582
5583function dismissGalleryPasswordOverlay() {
5584  if (window.PageManager && typeof window.PageManager.removeGalleryPasswordOverlay === 'function') {
5585    window.PageManager.removeGalleryPasswordOverlay();
5586    return;
5587  }
5588  document.querySelectorAll('.gallery-password-protection-overlay').forEach(function(overlay) {
5589    if (overlay._hydraPasswordAbort && typeof overlay._hydraPasswordAbort.abort === 'function') {
5590      overlay._hydraPasswordAbort.abort();
5591    }
5592    overlay.remove();
5593  });
5594}
5595window.dismissGalleryPasswordOverlay = dismissGalleryPasswordOverlay;
5596
5597function toggleEditMode() {
5598  // Prevent multiple simultaneous calls
5599  if (window._editModeToggleInProgress) {
5600    console.log('Edit mode toggle already in progress, skipping');
5601    return;
5602  }
5603  window._editModeToggleInProgress = true;
5604
5605  console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined');
5606
5607  stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance
5608
5609  // Authentication and Authorization Checks
5610  if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) {
5611    alert('You must be logged in to edit');
5612    window._editModeToggleInProgress = false;
5613    return;
5614  }
5615  if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) {
5616    window.isAdmin = true; // Synchronize global flag
5617  }
5618  if (!window.isAdmin) {
5619    alert('You must be an admin to edit');
5620    window._editModeToggleInProgress = false;
5621    return;
5622  }
5623
5624  const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false);
5625  const newEditState = !currentEditState;
5626  
5627  console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`);
5628
5629  // Apply body class for edit mode styling
5630  if (newEditState) {
5631    console.log('Entering edit mode - setting body class and theme');
5632    document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode
5633    document.body.classList.add('edit-mode-active');
5634  } else {
5635    console.log('Exiting edit mode - removing body class and theme');
5636    document.body.removeAttribute('data-theme');
5637    document.body.classList.remove('edit-mode-active');
5638  }
5639
5640  // Update the global state and dispatch event
5641  if (typeof window.updateGlobalEditState === 'function') {
5642    window.updateGlobalEditState(newEditState); 
5643  } else {
5644    window.isEditing = newEditState; // Fallback
5645    document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } }));
5646    if (!newEditState && typeof window.closeOptionsPanel === 'function') {
5647      window.closeOptionsPanel();
5648    }
5649  }
5650
5651  if (document.body.classList.contains('menu-layout-horizontal') && typeof syncContentMarginToHorizontalHeader === 'function') {
5652    setTimeout(syncContentMarginToHorizontalHeader, 50);
5653    setTimeout(syncContentMarginToHorizontalHeader, 350);
5654  }
5655
5656  const sidebarEditControls = document.querySelector('.edit-controls');
5657
5658  if (newEditState) {
5659    // Entering edit mode
5660    if (sidebarEditControls) {
5661        sidebarEditControls.style.display = 'flex';
5662        sidebarEditControls.classList.add('visible');
5663    }
5664    
5665    // Show logout button in edit controls when entering edit mode
5666    const logoutButton = document.getElementById('logoutButton');
5667    if (logoutButton) {
5668      logoutButton.style.display = 'flex';
5669      logoutButton.classList.add('visible');
5670    }
5671    
5672    // Always render the tree-like menu in the sidebar for editing
5673    if (typeof window.renderGalleries === 'function') {
5674        console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.');
5675        window.renderGalleries(); // This will show all items, including those with visible:false
5676    }
5677
5678    setTimeout(() => {
5679        if (typeof window.initializeNestedSortables === 'function') {
5680            console.log('Initializing nested sortables for edit mode');
5681            window.initializeNestedSortables();
5682        }
5683        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
5684            window.SidebarManager._reinitializeNestedSortables();
5685        }
5686    }, 150);
5687
5688    // If the currently active item was invisible, reload it now that we are in edit mode.
5689    if(window.galleries && window.activeGalleryId){
5690        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5691        if (currentGalleryItem && currentGalleryItem.visible === false) {
5692            console.log('toggleEditMode (Edit): Active item was invisible, reloading it.');
5693            if (currentGalleryItem.isPage && window.loadPage) {
5694                window.loadPage(window.activeGalleryId);
5695            } else if(window.loadGallery) {
5696                window.loadGallery(window.activeGalleryId);
5697            }
5698        }
5699    }
5700    if(typeof window.showStyleEditor === 'function' &&
5701        !(window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen)) {
5702      window.showStyleEditor(false); // Ensure style editor is hidden initially
5703    }
5704
5705  } else {
5706    // Exiting edit mode (going to Live view)
5707    if (sidebarEditControls) {
5708        sidebarEditControls.style.display = 'none';
5709        sidebarEditControls.classList.remove('visible');
5710    }
5711    
5712    // Hide logout button when exiting edit mode
5713    const logoutButton = document.getElementById('logoutButton');
5714    if (logoutButton) {
5715      logoutButton.style.display = 'none';
5716      logoutButton.classList.remove('visible');
5717    }
5718    if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer._prepareOverlayForLiveView === 'function') {
5719      window.MenuStyleCustomizer._prepareOverlayForLiveView();
5720    }
5721    if(typeof window.closeAllForms === 'function') window.closeAllForms();
5722    
5723    if (typeof window.destroyNestedSortables === 'function') {
5724        window.destroyNestedSortables();
5725    } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) {
5726        window.sortableInstances.forEach(instance => instance.destroy());
5727        window.sortableInstances = [];
5728    }
5729
5730    // Re-render the menu based on the actual view mode layout.
5731    // These functions should internally handle not showing items with visible:false.
5732    let currentLayout = 'sidebar';
5733    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5734        currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5735    }
5736    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
5737        window.renderHorizontalMenu();
5738    } else if (typeof window.renderGalleries === 'function') {
5739        window.renderGalleries();
5740    }
5741
5742    // Re-render sidebar elements (social, text, etc.) so edit-only UI (grid, dashed slots) is replaced by live view
5743    if (window.SidebarManager && typeof window.SidebarManager._syncWithGlobalEditState === 'function' && typeof window.SidebarManager.renderElements === 'function') {
5744      window.SidebarManager._syncWithGlobalEditState();
5745      window.SidebarManager.renderElements();
5746    }
5747
5748    // Handle the currently active gallery/page.
5749    if (window.galleries && window.activeGalleryId) {
5750        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5751        if (currentGalleryItem) {
5752            // The content for currentGalleryItem should remain loaded,
5753            // even if currentGalleryItem.visible is false.
5754            // The menu rendering above will hide it from the list if it's invisible.
5755            
5756            // Apply page-specific menu hiding rules.
5757            const bodyEl = document.body;
5758            // When exiting edit mode, isInEditMode() will effectively be false for this check.
5759            if (shouldApplyMenuHiddenForEmbedOrGallery(currentGalleryItem)) { 
5760                bodyEl.classList.add('menu-hidden-on-page');
5761            } else {
5762                bodyEl.classList.remove('menu-hidden-on-page');
5763            }
5764            applyMenuOverlayClassForGallery(currentGalleryItem);
5765            // Ensure active states in the (potentially now filtered) menu are updated.
5766            // If the active item is invisible, it won't be marked active in the menu, which is fine.
5767            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5768            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5769
5770        } else {
5771            // activeGalleryId points to a non-existent item, so clear it and the content.
5772            console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.');
5773            window.activeGalleryId = null;
5774            const galleryContainer = document.querySelector('.gallery-container');
5775            if (galleryContainer) galleryContainer.innerHTML = '';
5776            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5777            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5778        }
5779    }
5780    
5781    // If, after all that, no content is effectively active (e.g., initial load to root, 
5782    // or activeGalleryId became null because the item was deleted)
5783    // then try to load a default page (home page or first visible gallery).
5784    const galleryContainer = document.querySelector('.gallery-container');
5785    // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted)
5786    const noActiveContent = window.activeGalleryId === null || 
5787                            (galleryContainer && galleryContainer.innerHTML.trim() === '');
5788
5789    if (noActiveContent && window.galleries) {
5790      console.log("toggleEditMode (Live): No active content, attempting to load default page.");
5791      const homePage = window.galleries.find(g => g.isHomePage === true);
5792      let loadedDefault = false;
5793      if (homePage) {
5794          // Load home page regardless of its visibility status for this default loading logic
5795          console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`);
5796          if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; }
5797          else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; }
5798      }
5799      
5800      if (!loadedDefault) {
5801          // If no home page, load the first *visible* gallery/page in live view.
5802          const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
5803          if (firstVisibleGallery) {
5804            console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`);
5805            if (firstVisibleGallery.isPage && window.loadPage) {
5806                window.loadPage(firstVisibleGallery.id);
5807            } else if(window.loadGallery) {
5808                window.loadGallery(firstVisibleGallery.id);
5809            }
5810          } else {
5811            console.log('toggleEditMode (Live): No home page and no visible items to load.');
5812          }
5813      }
5814    }
5815
5816    if (window.MenuStyleCustomizer &&
5817        window.MenuStyleCustomizer._overlayOpen &&
5818        typeof window.MenuStyleCustomizer.applyStyles === 'function') {
5819      window.MenuStyleCustomizer.applyStyles();
5820    }
5821  }
5822  window._editModeToggleInProgress = false;
5823}
5824
5825
5826
5827/**
5828 * Improved toggleSidebarElementForm - Closes other forms first
5829 */
5830function toggleSidebarElementForm() {
5831  const formId = 'sidebarElementForm';
5832  const form = document.getElementById(formId);
5833  
5834  if (!form) return;
5835  
5836  // If this form is already open, just close everything
5837  if (window.currentOpenForm === formId) {
5838    closeAllForms();
5839    return;
5840  }
5841  
5842  // Otherwise close all forms and open this one
5843  closeAllForms();
5844  
5845  // Show the form
5846  form.style.display = 'block';
5847  form.classList.add('visible');
5848  
5849  // Set this as the current open form
5850  window.currentOpenForm = formId;
5851}
5852
5853// New function to add a sidebar element
5854function addSidebarElement() {
5855const typeInput = document.querySelector('input[name="sidebarElementType"]:checked');
5856if (!typeInput) {
5857alert('Please select an element type');
5858return;
5859}
5860
5861const type = typeInput.value;
5862
5863// Use the SidebarManager to add the element
5864if (window.SidebarManager) {
5865const newElementId = window.SidebarManager.addElement(type);
5866toggleSidebarElementForm();
5867
5868if (newElementId) {     
5869  // Save the changes to KV store
5870  saveGalleries().then(() => {
5871    console.log(`New ${type} element added successfully with ID: ${newElementId}`);
5872  }).catch(error => {
5873    console.error('Error saving new element:', error);
5874  });
5875}
5876} else {
5877alert('SidebarManager not available');
5878}
5879}
5880
5881
5882// Global variable to store metadata
5883window.siteMetadata = {
5884  title: '',
5885  description: '',
5886  googleAnalytics: '',
5887  noIndex: false,
5888  favicon: '',
5889  ogImageUrl: '',
5890  contentLanguage: 'en',  // Default to English
5891  copyrightEnabled: false,
5892  copyrightText: '',
5893  customPublicSiteEnabled: false,
5894  customPublicSiteHtmlR2Key: '',
5895  customPublicSiteHtmlFilename: '',
5896  siteWideBasicAuth: undefined
5897};
5898
5899// Global variable to store uploaded favicon URL temporarily
5900window.uploadedFaviconUrl = '';
5901
5902function updateMetadataCopyrightFieldState() {
5903  const toggle = document.getElementById('metadataCopyrightEnabled');
5904  const textInput = document.getElementById('metadataCopyrightText');
5905  const isEnabled = !!(toggle && toggle.checked);
5906
5907  if (textInput) {
5908    textInput.disabled = !isEnabled;
5909  }
5910
5911  // Intentionally leave the surrounding form group in place; the disabled attribute on the input
5912  // provides the visual cue in PicoCSS.
5913}
5914
5915/**
5916 * Improved toggleMetadataEditor - Closes other forms first
5917 */
5918function toggleMetadataEditor() {
5919  const formId = 'metadataEditor';
5920  const editor = document.getElementById(formId);
5921  
5922  if (!editor) return;
5923  
5924  // If this form is already open, just close everything
5925  if (window.currentOpenForm === formId) {
5926    closeAllForms();
5927    return;
5928  }
5929  
5930  // Otherwise close all forms and open this one
5931  closeAllForms();
5932  
5933  // Load current values before showing
5934  loadMetadataValues();
5935  editor.style.display = 'block';
5936  
5937  // Set this as the current open form
5938  window.currentOpenForm = formId;
5939  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
5940    syncCustomPublicSiteBypassVisibility();
5941  }
5942}
5943
5944
5945// Load metadata values into the form
5946function loadMetadataValues() {
5947  // Get the current values from the global object
5948  const metadata = window.siteMetadata || {};
5949  console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata));
5950  console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics);
5951  
5952  console.warn('=== FAVICON LOAD DEBUG ===');
5953  console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata));
5954  console.warn('metadata object:', JSON.stringify(metadata));
5955  console.warn('metadata.favicon value:', metadata.favicon);
5956  console.warn('metadata.favicon type:', typeof metadata.favicon);
5957  
5958  // Populate form fields
5959  document.getElementById('metaTitle').value = metadata.title || '';
5960  document.getElementById('metaDescription').value = metadata.description || '';
5961  document.getElementById('googleAnalytics').value = metadata.googleAnalytics || '';
5962  document.getElementById('noIndexToggle').checked = metadata.noIndex || false;
5963
5964  // Schema / Identity fields
5965  const schemaType = metadata.schemaType || '';
5966  const personRadio = document.getElementById('schemaTypePerson');
5967  const orgRadio = document.getElementById('schemaTypeOrganization');
5968  if (personRadio) personRadio.checked = schemaType === 'Person';
5969  if (orgRadio) orgRadio.checked = schemaType === 'Organization';
5970  const jobTitleEl = document.getElementById('schemaJobTitle');
5971  if (jobTitleEl) jobTitleEl.value = metadata.schemaJobTitle || '';
5972  const cityEl = document.getElementById('schemaCity');
5973  if (cityEl) cityEl.value = metadata.schemaCity || '';
5974  const emailEl = document.getElementById('schemaEmail');
5975  if (emailEl) emailEl.value = metadata.schemaEmail || '';
5976  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
5977  if (imageLicenseUrlEl) imageLicenseUrlEl.value = metadata.imageLicenseUrl || '';
5978  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
5979  if (acquireLicensePageUrlEl) acquireLicensePageUrlEl.value = metadata.acquireLicensePageUrl || '';
5980  if (typeof updateSchemaFields === 'function') updateSchemaFields();
5981
5982  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
5983  const copyrightInput = document.getElementById('metadataCopyrightText');
5984  if (copyrightToggle) {
5985    const enabled = !!metadata.copyrightEnabled;
5986    copyrightToggle.checked = enabled;
5987    if (copyrightInput) {
5988      copyrightInput.value = metadata.copyrightText || '';
5989    }
5990  }
5991  updateMetadataCopyrightFieldState();
5992
5993  const customPubToggle = document.getElementById('customPublicSiteEnabled');
5994  if (customPubToggle) {
5995    customPubToggle.checked = !!metadata.customPublicSiteEnabled;
5996  }
5997  const customPubKeyEl = document.getElementById('customPublicSiteHtmlR2Key');
5998  if (customPubKeyEl) {
5999    customPubKeyEl.value = metadata.customPublicSiteHtmlR2Key || '';
6000  }
6001  const customPubFileLabel = document.getElementById('customPublicSiteHtmlFilenameLabel');
6002  if (customPubFileLabel) {
6003    customPubFileLabel.textContent = metadata.customPublicSiteHtmlFilename
6004      ? ('Current file: ' + metadata.customPublicSiteHtmlFilename)
6005      : 'No HTML file uploaded yet.';
6006  }
6007
6008  const swAuth = metadata.siteWideBasicAuth;
6009  const swEnabled = !!(swAuth && swAuth.username && swAuth.password);
6010  const swEnableEl = document.getElementById('siteWideBasicAuthEnabled');
6011  if (swEnableEl) swEnableEl.checked = swEnabled;
6012  const swUserEl = document.getElementById('siteWideBasicAuthUsername');
6013  if (swUserEl) swUserEl.value = swAuth && swAuth.username ? swAuth.username : '';
6014  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6015  if (swPassEl) swPassEl.value = swAuth && swAuth.password ? swAuth.password : '';
6016  const swRealmEl = document.getElementById('siteWideBasicAuthRealm');
6017  if (swRealmEl) swRealmEl.value = swAuth && swAuth.realm ? swAuth.realm : '';
6018  if (typeof updateSiteWideBasicAuthFieldState === 'function') updateSiteWideBasicAuthFieldState();
6019  
6020  // Set content language dropdown (defaults to 'en')
6021  const contentLanguageSelect = document.getElementById('contentLanguage');
6022  if (contentLanguageSelect) {
6023    contentLanguageSelect.value = metadata.contentLanguage || 'en';
6024    console.log('Loaded content language:', contentLanguageSelect.value);
6025  }
6026  
6027  const faviconInput = document.getElementById('faviconUrl');
6028  if (faviconInput) {
6029    // Check if we have a recently uploaded favicon URL
6030    const faviconValue = metadata.favicon || window.uploadedFaviconUrl || '';
6031    faviconInput.value = faviconValue;
6032    console.warn('Set favicon input value to:', faviconInput.value);
6033    console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl);
6034  } else {
6035    console.error('Favicon input element not found!');
6036  }
6037  
6038  // Show favicon preview if there's an existing favicon
6039  if (metadata.favicon) {
6040    console.warn('Showing favicon preview for:', metadata.favicon);
6041    showFaviconPreview(metadata.favicon);
6042  } else {
6043    console.warn('No favicon to preview');
6044  }
6045
6046  const ogInput = document.getElementById('ogImageUrl');
6047  if (ogInput) {
6048    const ogVal = (metadata.ogImageUrl || '').trim();
6049    ogInput.value = ogVal;
6050  }
6051  const ogBtnText = document.getElementById('uploadOgImageBtnText');
6052  if (ogBtnText) {
6053    ogBtnText.textContent = (metadata.ogImageUrl || '').trim() ? 'Change image' : 'Upload image';
6054  }
6055  if ((metadata.ogImageUrl || '').trim()) {
6056    showOgImagePreview(metadata.ogImageUrl.trim());
6057  } else {
6058    const ogCont = document.getElementById('ogImagePreviewContainer');
6059    if (ogCont) ogCont.innerHTML = '';
6060  }
6061  
6062  console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value);
6063}
6064
6065// Save metadata values
6066function saveMetadata() {
6067  // Get values from form
6068  const googleAnalyticsElement = document.getElementById('googleAnalytics');
6069  console.log('Google Analytics element found:', !!googleAnalyticsElement);
6070  console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND');
6071  
6072  const faviconInput = document.getElementById('faviconUrl');
6073  const faviconValue = faviconInput ? faviconInput.value : '';
6074  const ogImageInput = document.getElementById('ogImageUrl');
6075  const ogImageValue = ogImageInput ? (ogImageInput.value || '').trim() : '';
6076  
6077  const contentLanguageSelect = document.getElementById('contentLanguage');
6078  const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en';
6079
6080  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
6081  const copyrightInput = document.getElementById('metadataCopyrightText');
6082  const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false;
6083  const copyrightText = copyrightInput ? copyrightInput.value.trim() : '';
6084  
6085  console.warn('=== FAVICON SAVE DEBUG ===');
6086  console.warn('Favicon input element exists:', !!faviconInput);
6087  console.warn('Favicon input value:', faviconValue);
6088  console.warn('Favicon input value length:', faviconValue.length);
6089  
6090  console.warn('=== CONTENT LANGUAGE SAVE ===');
6091  console.warn('Content language selected:', contentLanguageValue);
6092  
6093  const rawGa = googleAnalyticsElement ? googleAnalyticsElement.value : '';
6094  // Normalize GA to ID only (client may not have normalizeGoogleAnalyticsId in scope)
6095  let gaId = (rawGa && typeof rawGa === 'string') ? rawGa.trim() : '';
6096  if (gaId && !/^G-[A-Z0-9]+$/i.test(gaId) && !/^UA-d+-d+$/.test(gaId)) {
6097    const match = gaId.match(/(G-[A-Z0-9]+|UA-d+-d+)/i);
6098    gaId = match ? match[1] : '';
6099  }
6100  const customPubKeyInput = document.getElementById('customPublicSiteHtmlR2Key');
6101  const customPubFilename = document.getElementById('customPublicSiteHtmlFilenameStore');
6102  const schemaPersonRadio = document.getElementById('schemaTypePerson');
6103  const schemaOrgRadio = document.getElementById('schemaTypeOrganization');
6104  const schemaType = schemaPersonRadio && schemaPersonRadio.checked ? 'Person'
6105    : schemaOrgRadio && schemaOrgRadio.checked ? 'Organization'
6106    : '';
6107  const schemaJobTitleEl = document.getElementById('schemaJobTitle');
6108  const schemaCityEl = document.getElementById('schemaCity');
6109  const schemaEmailEl = document.getElementById('schemaEmail');
6110  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
6111  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
6112  const imageLicenseUrlRaw = imageLicenseUrlEl ? imageLicenseUrlEl.value.trim() : '';
6113  const acquireLicensePageUrlRaw = acquireLicensePageUrlEl ? acquireLicensePageUrlEl.value.trim() : '';
6114
6115  const normalizePublicAbsoluteUrl = (value) => {
6116    if (!value) return '';
6117    try {
6118      const parsed = new URL(value);
6119      if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return '';
6120      return parsed.href;
6121    } catch {
6122      return '';
6123    }
6124  };
6125  const imageLicenseUrl = normalizePublicAbsoluteUrl(imageLicenseUrlRaw);
6126  const acquireLicensePageUrl = normalizePublicAbsoluteUrl(acquireLicensePageUrlRaw);
6127  if ((imageLicenseUrlRaw || acquireLicensePageUrlRaw) && (!imageLicenseUrl || !acquireLicensePageUrl)) {
6128    alert('Image licensing: enter two public http(s) URLs, or leave both fields empty.');
6129    return;
6130  }
6131
6132  const siteWideBasicAuthEnabled = !!(document.getElementById('siteWideBasicAuthEnabled') && document.getElementById('siteWideBasicAuthEnabled').checked);
6133  const swUser = document.getElementById('siteWideBasicAuthUsername') ? document.getElementById('siteWideBasicAuthUsername').value.trim() : '';
6134  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6135  const swPass = swPassEl ? swPassEl.value : '';
6136  const swRealm = document.getElementById('siteWideBasicAuthRealm') ? document.getElementById('siteWideBasicAuthRealm').value.trim() : '';
6137
6138  if (siteWideBasicAuthEnabled && (!swUser || !swPass)) {
6139    alert('Site-wide password: enter both username and password, or turn off the option.');
6140    return;
6141  }
6142
6143  const metadata = {
6144    title: document.getElementById('metaTitle').value,
6145    description: document.getElementById('metaDescription').value,
6146    googleAnalytics: gaId,
6147    noIndex: document.getElementById('noIndexToggle').checked,
6148    favicon: faviconValue,
6149    ogImageUrl: ogImageValue,
6150    contentLanguage: contentLanguageValue,
6151    copyrightEnabled,
6152    copyrightText,
6153    customPublicSiteEnabled: !!(document.getElementById('customPublicSiteEnabled') && document.getElementById('customPublicSiteEnabled').checked),
6154    customPublicSiteHtmlR2Key: customPubKeyInput ? customPubKeyInput.value.trim() : '',
6155    customPublicSiteHtmlFilename: customPubFilename ? customPubFilename.value.trim() : '',
6156    schemaType: schemaType || undefined,
6157    schemaJobTitle: schemaJobTitleEl ? schemaJobTitleEl.value.trim() : '',
6158    schemaCity: schemaCityEl ? schemaCityEl.value.trim() : '',
6159    schemaEmail: schemaEmailEl ? schemaEmailEl.value.trim() : '',
6160    ...(imageLicenseUrl && acquireLicensePageUrl ? { imageLicenseUrl, acquireLicensePageUrl } : {})
6161  };
6162
6163  if (siteWideBasicAuthEnabled && swUser && swPass) {
6164    metadata.siteWideBasicAuth = {
6165      username: swUser,
6166      password: swPass,
6167      ...(swRealm ? { realm: swRealm } : {})
6168    };
6169  }
6170  
6171  console.warn('Metadata object being created:', JSON.stringify(metadata));
6172  console.warn('Favicon in metadata object:', metadata.favicon);
6173
6174  // Show normalized GA ID in the form so user sees what was saved (e.g. after pasting full snippet)
6175  if (googleAnalyticsElement && metadata.googleAnalytics !== rawGa.trim()) {
6176    googleAnalyticsElement.value = metadata.googleAnalytics;
6177  }
6178
6179  // Update global metadata object immediately
6180  window.siteMetadata = metadata;
6181  console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata));
6182  if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
6183    window.SidebarManager.updateMetadataFooter();
6184  }
6185
6186  // Ensure we have the latest galleries, sidebar elements, and styles
6187  // before constructing the customData object.
6188
6189  // Synchronize galleries first if the function exists
6190  if (typeof synchronizeGalleries === 'function') {
6191      synchronizeGalleries();
6192      console.log('Galleries synchronized within saveMetadata');
6193  }
6194
6195  // Retrieve current galleries (use window.galleries as the source of truth after sync)
6196  const currentGalleries = window.galleries || galleries || [];
6197  console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`);
6198
6199  // Retrieve current sidebar elements
6200  const currentSidebarElements = window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function'
6201    ? window.SidebarManager.getElementsForSave()
6202    : null;
6203
6204  // Retrieve current menu styles
6205  const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {};
6206
6207  // Now construct customData using the retrieved current state
6208  if (window.saveGalleries) {
6209    const customData = {
6210      galleries: currentGalleries, // Use the synchronized/retrieved galleries
6211      siteMetadata: metadata, // Use the metadata gathered from the form
6212      menuStyles: currentMenuStyles // Use the retrieved menu styles
6213    };
6214    if (Array.isArray(currentSidebarElements) && currentSidebarElements.length > 0) {
6215      customData.sidebarElements = currentSidebarElements;
6216    }
6217  
6218
6219    console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`);
6220    console.log("Metadata being saved:", JSON.stringify(metadata));
6221    console.log("Google Analytics specifically:", metadata.googleAnalytics);
6222    console.log("Sidebar elements being saved:", Array.isArray(customData.sidebarElements) ? customData.sidebarElements.length : '(omitted)');
6223    console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles));
6224
6225    // Save with our correctly constructed custom data object
6226    window.saveGalleries(customData)
6227      .then(() => {
6228        alert('Metadata saved successfully');
6229        toggleMetadataEditor(); // Hide the editor
6230      })
6231      .catch(error => {
6232        console.error('Error saving metadata:', error);
6233        alert('Error saving metadata. Please try again.');
6234      });
6235  } else {
6236    alert('Save function not available');
6237  }
6238}
6239
6240  
6241// Function to add new entries to the style editor
6242function showStyleEditor(display = false) {
6243  if (display && window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.openOverlay === 'function') {
6244    window.MenuStyleCustomizer.openOverlay();
6245    window.currentOpenForm = 'menuStyleEditor';
6246    return;
6247  }
6248  hideStyleEditor();
6249}
6250
6251// New function to hide the style editor
6252function hideStyleEditor() {
6253  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6254    window.MenuStyleCustomizer.closeOverlay();
6255  }
6256  const styleEditor = document.getElementById('menuStyleEditor');
6257  if (styleEditor) {
6258    styleEditor.style.display = 'none';
6259  }
6260}
6261
6262
6263/**
6264 * Improved toggleAddForm - Closes other forms first and ensures proper sizing
6265 */
6266function toggleAddForm() {
6267  const formId = 'addForm';
6268  const form = document.getElementById(formId);
6269  
6270  if (!form) return;
6271  
6272  // If this form is already open, just close everything
6273  if (window.currentOpenForm === formId) {
6274    closeAllForms();
6275    return;
6276  }
6277  
6278  // Otherwise close all forms and open this one
6279  closeAllForms();
6280  
6281  // Determine the appropriate max-height based on content
6282  // First check if we're showing the page form by checking the radio button
6283  const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked;
6284  
6285  // Allow extra space for the page form
6286  const maxHeight = isPageSelected ? '600px' : '500px';
6287  
6288  // Show the form with proper max height
6289  form.style.maxHeight = maxHeight;
6290  form.style.overflow = 'visible';
6290 // Ensure all content is visible
6291  form.classList.add('visible');
6292  
6293  // Also add inline styles to ensure visibility during animation
6294  form.style.padding = '15px';
6295  
6296  // Force recalculation of max-height after UI is visible
6297  setTimeout(() => {
6298    // Get actual content height + padding
6299    const contentHeight = form.scrollHeight + 30; // Add padding
6300    
6301    // Set max-height based on content with minimum of 500px
6302    form.style.maxHeight = Math.max(contentHeight, 500) + 'px';
6303    
6304    console.log(`Setting add form max height to ${form.style.maxHeight}`);
6305  }, 50);
6306  
6307  // Populate parent options
6308  const parentSelect = document.getElementById('galleryParent');
6309  if (parentSelect) {
6310    parentSelect.innerHTML = '<option value="">None (Top Level)</option>';
6311    
6312    // Add all galleries that could be parents (including submenus)
6313    galleries.forEach(gallery => {
6314      parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
6315    });
6316  }
6317  
6318  // Set this as the current open form
6319  window.currentOpenForm = formId;
6320}
6321
6322
6323// Store the currently open form ID
6324window.currentOpenForm = null;
6325
6326/**
6327 * Close a child panel opened on top of the Style Customizer (palette explorer, custom code, color mixer).
6328 * Returns true when a child was closed so callers can keep the Style Customizer open.
6329 */
6330function closeTopmostStyleCustomizerChildOverlay() {
6331  if (document.querySelector('.pe-palette-explorer-overlay')) {
6332    if (window.PaletteExplorer && typeof window.PaletteExplorer.close === 'function') {
6333      const wasOpen = !!document.querySelector('.pe-palette-explorer-overlay');
6334      window.PaletteExplorer.close();
6335      if (wasOpen && document.querySelector('.pe-palette-explorer-overlay')) {
6336        return false;
6337      }
6338      return wasOpen;
6339    }
6340  }
6341  if (document.querySelector('.pe-style-history-overlay, .sc-style-history-overlay') &&
6342      window.MenuStyleCustomizer &&
6343      typeof window.MenuStyleCustomizer.closeStyleHistoryOverlay === 'function') {
6344    window.MenuStyleCustomizer.closeStyleHistoryOverlay();
6345    return true;
6346  }
6347  if (document.querySelector('.pe-font-explorer-overlay') &&
6348      window.MenuStyleCustomizer &&
6349      typeof window.MenuStyleCustomizer.closeFontExplorer === 'function') {
6350    window.MenuStyleCustomizer.closeFontExplorer();
6351    return true;
6352  }
6353  if (document.querySelector('.pe-custom-code-overlay') &&
6354      window.MenuStyleCustomizer &&
6355      typeof window.MenuStyleCustomizer.closeCustomCodeOverlay === 'function') {
6356    window.MenuStyleCustomizer.closeCustomCodeOverlay();
6357    return true;
6358  }
6359  const colorMixer = document.getElementById('colorMixerPopup');
6360  if (colorMixer && colorMixer.classList.contains('is-open')) {
6361    colorMixer.classList.remove('is-open');
6362    return true;
6363  }
6364  return false;
6365}
6366
6367/**
6368 * Close all editor forms with improved handling for add form
6369 */
6370function closeAllForms() {
6371  if (typeof closeTopmostStyleCustomizerChildOverlay === 'function') {
6372    while (closeTopmostStyleCustomizerChildOverlay()) {
6373      // Close nested Style Customizer panels (palette, custom code, color mixer) first.
6374    }
6375  }
6376
6377  const keepMinimizedStyleCustomizer = !!(window.MenuStyleCustomizer &&
6378    window.MenuStyleCustomizer._overlayOpen &&
6379    window.MenuStyleCustomizer._overlayMinimized);
6380
6381  if (!keepMinimizedStyleCustomizer &&
6382      window.MenuStyleCustomizer &&
6383      typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6384    const styleOverlayWasOpen = window.MenuStyleCustomizer._overlayOpen;
6385    window.MenuStyleCustomizer.closeOverlay();
6386    if (styleOverlayWasOpen && window.MenuStyleCustomizer._overlayOpen) {
6387      return;
6388    }
6389  }
6390
6391  // Add Form - Special handling to ensure proper animation
6392  const addForm = document.getElementById('addForm');
6393  if (addForm) {
6394    addForm.classList.remove('visible');
6395    
6396    // Also force style reset after animation completes
6397    setTimeout(() => {
6398      if (!addForm.classList.contains('visible')) {
6399        addForm.style.maxHeight = '0';
6400        addForm.style.overflow = 'hidden';
6401        addForm.style.padding = '0 15px';
6402      }
6403    }, 350); // Set slightly longer than CSS transition
6404  }
6405  
6406  // Style Editor (do not hide the overlay host while the minimized mutate bar is live)
6407  const styleEditor = document.getElementById('menuStyleEditor');
6408  if (styleEditor && !keepMinimizedStyleCustomizer) {
6409    styleEditor.style.display = 'none';
6410  }
6411  
6412  // Metadata Editor
6413  const metadataEditor = document.getElementById('metadataEditor');
6414  if (metadataEditor) {
6415    metadataEditor.style.display = 'none';
6416  }
6417  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
6418    syncCustomPublicSiteBypassVisibility();
6419  }
6420  
6421  // Sidebar Element Form
6422  const sidebarElementForm = document.getElementById('sidebarElementForm');
6423  if (sidebarElementForm) {
6424    sidebarElementForm.style.display = 'none';
6425    sidebarElementForm.classList.remove('visible');
6426  }
6427  
6428  // Import Classic Form
6429  const importClassicForm = document.getElementById('importClassicForm');
6430  if (importClassicForm) {
6431    importClassicForm.style.display = 'none';
6432    importClassicForm.classList.remove('visible');
6433  }
6434  
6435  // Clear the current open form
6436  window.currentOpenForm = null;
6437}
6438function downloadJson() {
6439  const data = JSON.stringify(galleries, null, 2);
6440  const blob = new Blob([data], { type: 'application/json' });
6441  const url = URL.createObjectURL(blob);
6442  const a = document.createElement('a');
6443  a.href = url;
6444  a.download = 'galleries.json';
6445  document.body.appendChild(a);
6446  a.click();
6447  document.body.removeChild(a);
6448  URL.revokeObjectURL(url);
6449}
6450
6451let galleriesBackup = [];
6452
6453
6454
6455/**
6456 * Improved version of initializeNestedSortables with a better nesting approach
6457 */
6458function initializeNestedSortables() {
6459  console.log('Initializing nested sortables with improved nesting logic...');
6460
6461  // Destroy leftover instances before binding again. A second Sortable on the
6462  // same list makes items preview during drag and snap back on release.
6463  document.querySelectorAll('#galleryTree .nested-sortable').forEach((el) => {
6464    try {
6465      const existing = typeof Sortable !== 'undefined' && Sortable.get ? Sortable.get(el) : null;
6466      if (existing && typeof existing.destroy === 'function') existing.destroy();
6467    } catch (error) {
6468      console.warn('Could not destroy existing nested sortable:', error);
6469    }
6470  });
6471
6472  // Create a backup of galleries
6473  try {
6474    galleriesBackup = JSON.parse(JSON.stringify(galleries));
6475  } catch (error) {
6476    console.error('Error creating galleries backup:', error);
6477    galleriesBackup = [];
6478  }
6479
6480  // Find all nested sortable lists
6481  const nestedSortables = document.querySelectorAll('.nested-sortable');
6482  if (nestedSortables.length === 0) {
6483    console.warn('No nested sortable elements found! Check your HTML 
6483structure.');
6484    return;
6485  }
6486
6487  // Helper: compute depth based on ancestor nested lists
6488  function computeDepth(liEl) {
6489    let depth = 0;
6490    let node = liEl;
6491    while (node) {
6492      const parentOl = node.closest('ol.nested-sortable');
6493      if (!parentOl) break;
6494      const parentLi = parentOl.closest('li');
6495      if (parentLi) {
6496        depth += 1;
6497        node = parentLi;
6498      } else {
6499        break;
6500      }
6501    }
6502    // Top-level depth should be 0
6503    return Math.max(depth - 1, 0);
6504  }
6505
6506  // Helper: apply indentation styling to an li and its subtree
6507  function applyIndentationFromDepth(rootLi) {
6508    if (!rootLi) return;
6509    const applyToItem = (li) => {
6510      const depth = computeDepth(li);
6511      // Update data-nesting-level attribute
6512      li.setAttribute('data-nesting-level', depth);
6513      // Apply padding-left style
6514      const content = li.querySelector('.gallery-item-content');
6515      if (content) {
6516        content.style.paddingLeft = (depth * 16) + 'px';
6517      }
6518    };
6519    applyToItem(rootLi);
6520    rootLi.querySelectorAll('li').forEach(applyToItem);
6521  }
6522
6523  // Helper: ensure parent has proper state and nested list
6524  function ensureParentChildState(parentLi) {
6525    if (!parentLi) return null;
6526    parentLi.classList.add('has-children');
6527    let nestedList = parentLi.querySelector('ol.nested-sortable');
6528    if (!nestedList) {
6529      nestedList = document.createElement('ol');
6530      nestedList.className = 'nested-sortable';
6531      parentLi.appendChild(nestedList);
6532    }
6533    return nestedList;
6534  }
6535
6536  // Helper: clean parent if it has no children
6537  function cleanupParentIfEmpty(parentLi) {
6538    if (!parentLi) return;
6539    const nestedList = parentLi.querySelector(':scope > ol.nested-sortable');
6540    const parentId = parseInt(parentLi.dataset && parentLi.dataset.id, 10);
6541    const parentItem = !isNaN(parentId) ? galleries.find(g => g.id === parentId) : null;
6542    // Opted-in pages/galleries must keep an empty drop zone and chevron.
6543    if (isNavigableMenuParent(parentItem)) {
6544      parentLi.classList.add('has-children');
6545      return;
6546    }
6547    if (nestedList && nestedList.children.length === 0) {
6548      nestedList.remove();
6549      parentLi.classList.remove('has-children');
6550    }
6551  }
6552
6553  // Initialize each sortable with improved nesting settings
6554  let sortableInstances = [];
6555
6556  for (let i = 0; i < nestedSortables.length; i++) {
6557    try {
6558      const container = nestedSortables[i];
6559      
6560      // Track where the placeholder indicates the item should be placed
6561      // This helps us detect when SortableJS incorrectly auto-nests based on mouse position
6562      let intendedPlacementContainer = null;
6563      let intendedRelatedElement = null;
6564      
6565      // Track last move to prevent oscillation
6566      let lastMoveTime = 0;
6567      let lastMoveRelatedId = null;
6568      const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds)
6569      
6570      // Track if we're dragging the last item in a folder (to prevent boundary oscillation)
6571      let isLastItemInFolder = false;
6572      let draggedItemOriginalContainer = null;
6573      
6574      // Track container changes to prevent rapid switching (hysteresis)
6575      let firstPositionFixLoopActive = false; // Prevent multiple fix loops
6576      let lastContainerDecision = null; // 'inside' or 'outside'
6577      let lastContainerDecisionTime = 0;
6578      const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches
6579      
6580      const sortable = new Sortable(container, {
6581        group: 'nested',
6582        animation: 150, // Reduced animation during drag to reduce lag
6583        easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function
6584        fallbackOnBody: false, // Disable forced fallback on desktop for better performance
6585        forceFallback: false, // Use native drag on desktop
6586        
6587        // CRITICAL: These settings make nesting much easier
6588        // Higher swap threshold makes it easier to drag over items
6589        swapThreshold: 0.65, // Increased to reduce oscillation within same folder
6590        
6591        // IMPROVED NESTING SETTINGS
6592        // Used when determining if an item should be nested
6593        invertSwap: false,   // Disable to reduce oscillation
6594        direction: 'vertical', // Lock direction to vertical to reduce oscillation
6595        
6596        // Use handle for dragging
6597        handle: '.nested-sortable-handle',
6598        
6599        // Filter out only the toggle button - this prevents SortableJS from intercepting clicks on it
6600        // The folder text (.menu-item.folder) is NOT filtered, so it can be used for both:
6601        // - Quick click → toggles folder (onclick handler fires before drag starts)
6602        // - Click and drag → drags the item (SortableJS handles the drag)
6603        // The delay helps distinguish between click and drag, but on desktop with delayOnTouchOnly: true,
6604        // the onclick should fire quickly enough before SortableJS intercepts
6605        filter: '.submenu-toggle, .submenu-toggle *, .toggle-icon',
6606        preventOnFilter: true, // Prevent drag when clicking filtered elements (toggle button only)
6607        
6608        // Distinguish between click and drag
6609        delay: 120,
6610        delayOnTouchOnly: true,
6611        
6612        // Empty container settings
6613        emptyInsertThreshold: 10,
6614        
6615        // Add custom ghost class
6616        ghostClass: 'sortable-ghost',
6617        chosenClass: 'sortable-chosen',
6618        dragClass: 'sortable-drag',
6619        
6620        // Make sure we listen to external move events
6621        dragoverBubble: true,
6622        
6623        // Better drag preview stability
6624        scroll: true,
6625        scrollSensitivity: 100,
6626        scrollSpeed: 20,
6627        
6628        // Reduce oscillation by making placeholder movement less sensitive
6629        forceFallback: false, // Use native drag for better performance
6630        fallbackTolerance: 0, // No tolerance - use exact position
6631        
6632        /**
6633         * NESTING LOGIC: Prevent auto-nesting based on hover
6634         * Instead, rely on SortableJS's placeholder position which is accurate
6635         * We'll verify and correct placement in onEnd based on actual DOM position
6636         */
6637        onMove: function(evt, originalEvent) {
6638          const dragged = evt.dragged;
6639          const related = evt.related;
6640          
6641          if (!dragged || !related) return true;
6642          
6643          // Safety check: Ensure items have data-id attributes
6644          // This can be missing if item was just added and DOM not fully updated
6645          if (!dragged.dataset.id || !related.dataset.id) {
6646            console.warn('⚠️ [Drag] Item missing data-id attribute, aborting drag', {
6647              draggedHasId: !!dragged.dataset.id,
6648              relatedHasId: !!related.dataset.id
6649            });
6650            return false;
6651          }
6652          
6653          // Get the IDs to check for circular references
6654          const draggedId = parseInt(dragged.dataset.id);
6655          const relatedId = parseInt(related.dataset.id);
6656          
6657          // Don't allow dropping inside itself or its descendants
6658          if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) {
6659            return false;
6660          }
6661          
6662          const currentTime = Date.now();
6663          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
6664          const draggedParent = draggedItemOriginalContainer || dragged.parentElement;
6665          
6666          // Determine if we're moving inside or outside a folder
6667          // IMPORTANT: Check if related element IS the folder item itself (parent li)
6668          // When targeting first position, SortableJS might use the folder item as related
6669          const isRelatedFolderItem = related.classList && related.classList.contains('has-children');
6670          
6671          // CRITICAL: Handle empty folder case FIRST - before any other logic
6672          // When dragging into an empty folder, related is the folder item itself
6673          // This is essentially the same as targeting first position, but for an empty folder
6674          // We need to allow this immediately, bypassing all debounce and boundary checks
6675          if (isRelatedFolderItem) {
6676            const folderNestedSortable = related.querySelector(':scope > ol.nested-sortable');
6677            const relatedIsNavigableParent = related.classList.contains('navigable-parent-item');
6678            // Navigable parents look like normal links. Only treat them as an
6679            // empty folder when the pointer is actually over the empty list.
6680            let allowEmptyFolderInsert = !!(folderNestedSortable && folderNestedSortable.children.length === 0);
6681            if (allowEmptyFolderInsert && relatedIsNavigableParent && originalEvent) {
6682              const listRect = folderNestedSortable.getBoundingClientRect();
6683              const pointerY = originalEvent.clientY;
6684              allowEmptyFolderInsert = pointerY >= listRect.top - 6 && pointerY <= listRect.bottom + 10;
6685            }
6686            if (allowEmptyFolderInsert) {
6687              // Empty folder detected - this is a drop into empty folder (first position)
6688              // Always allow it, bypass all debounce and boundary logic
6689              lastMoveTime = currentTime;
6690              lastMoveRelatedId = relatedId;
6691              intendedPlacementContainer = folderNestedSortable;
6692              intendedRelatedElement = null; // No related element in empty folder
6693              // Mark as moving inside folder for tracking
6694              lastContainerDecision = 'inside';
6695              lastContainerDecisionTime = currentTime;
6696              
6697              // CRITICAL: Manually move placeholder into empty folder's nested-sortable
6698              // SortableJS might not do this automatically for empty containers
6699              requestAnimationFrame(() => {
6700                const placeholder = document.querySelector('.sortable-ghost');
6701                if (placeholder && placeholder.parentElement !== folderNestedSortable) {
6702                  // Move placeholder into the empty folder
6703                  folderNestedSortable.appendChild(placeholder);
6704                }
6705                
6706                // Update ghost indentation for empty folder
6707                const ghost = document.querySelector('.sortable-ghost');
6708                if (ghost) {
6709                  const ghostContent = ghost.querySelector('.gallery-item-content');
6710                  if (ghostContent) {
6711                    // Calculate depth from parent folder
6712                    const parentFolder = folderNestedSortable.closest('li.has-children');
6713                    if (parentFolder) {
6714                      const targetDepth = computeDepth(parentFolder) + 1;
6715                      ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
6716                    }
6717                  }
6718                }
6719              });
6720              
6721              // Return true immediately to allow the move
6722              return true;
6723            }
6724          }
6725          
6726          let actualRelatedParent = related.parentElement;
6727          
6728          // If related IS the folder item, we need to get its nested sortable container
6729          if (isRelatedFolderItem) {
6730            const nestedSortable = related.querySelector('ol.nested-sortable');
6731            if (nestedSortable) {
6732              actualRelatedParent = nestedSortable;
6733            }
6734          }
6735          
6736          const relatedParent = actualRelatedParent;
6737          
6738          const isMovingInsideFolder = relatedParent && 
6739                                       relatedParent.classList.contains('nested-sortable') && 
6740                                       relatedParent !== rootContainer;
6741          const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer;
6742          const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null);
6743          
6744          // Determine if we're moving within the same folder - this is the key check
6745          // If related IS the folder item, we're still moving within the same folder
6746          let isMovingWithinSameFolder = (isMovingInsideFolder && 
6747                                          draggedParent && 
6748                                          draggedParent.classList.contains('nested-sortable') &&
6749                                          draggedParent !== rootContainer &&
6750                                          relatedParent === draggedParent) ||
6751                                          // OR: if related is the folder item itself and we're dragging from that folder
6752                                          (isRelatedFolderItem && 
6753                                           draggedParent && 
6754                                           draggedParent.classList.contains('nested-sortable') &&
6755                                           draggedParent !== rootContainer &&
6756                                           related.querySelector('ol.nested-sortable') === draggedParent);
6757          
6758          // Special case: If related is the folder item, we're definitely trying to move within that folder
6759          // This happens when targeting first position - SortableJS uses the folder item as related
6760          if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6761            const folderNestedSortable = related.querySelector('ol.nested-sortable');
6762            if (folderNestedSortable === draggedParent) {
6763              // We're dragging within the folder, and related is the folder item
6764              // This means we're targeting first position - FORCE it to be treated as same-folder move
6765              // FORCE same-folder detection - override any previous calculation
6766              isMovingWithinSameFolder = true;
6767            }
6768          }
6769          
6770          // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens
6771          // (Don't log every single move - too noisy)
6772          if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6773            const folderItems = Array.from(draggedParent.children);
6774            const draggedIndex = folderItems.indexOf(dragged);
6775            const relatedIndex = folderItems.indexOf(related);
6776            
6777            // Get placeholder element to see where it actually is
6778            const placeholder = document.querySelector('.sortable-ghost');
6779            const placeholderParent = placeholder ? placeholder.parentElement : null;
6780            const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') 
6781              ? Array.from(placeholderParent.children).indexOf(placeholder) 
6782              : -1;
6783            
6784            const folderItem = draggedParent.closest('li.has-children');
6785            const folderItemId = folderItem ? folderItem.dataset.id : null;
6786            const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null;
6787            const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null;
6788            
6789            // Check if we're trying to target first position
6790            // related could be the first item OR the folder item itself (when SortableJS uses folder as related)
6791            const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder;
6792            const isRelatedFolderItemCheck = related === folderItem;
6793            const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean
6794            
6795            // When targeting first via folder item, placeholder should be at index 0
6796            // But we're seeing it at index 1 - this is the issue!
6797            const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0;
6798            
6799            // Check if placeholder is outside the folder when it should be inside
6800            const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable'));
6801            const shouldBeInside = draggedIndex >= 0; // We're dragging from inside
6802            
6803            // Only log when there's a potential issue or interesting state change
6804            const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0;
6805            const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0;
6806            const jumpingOutToRoot = placeholderIsOutside && shouldBeInside;
6807            
6808            // First position issue: trying to target first but:
6809            // - placeholder is outside OR 
6810            // - placeholder is at index 1 instead of 0 (when targeting first via folder item)
6811            const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition);
6812            
6813            // ALWAYS log when targeting first position via folder item (this is the key case we're debugging)
6814            // OR when placeholder jumps out when it should stay in
6815            // OR when placeholder should be at first position but isn't
6816            const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition;
6817            
6818            if (shouldLog) {
6819              console.error('🔍 [Placeholder Position]', {
6820                draggedId,
6821                draggedIndex,
6822                relatedId,
6823                relatedIndex,
6824                placeholderIndex,
6825                relatedElementTag: related.tagName,
6826                isRelatedFolderItem: isRelatedFolderItemCheck,
6827                isTargetingFirstViaFolderItem,
6828                isMovingWithinSameFolder,
6829                placeholderMismatch,
6830                jumpingOut,
6831                jumpingOutToRoot,
6832                isFirstPosition,
6833                placeholderIsOutside,
6834                placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'),
6835                folderItemId,
6836                firstItemInFolderId,
6837                firstPositionIssue,
6838                shouldBeAtFirstPosition,
6839                expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex
6840              });
6841            }
6842          }
6843          
6844          // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic
6845          // This allows smooth reordering just like items not in folders
6846          if (isMovingWithinSameFolder) {
6847            // Simple debounce only - same as root level items
6848            const timeSinceLastMove = currentTime - lastMoveTime;
6849            // Only prevent very rapid oscillation to same element
6850            if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) {
6851              return false;
6852            }
6853            // Allow all moves within same folder - update tracking and continue
6854            
6855            // SPECIAL FIX: When targeting first position via folder item, fix placeholder position
6856            // SortableJS places it at index 1 when related is the folder item, but we want it at index 0
6857            // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive
6858            if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6859              const folderItems = Array.from(draggedParent.children);
6860              const firstItem = folderItems.length > 0 ? folderItems[0] : null;
6861              
6862              // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up)
6863              // This prevents the fix from being too aggressive when moving to other positions
6864              const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions
6865              
6866              if (firstItem && firstItem !== dragged && isNearFirstPosition) {
6867                // Fix immediately (synchronously) first - don't wait for next frame
6868                const placeholder = document.querySelector('.sortable-ghost');
6869                if (placeholder && placeholder.parentElement === draggedParent) {
6870                  const currentIndex = Array.from(draggedParent.children).indexOf(placeholder);
6871                  // Only fix if placeholder is at index 1 when we're targeting first position
6872                  // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3)
6873                  if (currentIndex === 1 && draggedIndex <= 1) {
6874                    draggedParent.insertBefore(placeholder, firstItem);
6875                  }
6876                }
6877              } else {
6878                // Not near first position - stop any active fix loop
6879                firstPositionFixLoopActive = false;
6880              }
6881            } else {
6882              // Not targeting first position anymore - reset flag
6883              firstPositionFixLoopActive = false;
6884            }
6885            
6886            lastMoveTime = currentTime;
6887            lastMoveRelatedId = relatedId;
6888            // Skip all the boundary case logic below - just continue to allow the move
6889            // Fall through to update intendedPlacementContainer and return true
6890          } else {
6891            // Only apply complex boundary logic for moves BETWEEN folders or to/from root
6892            
6893            // Hysteresis: Prevent rapid switching between inside/outside folder
6894            const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : [];
6895            const isFirstPositionInFolder = isMovingInsideFolder && 
6896                                           targetFolderItems.length > 0 && 
6897                                           related === targetFolderItems[0];
6898            // Special case: Empty folder - allow easy dropping into it
6899            const isEmptyFolder = isMovingInsideFolder && targetFolderItems.length === 0;
6900            
6901            if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) {
6902              const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime;
6903              
6904              // Make first position easier to target from outside
6905              if (isFirstPositionInFolder) {
6906                // Moving to first position from outside - use shorter debounce
6907                const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3;
6908                if (timeSinceContainerSwitch < effectiveDebounce) {
6909                  // Only block if it's very rapid (oscillation)
6910                  if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) {
6911                    return false;
6912                  }
6913                }
6914              } else if (isEmptyFolder) {
6915                // Empty folder - allow easy dropping, minimal debounce
6916                // Only block very rapid oscillation
6917                if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS * 0.2) {
6918                  return false;
6919                }
6920              } else {
6921                // Normal container switch - apply full hysteresis
6922                if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) {
6923                  return false;
6924                }
6925              }
6926            }
6927          }
6928          
6929          // Special handling for boundary cases (last item in folder, or dragging into folder)
6930          // BUT: Skip all this logic if we're moving within the same folder (already handled above)
6931          if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6932            const folderItem = draggedParent.closest('li.has-children');
6933            const currentFolderItems = Array.from(draggedParent.children);
6934            const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && 
6935                                                   currentFolderItems[0] !== dragged &&
6936                                                   (related === currentFolderItems[0] || 
6937                                                    (isMovingInsideFolder && relatedParent === draggedParent && 
6938                                                     related === currentFolderItems[0]));
6939            
6940            // Case 0: This case is now handled earlier for same-folder moves
6941            // Only handle cross-folder moves here
6942            
6943            // Case 1: Dragging last item out of folder
6944            if (isLastItemInFolder && isMovingOutsideFolder) {
6945              const timeSinceLastMove = currentTime - lastMoveTime;
6946              
6947              // BUT: If we're trying to move to first position, don't block it
6948              // Check if the related element is the folder itself or right after it
6949              if (folderItem) {
6950                const folderNextSibling = folderItem.nextElementSibling;
6951                // If we're moving to position right after folder, it might be trying to go to first position
6952                // Allow it if it's been a reasonable time
6953                if (folderNextSibling && related === folderNextSibling) {
6954                  // This is ambiguous - could be first position or outside
6955                  // Require more time but not as much as pure outside move
6956                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
6957                    if (relatedId === lastMoveRelatedId) {
6958                      return false;
6959                    }
6960                  }
6961                } else {
6962                  // Definitely moving outside - require longer debounce
6963                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
6964                    if (relatedId === lastMoveRelatedId) {
6965                      return false; // Reject if oscillating
6966                    }
6967                  }
6968                }
6969              } else {
6970                // No folder item found - apply standard debounce
6971                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
6972                  if (relatedId === lastMoveRelatedId) {
6973                    return false;
6974                  }
6975                }
6976              }
6977            }
6978            
6979            // Case 2: Dragging item into folder from outside
6980            if (isMovingInsideFolder && relatedParent !== draggedParent) {
6981              const targetFolderItems = Array.from(relatedParent.children);
6982              const isEmptyFolder = targetFolderItems.length === 0;
6983              const isNearBottom = targetFolderItems.length > 0 && 
6984                                   (related === targetFolderItems[targetFolderItems.length - 1] ||
6985                                    targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
6986              const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0];
6987              
6988              // Special handling for empty folder - allow easy dropping
6989              if (isEmptyFolder) {
6990                const timeSinceLastMove = currentTime - lastMoveTime;
6991                // Very minimal debounce for empty folder - only prevent rapid oscillation
6992                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2 && relatedId === lastMoveRelatedId) {
6993                  return false;
6994                }
6995                // Allow move into empty folder - don't block it
6996              } else if (isFirstPos) {
6997                // Moving to first position - make it easy
6998                const timeSinceLastMove = currentTime - lastMoveTime;
6999                // Very short debounce for first position
7000                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) {
7001                  return false;
7002                }
7003                // Allow move to first position
7004              } else if (isNearBottom) {
7005                // For bottom position, still use some debounce to prevent oscillation
7006                const timeSinceLastMove = currentTime - lastMoveTime;
7007                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) {
7008                  if (relatedId === lastMoveRelatedId) {
7009                    return false;
7010                  }
7011                }
7012              }
7013            }
7014            
7015          // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within
7016          // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last
7017          if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) {
7018            // We're dragging from within a folder but detected as moving outside
7019            // Check if the related element is adjacent to the folder (which might indicate targeting first/last)
7020            const folderIndex = Array.from(rootContainer.children).indexOf(folderItem);
7021            const relatedIndex = Array.from(rootContainer.children).indexOf(related);
7022            
7023            // If related is immediately before or after folder, might be targeting first/last
7024            if (folderIndex >= 0 && relatedIndex >= 0) {
7025              const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1);
7026              
7027              if (isAdjacentToFolder) {
7028                // Might be targeting first/last position - be lenient
7029                const timeSinceLastMove = currentTime - lastMoveTime;
7030                // Only block if it's very rapid oscillation
7031                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) {
7032                  return false;
7033                }
7034                // Otherwise allow - user might be trying to target first/last position
7035                // Don't block based on this check
7036              }
7037            }
7038          }
7039          }
7040          
7041          // Case 3: Dragging from root into folder
7042          if (draggedParent === rootContainer && isMovingInsideFolder) {
7043            const targetFolderItems = Array.from(relatedParent.children);
7044            const isEmptyFolder = targetFolderItems.length === 0;
7045            const isNearBottom = targetFolderItems.length > 0 && 
7046                                 (related === targetFolderItems[targetFolderItems.length - 1] ||
7047                                  targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
7048            const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0];
7049            
7050            // Special handling for empty folder - allow easy dropping
7051            if (isEmptyFolder) {
7052              const timeSinceLastMove = currentTime - lastMoveTime;
7053              // Very minimal debounce for empty folder - only prevent rapid oscillation
7054              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2) {
7055                if (relatedId === lastMoveRelatedId) {
7056                  return false;
7057                }
7058              }
7059              // Allow move into empty folder - don't block it
7060            } else if (isFirstPosition) {
7061              // Special handling for first position - make it easier to target
7062              const timeSinceLastMove = currentTime - lastMoveTime;
7063              // Make first position easier - only prevent very rapid oscillation
7064              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) {
7065                if (relatedId === lastMoveRelatedId) {
7066                  return false;
7067                }
7068              }
7069              // Allow move to first position - don't block it
7070              // Continue to track but don't apply strict debouncing
7071            } else if (isNearBottom) {
7072              // Keep strict debouncing for bottom position
7073              const timeSinceLastMove = currentTime - lastMoveTime;
7074              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
7075                if (relatedId === lastMoveRelatedId) {
7076                  return false;
7077                }
7078              }
7079            }
7080          }
7081          
7082          // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position)
7083          // If we're at first position and trying to move outside, require deliberate movement
7084          if (isMovingInsideFolder) {
7085            const currentFolderItems = Array.from(relatedParent.children);
7086            const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0];
7087            
7088            // If we're moving FROM first position to outside (reverse hysteresis)
7089            if (isFirstPos && lastContainerDecision === 'outside') {
7090              const timeSinceLastMove = currentTime - lastMoveTime;
7091              // If we just moved outside, require more time before allowing back to first position
7092              // BUT: if it's been a reasonable time, allow it (don't make it too hard)
7093              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) {
7094                // Very rapid - might be oscillation
7095                return false;
7096              }
7097            }
7098          }
7099          
7100          // Case 5: Prevent oscillation between first and second position within folder
7101          // BUT: Only apply if NOT moving within same folder (to allow smooth reordering)
7102          if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) {
7103            const folderItems = Array.from(relatedParent.children);
7104            const isFirstPos = folderItems.length > 0 && related === folderItems[0];
7105            const isSecondPos = folderItems.length > 1 && related === folderItems[1];
7106            
7107            // If moving from first to second (or vice versa) very rapidly, prevent oscillation
7108            if ((isFirstPos || isSecondPos) && lastMoveRelatedId) {
7109              const lastRelated = document.querySelector('#galleryTree li[data-id="' + lastMoveRelatedId + '"]') || document.querySelector('li[data-id="' + lastMoveRelatedId + '"]');
7110              if (lastRelated && lastRelated.parentElement === relatedParent) {
7111                const lastRelatedIndex = folderItems.indexOf(lastRelated);
7112                const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0);
7113                
7114                if (isOscillating) {
7115                  const timeSinceLastMove = currentTime - lastMoveTime;
7116                  // Allow moving to first position easily, but prevent rapid oscillation between first/second
7117                  if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) {
7118                    return false; // Prevent jumping from first to second too easily
7119                  }
7120                  // But allow first position more easily
7121                  if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) {
7122                    return false; // Only prevent very rapid oscillation
7123                  }
7124                }
7125              }
7126            }
7127          }
7128          
7129          // Prevent rapid oscillation when dragging within same container
7130          // NOTE: For same-folder moves, we already updated tracking above, so skip this 
7130section
7131          if (!isMovingWithinSameFolder) {
7132            const timeSinceLastMove = currentTime - lastMoveTime;
7133            
7134            // If moving to the same related element very quickly, reject to prevent oscillation
7135            if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) {
7136              return false;
7137            }
7138            
7139            // Update tracking variables (only for non-same-folder moves)
7140            lastMoveTime = currentTime;
7141            lastMoveRelatedId = relatedId;
7142          }
7143          // For same-folder moves, tracking was already updated in the earlier block
7144          
7145          // Update container decision tracking
7146          if (currentDecision) {
7147            if (currentDecision !== lastContainerDecision) {
7148              lastContainerDecisionTime = currentTime;
7149            }
7150            lastContainerDecision = currentDecision;
7151          }
7152          
7153          // Track where the placeholder indicates the item should be placed
7154          // The related element is next to where the placeholder will be positioned
7155          // Check if it's at root level or nested level
7156          if (related) {
7157            intendedRelatedElement = related;
7158            const relatedParent = related.parentElement;
7159            const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7160            
7161            // Determine if the placeholder is at root level or nested level
7162            if (relatedParent === rootContainer) {
7163              // Placeholder is at root level (not nested)
7164              intendedPlacementContainer = rootContainer;
7165            } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) {
7166              // Placeholder is in a nested list (inside a folder)
7167              intendedPlacementContainer = relatedParent;
7168            }
7169            
7170            // Update ghost element indentation to match the target position
7171            requestAnimationFrame(() => {
7172              const ghost = document.querySelector('.sortable-ghost');
7173              if (ghost && intendedPlacementContainer) {
7174                const ghostContent = ghost.querySelector('.gallery-item-content');
7175                if (ghostContent) {
7176                  // Calculate the depth where the ghost will be placed
7177                  // Use the related element to determine the depth (if it exists and is in the target container)
7178                  let targetDepth = 0;
7179                  if (intendedPlacementContainer === rootContainer) {
7180                    targetDepth = 0; // Root level
7181                  } else if (related && related.parentElement === intendedPlacementContainer) {
7182                    // Use the related element's depth to determine target depth
7183                    targetDepth = computeDepth(related);
7184                  } else {
7185                    // Fallback: find first child in the container or calculate from parent folder
7186                    const firstChild = intendedPlacementContainer.querySelector('li');
7187                    if (firstChild) {
7188                      targetDepth = computeDepth(firstChild);
7189                    } else {
7190                      // Empty container (like empty folder) - calculate from parent folder
7191                      const parentFolder = intendedPlacementContainer.closest('li.has-children');
7192                      if (parentFolder) {
7193                        targetDepth = computeDepth(parentFolder) + 1;
7194                      }
7195                    }
7196                  }
7197                  
7198                  // Special handling: If intendedPlacementContainer is empty and placeholder isn't in it yet,
7199                  // ensure placeholder is moved there and indentation is correct
7200                  if (intendedPlacementContainer.children.length === 0) {
7201                    const placeholder = document.querySelector('.sortable-ghost');
7202                    if (placeholder && placeholder.parentElement !== intendedPlacementContainer) {
7203                      // Move placeholder into empty container
7204                      intendedPlacementContainer.appendChild(placeholder);
7205                    }
7206                  }
7207                  ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
7208                }
7209              }
7210            });
7211          }
7212          
7213          // Let SortableJS handle the placement based on placeholder position
7214          // We'll verify and correct if needed in onEnd
7215          return true; // Allow the move
7216        },
7217        
7218        // Capture indentation before drag starts (fires when item is chosen)
7219        onChoose: function(evt) {
7220          if (!evt.item) return;
7221          
7222          const draggedItem = evt.item;
7223          const content = draggedItem.querySelector('.gallery-item-content');
7224          
7225          // Store the original padding-left for the ghost
7226          if (content) {
7227            const computedStyle = window.getComputedStyle(content);
7228            const originalPaddingLeft = computedStyle.paddingLeft;
7229            draggedItem.dataset.originalPaddingLeft = originalPaddingLeft;
7230          }
7231        },
7232        
7233        // Visual feedback on drag start
7234        onStart: function(evt) {
7235          if (!evt.item) return;
7236          
7237          // Don't set dimensions on the dragged item - let it maintain natural size
7238          // Setting width/height can cause layout shifts and menu expansion
7239          const draggedItem = evt.item;
7240          
7241          // Store original container and check if it's the last item in folder
7242          draggedItemOriginalContainer = draggedItem.parentElement;
7243          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7244          if (draggedItemOriginalContainer && 
7245              draggedItemOriginalContainer.classList.contains('nested-sortable') && 
7246              draggedItemOriginalContainer !== rootContainer) {
7247            const siblings = Array.from(draggedItemOriginalContainer.children);
7248            isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem;
7249          } else {
7250            isLastItemInFolder = false;
7251          }
7252          
7253          // Apply the stored padding-left to the ghost element
7254          // The ghost is created by SortableJS at this point
7255          requestAnimationFrame(() => {
7256            const ghost = document.querySelector('.sortable-ghost');
7257            if (ghost) {
7258              const ghostContent = ghost.querySelector('.gallery-item-content');
7259              const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft;
7260              if (ghostContent && originalPaddingLeft) {
7261                ghostContent.style.paddingLeft = originalPaddingLeft;
7262              }
7263            }
7264          });
7265          
7266          // Reset oscillation tracking variables
7267          lastMoveTime = 0;
7268          lastMoveRelatedId = null;
7269          lastContainerDecision = null;
7270          lastContainerDecisionTime = 0;
7271          
7272          // Create a backup
7273          try {
7274            window._dragStartElementIds = [];
7275            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7276              if (li.dataset.id) {
7277                window._dragStartElementIds.push(li.dataset.id);
7278              }
7279            });
7280            
7281            galleriesBackup = JSON.parse(JSON.stringify(galleries));
7282          } catch (error) {
7283            console.error('Error creating backup:', error);
7284          }
7285          
7286          // Add indicator classes
7287          document.body.classList.add('menu-item-dragging');
7288          
7289          // Add hover indicator class to submenu items
7290          document.querySelectorAll('.has-children').forEach(item => {
7291            item.classList.add('potential-parent');
7292          });
7293          
7294          // Prevent layout recalculation during drag
7295          document.body.style.setProperty('--is-dragging', '1');
7296        },
7297        
7298        // Cleanup on end
7299        onEnd: function(evt) {
7300          // Verify and correct placement based on placeholder's intended position
7301          // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level
7302          const dragged = evt.item;
7303          const previousParentLi = evt.from ? evt.from.closest('li') : null;
7304          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7305          
7306          if (!dragged || !rootContainer) {
7307            intendedPlacementContainer = null;
7308            intendedRelatedElement = null;
7309            // Restore normal sizing
7310            dragged.style.width = '';
7311            dragged.style.height = '';
7312            dragged.style.minHeight = '';
7313            document.body.style.removeProperty('--is-dragging');
7314            return;
7315          }
7316          
7317          // Get the actual parent container where SortableJS placed the item
7318          const actualParentList = dragged.parentElement;
7319          
7320          // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing
7321          requestAnimationFrame(() => {
7322            dragged.style.width = '';
7323            dragged.style.height = '';
7324            dragged.style.minHeight = '';
7325            document.body.style.removeProperty('--is-dragging');
7326          });
7327          
7328          if (intendedPlacementContainer && !document.body.contains(intendedPlacementContainer)) {
7329            intendedPlacementContainer = null;
7330            intendedRelatedElement = null;
7331          }
7332
7333          // Check if the intended placement (where placeholder showed) differs from actual placement
7334          // This happens when SortableJS auto-nests based on mouse position over a folder
7335          if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) {
7336            // The item ended up in a different container than the placeholder indicated
7337            // Move it to where the placeholder showed it should be (next to intendedRelatedElement)
7338            if (intendedPlacementContainer === rootContainer) {
7339              // Placeholder was at root level - move item to root, positioned next to related element
7340              if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) {
7341                // Insert before the related element (where placeholder was)
7342                rootContainer.insertBefore(dragged, intendedRelatedElement);
7343                console.log("Corrected placement: moved from folder to root level where placeholder indicated");
7344              } else if (intendedRelatedElement) {
7345                // Related element exists but not at root - find its folder and insert before it at root
7346                const folderItem = intendedRelatedElement.closest('li.has-children');
7347                if (folderItem && folderItem.parentElement === rootContainer) {
7348                  rootContainer.insertBefore(dragged, folderItem);
7349                  console.log("Corrected placement: moved to root level above folder where placeholder indicated");
7350                } else {
7351                  rootContainer.appendChild(dragged);
7352                }
7353              } else {
7354                rootContainer.appendChild(dragged);
7355              }
7356              // Update indentation since it's now at root
7357              applyIndentationFromDepth(dragged);
7358            } else {
7359              // Placeholder was in a nested list - move it there
7360              if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) {
7361                // Insert before the related element in the nested list
7362                intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement);
7363              } else {
7364                intendedPlacementContainer.appendChild(dragged);
7365              }
7366              console.log("Corrected placement: moved to nested list where placeholder indicated");
7367              // Update indentation for nested item
7368              applyIndentationFromDepth(dragged);
7369              
7370              // Ensure parent folder has proper structure
7371              const folderItem = intendedPlacementContainer.closest('li.has-children');
7372              if (folderItem) {
7373                const nestedList = folderItem.querySelector('ol.nested-sortable');
7374                if (nestedList && intendedPlacementContainer !== nestedList) {
7375                  nestedList.appendChild(dragged);
7376                }
7377                
7378                const parentDepth = computeDepth(folderItem);
7379                const nestedDepth = parentDepth + 1;
7380                
7381                if (nestedList) {
7382                  nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7383                }
7384                
7385                folderItem.classList.add('has-children');
7386              }
7387            }
7388          } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) {
7389            // Item is correctly in a nested list - ensure proper structure
7390            const folderItem = actualParentList.closest('li.has-children');
7391            if (folderItem) {
7392              const nestedList = folderItem.querySelector('ol.nested-sortable');
7393              if (nestedList && actualParentList !== nestedList) {
7394                nestedList.appendChild(dragged);
7395              }
7396              
7397              const parentDepth = computeDepth(folderItem);
7398              const nestedDepth = parentDepth + 1;
7399              
7400              if (nestedList) {
7401                nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7402              }
7403              
7404              folderItem.classList.add('has-children');
7405            }
7406            applyIndentationFromDepth(dragged);
7407          } else {
7408            // Item is correctly at root level
7409            applyIndentationFromDepth(dragged);
7410          }
7411
7412          // Reset tracking variables
7413          intendedPlacementContainer = null;
7414          intendedRelatedElement = null;
7415          isLastItemInFolder = false;
7416          draggedItemOriginalContainer = null;
7417          lastContainerDecision = null;
7418          lastContainerDecisionTime = 0;
7419
7420          // Apply indentation updates for ALL affected items after drag
7421          // Use requestAnimationFrame to batch DOM updates and prevent visual jumps
7422          requestAnimationFrame(() => {
7423            if (dragged) {
7424              // Update the dragged item and all its descendants
7425              applyIndentationFromDepth(dragged);
7426              
7427              // Update all items in the new container (siblings of dragged item)
7428              const newContainer = dragged.parentElement;
7429              if (newContainer && newContainer.classList.contains('nested-sortable')) {
7430                const siblings = Array.from(newContainer.children);
7431                siblings.forEach(sibling => {
7432                  if (sibling !== dragged && sibling.tagName === 'LI') {
7433                    applyIndentationFromDepth(sibling);
7434                  }
7435                });
7436              }
7437              
7438              // Update all items in the old container (if it still exists and has items)
7439              if (evt.from && evt.from.classList.contains('nested-sortable')) {
7440                const oldSiblings = Array.from(evt.from.children);
7441                oldSiblings.forEach(sibling => {
7442                  if (sibling.tagName === 'LI') {
7443                    applyIndentationFromDepth(sibling);
7444                  }
7445                });
7446              }
7447              
7448              // Also update all items in the entire tree to ensure consistency
7449              // This handles edge cases where indentation might be off
7450              const allItems = document.querySelectorAll('#galleryTree .nested-sortable li');
7451              allItems.forEach(li => {
7452                const depth = computeDepth(li);
7453                li.setAttribute('data-nesting-level', depth);
7454                const content = li.querySelector('.gallery-item-content');
7455                if (content) {
7456                  content.style.paddingLeft = (depth * 16) + 'px';
7457                }
7458              });
7459            }
7460          });
7461
7462          // Clean up previous parent if it lost its last child
7463          if (previousParentLi) {
7464            cleanupParentIfEmpty(previousParentLi);
7465          }
7466          
7467          // Remove all indicator classes
7468          document.body.classList.remove('menu-item-dragging');
7469          document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => {
7470            el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested');
7471          });
7472          
7473          // Standard cleanup for ghost elements
7474          document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => {
7475            item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag');
7476          });
7477          
7478          // Check if drop was outside a valid container
7479          if (!evt.to || !evt.to.classList.contains('nested-sortable')) {
7480            console.warn('Item was dropped outside a valid container - reverting');
7481            restoreFromBackup();
7482            return;
7483          }
7484          
7485          // Verify no elements were lost
7486          try {
7487            // Check DOM elements
7488            const endElementIds = [];
7489            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7490              if (li.dataset.id) {
7491                endElementIds.push(li.dataset.id);
7492              }
7493            });
7494            
7495            if (window._dragStartElementIds && window._dragStartElementIds.length > 0) {
7496              const startCount = window._dragStartElementIds.length;
7497              const endCount = endElementIds.length;
7498              
7499              if (endCount < startCount) {
7500                console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`);
7501                restoreFromBackup();
7502                return;
7503              }
7504            }
7505            
7506            // Update the data structure
7507            updateGalleryStructure();
7508            
7509            // Verify data model
7510            const originalCount = countGalleries(galleriesBackup);
7511            const newCount = countGalleries(galleries);
7512            
7513            if (newCount < originalCount) {
7514              console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`);
7515              restoreFromBackup();
7516              return;
7517            }
7518            
7519            // updateGalleryStructure already persists; do not save or re-render again
7520            // here. A second save/render can rebind Sortable and snap the drop back.
7521          } catch (error) {
7522            console.error('Error updating gallery structure:', error);
7523            restoreFromBackup();
7524          }
7525        }
7526      });
7527      
7528      sortableInstances.push(sortable);
7529    } catch (error) {
7530      console.error(`Error initializing sortable for container ${i}:`, error);
7531    }
7532  }
7533
7534  // Add event delegation for folder toggles (will be added by setupFolderToggleHandler)
7535  setupFolderToggleHandler();
7536
7537  return sortableInstances;
7538}
7539
7540// Helper function to set up folder toggle event delegation
7541// This must be called after galleries are rendered, in both view and edit modes
7542function setupFolderToggleHandler() {
7543  const galleryTree = document.getElementById('galleryTree');
7544  if (galleryTree && !galleryTree.dataset.folderToggleHandlerAdded) {
7545    galleryTree.addEventListener('click', function(e) {
7546      
7547      const target = e.target;
7548      // Resolve the clicked row first — do not use target.closest('.menu-item.folder'),
7549      // which matches ancestor folder labels when clicking nested pages/galleries.
7550      const listItem = target.closest('li[data-id]');
7551      if (!listItem) {
7552        return;
7553      }
7554
7555      const contentDiv = listItem.querySelector(':scope > .gallery-item-content');
7556      if (!contentDiv) {
7557        return;
7558      }
7559
7560      const toggleButton = contentDiv.querySelector('.submenu-toggle');
7561      const folderText = contentDiv.querySelector('.menu-item.folder');
7562      const isToggleClick = toggleButton && toggleButton.contains(target);
7563      const isFolderTextClick = folderText && folderText.contains(target);
7564      
7565      
7566      if (isToggleClick || isFolderTextClick) {
7567        const folderId = parseInt(listItem.dataset.id, 10);
7568        
7569        if (folderId && !isNaN(folderId)) {
7570          // Prevent SortableJS from intercepting this click
7571          e.stopPropagation();
7572          e.stopImmediatePropagation();
7573          toggleFolder(folderId, e);
7574        }
7575      }
7576    }, true); // Use capture phase to run before SortableJS
7577    
7578    galleryTree.dataset.folderToggleHandlerAdded = 'true';
7579  }
7580}
7581
7582// NEW HELPER FUNCTION: Gets the nesting level of an element
7583function getElementNestingLevel(element) {
7584  let level = 0;
7585  let current = element;
7586  
7587  // Count how many nested-sortable parents this element has
7588  while (current && current.parentElement) {
7589    if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) {
7590      level++;
7591    }
7592    current = current.parentElement;
7593  }
7594  
7595  return level;
7596}
7597
7598// NEW HELPER FUNCTION: Determines if an item would become nested
7599function wouldBeNestedItem(draggedEl, relatedEl, event) {
7600  if (!relatedEl || !draggedEl) return false;
7601  
7602  // Get mouse position
7603  const mouseX = event.clientX;
7604  
7605  // Get the bounding rect of the related element
7606  const rect = relatedEl.getBoundingClientRect();
7607  
7608  // Determine the nesting threshold - how far from the left edge
7609  // the mouse needs to be to consider it a nesting operation
7610  const nestingThreshold = 25; // pixels from left edge
7611  
7612  // Check if mouse is within the nesting threshold from the left edge
7613  const distanceFromLeft = mouseX - rect.left;
7614  
7615  // Check if we should nest (mouse is within threshold of left edge)
7616  const wouldNest = distanceFromLeft <= nestingThreshold;
7617  
7618  // Log the calculation for debugging
7619  console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`);
7620  
7621  return wouldNest;
7622}
7623
7624/**
7625 * Helper function to find gallery by ID in a tree structure
7626 * @param {Array} galleries - The gallery array to search
7627 * @param {number} id - The ID to find
7628 * @returns {Object|null} - The found gallery or null
7629 */
7630function findGalleryById(galleries, id) {
7631  if (window.HelpSystem && typeof window.HelpSystem.isDemoId === 'function' && window.HelpSystem.isDemoId(id)) {
7632    return window.HelpSystem.getDemoRecord(id);
7633  }
7634  if (!Array.isArray(galleries)) return null;
7635  
7636  // First check at the current level
7637  const directMatch = galleries.find(g => g.id == id);
7638  if (directMatch) return directMatch;
7639  
7640  // Then check in children
7641  for (const gallery of galleries) {
7642    if (gallery.children && gallery.children.length > 0) {
7643      const childMatch = findGalleryById(gallery.children, id);
7644      if (childMatch) return childMatch;
7645    }
7646  }
7647  
7648  return null;
7649}
7650
7651/** Detect page menu items even when isPage flag was not persisted */
7652function isPageMenuItem(item) {
7653  if (!item) return false;
7654  if (item.isPage === true) return true;
7655  if (item.pageId && String(item.pageId).indexOf('page_') === 0) return true;
7656  if (Array.isArray(item.pageElements) && item.pageElements.length > 0) return true;
7657  return false;
7658}
7659
7660/**
7661 * Return true only for pages/galleries that explicitly opted into navigable-parent behavior.
7662 * Never infer this from children or parentId; legacy items must retain their existing behavior.
7663 */
7664function isNavigableMenuParent(item) {
7665  if (!item || item.actsAsMenuParent !== true) return false;
7666  if (item.isFolder === true || item.isSubmenu === true || item.isSpacer === true || item.isExternal === true) {
7667    return false;
7668  }
7669  return isPageMenuItem(item) || item.isIntegrated === true;
7670}
7671
7672/**
7673 * Get all gallery IDs from a tree structure
7674 * @param {Array} galleries - Array of galleries
7675 * @returns {Array} - Flat array of all IDs
7676 */
7677function getAllGalleryIds(galleries) {
7678  if (!Array.isArray(galleries)) return [];
7679  
7680  let ids = [];
7681  
7682  galleries.forEach(gallery => {
7683    if (gallery && gallery.id) {
7684      ids.push(gallery.id);
7685    }
7686    
7687    // Add child IDs if any
7688    if (gallery.children && Array.isArray(gallery.children)) {
7689      ids = ids.concat(getAllGalleryIds(gallery.children));
7690    }
7691  });
7692  
7693  return ids;
7694}
7695
7696/**
7697 * Enhanced count function that handles edge cases
7698 * @param {Array} galleryArray - The array to count
7699 * @returns {number} - The total count of galleries
7700 */
7701function countGalleries(galleryArray) {
7702  if (!Array.isArray(galleryArray)) return 0;
7703
7704  // Menu items are stored flat and also get a derived children[] at render time.
7705  // Counting children recursively double-counts those items and falsely triggers
7706  // restoreFromBackup after a successful drop.
7707  const ids = new Set();
7708  function walk(items) {
7709    if (!Array.isArray(items)) return;
7710    items.forEach(function(gallery) {
7711      if (!gallery) return;
7712      if (gallery.id != null) ids.add(gallery.id);
7713      if (gallery.children && Array.isArray(gallery.children)) walk(gallery.children);
7714    });
7715  }
7716  walk(galleryArray);
7717  return ids.size;
7718}
7719
7720/**
7721 * Enhanced backup restoration function with better debugging and notification
7722 */
7723function restoreFromBackup() {
7724  console.warn('Restoring galleries from backup due to invalid operation');
7725  
7726  // Verify we have a valid backup
7727  if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) {
7728    console.error('No valid backup available for restoration');
7729    
7730    // Create a backup from the current DOM as a last resort
7731    try {
7732      console.log('Attempting to rebuild from DOM structure...');
7733      rebuildGalleriesFromDOM();
7734      return;
7735    } catch (rebuildError) {
7736      console.error('Failed to rebuild from DOM:', rebuildError);
7737      // Continue with normal user notification
7738    }
7739  } else {
7740    // Restore from backup
7741    galleries = JSON.parse(JSON.stringify(galleriesBackup));
7742  }
7743  
7744  // Re-render the tree with the restored data
7745  renderGalleries();
7746  
7747  // Force reinitialize nested sortables after restoration
7748  setTimeout(() => {
7749    initializeNestedSortables();
7750  }, 300);
7751}
7752
7753/**
7754 * Emergency recovery function that tries to rebuild galleries from DOM
7755 * This is a last resort when backup restoration fails
7756 */
7757function rebuildGalleriesFromDOM() {
7758  console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure');
7759  
7760  // Create a new galleries array
7761  const rebuiltGalleries = [];
7762  
7763  // This will track items we've already processed
7764  const processedIds = new Set();
7765  
7766  // Function to process a container and its children
7767  function processContainer(container, parentId = null) {
7768    // Get all immediate list items
7769    const items = container.querySelectorAll(':scope > li');
7770    
7771    items.forEach(item => {
7772      // Get item ID from data attribute
7773      const id = parseInt(item.dataset.id);
7774      if (isNaN(id)) return; // Skip if no valid ID
7775      
7776      // Skip already processed items to avoid duplicates
7777      if (processedIds.has(id)) return;
7778      processedIds.add(id);
7779      
7780      // Try to find the original gallery data
7781      let galleryData = null;
7782      
7783      // First check current galleries array
7784      const existingGallery = findGalleryById(galleries, id);
7785      if (existingGallery) {
7786        galleryData = { ...existingGallery };
7787        delete galleryData.children; // We'll rebuild the hierarchy
7788      }
7789      // If not found, check if we have a backup
7790      else if (galleriesBackup && Array.isArray(galleriesBackup)) {
7791        const backupGallery = findGalleryById(galleriesBackup, id);
7792        if (backupGallery) {
7793          galleryData = { ...backupGallery };
7794          delete galleryData.children; // We'll rebuild the hierarchy
7795        }
7796      }
7797      
7798      // If we still don't have data, create minimal placeholder
7799      if (!galleryData) {
7800        // Try to extract title from DOM
7801        const titleElement = item.querySelector('.menu-item');
7802        const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`;
7803        
7804        galleryData = {
7805          id: id,
7806          title: title,
7807          visible: !item.classList.contains('hidden-gallery')
7808        };
7809      }
7810      
7811      // Update parent ID to match DOM structure
7812      galleryData.parentId = parentId;
7813      
7814      // Add to rebuilt galleries
7815      rebuiltGalleries.push(galleryData);
7816      
7817      // Process children if any
7818      const childContainer = item.querySelector('ol.nested-sortable');
7819      if (childContainer) {
7820        processContainer(childContainer, id);
7821      }
7822    });
7823  }
7824  
7825  // Start with root container
7826  const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7827  if (rootContainer) {
7828    processContainer(rootContainer);
7829    
7830    // Replace global galleries array with our rebuilt one
7831    galleries = rebuiltGalleries;
7832    
7833    // Re-render with the rebuilt data
7834    renderGalleries();
7835    
7836    console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`);
7837    return true;
7838  } else {
7839    console.error('RECOVERY FAILED: Root container not found');
7840    return false;
7841  }
7842}
7843
7844// Make sure these functions are available to the global scope
7845window.initializeNestedSortables = initializeNestedSortables;
7846window.restoreFromBackup = restoreFromBackup;
7847window.getElementNestingLevel = getElementNestingLevel;
7848window.wouldBeNestedItem = wouldBeNestedItem;
7849
7850
7851
7852function hasDescendant(itemId, possibleDescendantId) {
7853// Check if possibleDescendantId is a descendant of itemId
7854const children = galleries.filter(g => g.parentId === itemId);
7855
7856if (children.some(child => child.id === possibleDescendantId)) {
7857return true;
7858}
7859
7860return children.some(child => hasDescendant(child.id, possibleDescendantId));
7861}
7862
7863function updateGalleryStructure() {
7864// Create a new array to hold the updated gallery structure
7865const newGalleries = [];
7866
7867// Function to recursively process nested lists
7868function processNestedList(container, parentId = null) {
7869if (!container || !container.children) {
7870  console.warn('Invalid container in processNestedList', container);
7871  return;
7872}
7873
7874const items = container.children;
7875
7876Array.from(items).forEach(item => {
7877  if (item.tagName !== 'LI') return; // Skip non-list items
7878  
7879  const id = parseInt(item.dataset.id);
7880  if (isNaN(id)) {
7881    console.warn('Invalid ID in list item:', item);
7882    return;
7883  }
7884  
7885  const gallery = galleries.find(g => g.id === id);
7886  
7887  if (gallery) {
7888    // Create a new gallery object without children
7889    const newGallery = { ...gallery };
7890    if (newGallery.children) delete newGallery.children;
7891    
7892    // Update parent ID
7893    newGallery.parentId = parentId;
7894    
7895    // Add to new galleries array
7896    newGalleries.push(newGallery);
7897    
7898    // Process any nested OL with class 'nested-sortable' within this item
7899    const nestedList = item.querySelector('ol.nested-sortable');
7900    if (nestedList) {
7901      processNestedList(nestedList, id);
7902    }
7903  } else {
7904    console.warn(`Gallery with ID ${id} not found in galleries array`);
7905  }
7906});
7907}
7908
7909// Start processing from the root list
7910const rootList = document.querySelector('#galleryTree > ol.nested-sortable');
7911if (rootList) {
7912try {
7913  processNestedList(rootList);
7914  
7915  // Verify that we haven't lost any galleries
7916  if (newGalleries.length < galleries.length) {
7917    console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`);
7918    
7919    // Find missing galleries
7920    const originalIds = galleries.map(g => g.id);
7921    const newIds = newGalleries.map(g => g.id);
7922    const missingIds = originalIds.filter(id => !newIds.includes(id));
7923    
7924    if (missingIds.length > 0) {
7925      console.error('Missing gallery IDs:', missingIds);
7926      
7927      // Add missing galleries to the new structure
7928      missingIds.forEach(id => {
7929        const missingGallery = galleries.find(g => g.id === id);
7930        if (missingGallery) {
7931          // Add it as a top-level item
7932          const newGallery = { ...missingGallery, parentId: null };
7933          if (newGallery.children) delete newGallery.children;
7934          newGalleries.push(newGallery);
7935          console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`);
7936        }
7937      });
7938    }
7939  }
7940  
7941  // Update the galleries array with the new structure
7942  galleries = newGalleries;
7943  if (typeof window !== 'undefined') {
7944    window.galleries = galleries;
7945  }
7946  
7947  // Save the updated structure
7948  saveGalleries();
7949} catch (error) {
7950  console.error('Error in updateGalleryStructure:', error);
7951  throw error; // Re-throw to trigger the backup restore
7952}
7953} else {
7954console.warn('Root list not found');
7955throw new Error('Root list not found'); // Throw error to trigger backup restore
7956}
7957}
7958
7959function toggleSubmenu(id, event) {
7960// Ensure the event doesn't interfere with drag operations
7961event.stopPropagation();
7962
7963const listItem = getGalleryTreeListItem(id, event);
7964if (listItem) {
7965// Toggle the expanded class
7966listItem.classList.toggle('expanded');
7967
7968// Find the submenu toggle icon and rotate it
7969const toggleIcon = listItem.querySelector('.toggle-icon');
7970if (toggleIcon) {
7971  toggleIcon.classList.toggle('rotated');
7972}
7973}
7974}
7975
7976/**
7977 * Improved slugify function with robust character handling
7978 * @param {string} text - The text to convert to a slug
7979 * @returns {string} A URL-friendly slug
7980 */
7981function slugify(text) {
7982  // Guard against null or empty input
7983  if (!text || text.trim() === '') {
7984    return 'page-' + Date.now();
7985  }
7986  
7987  // First convert to lowercase
7988  let slug = text.toLowerCase();
7989  
7990  // Replace spaces with hyphens
7991  slug = slug.split(' ').join('-');
7992  
7993  // Filter out unwanted characters (keep only a-z, 0-9, and hyphens)
7994  let filtered = '';
7995  for (let i = 0; i < slug.length; i++) {
7996    const char = slug.charAt(i);
7997    if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') {
7998      filtered += char;
7999    }
8000  }
8001  slug = filtered;
8002  
8003  /*
8004  // Clean up multiple hyphens
8005  while (slug.indexOf('--') !== -1) {
8006    slug = slug.replace('--', '-');
8007  }
8008    */
8009  
8010  // Remove leading and trailing hyphens
8011  if (slug.startsWith('-')) {
8012    slug = slug.substring(1);
8013  }
8014  if (slug.endsWith('-')) {
8015    slug = slug.substring(0, slug.length - 1);
8016  }
8017  
8018  // If slug is empty after processing, use a default
8019  if (!slug || slug === '-') {
8020    return 'page-' + Date.now();
8021  }
8022  
8023  return slug;
8024}
8025
8026// Update the addPage function to use our new robust slugify function
8027/**
8028 * Adds a new page to the galleries array
8029 * @returns {number|null} The new page ID or null if error
8030 */
8031function addPage() {
8032  try {
8033    // Get the page title
8034    const titleInput = document.getElementById('galleryTitle');
8035    if (!titleInput) {
8036      console.error('Page title input not found');
8037      return;
8038    }
8039    const title = titleInput.value.trim();
8040
8041    // Validate title is not empty
8042    if (!title) {
8043      alert('Please enter a page title');
8044      return;
8045    }
8046
8047    // Get parent ID with error handling
8048    const parentSelect = document.getElementById('galleryParent');
8049    const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null;
8050
8051    // Generate a unique ID for the page
8052    const id = Date.now();
8053    const pageId = `page_${id}`;
8054
8055    // Calculate slug using our more robust slugify function
8056    const pageSlug = slugify(title);
8057 
8058    // Create the page object with explicit properties
8059    const page = {
8060      id: id,
8061      title: title,
8062      isPage: true,
8063      isIntegrated: true,
8064      isSubmenu: false,
8065      pageId: pageId,
8066      visible: true,
8067      parentId: parentId,
8068      siteId: siteId,
8069      // Set slug and URL explicitly
8070      slug: pageSlug,
8071      url: '/' + pageSlug
8072    };
8073
8074    console.log(`Creating new page with ID: ${id}, title: "${title}"`);
8075
8076    // Add to galleries array
8077    galleries.push(page);
8078    
8079    // CRITICAL: Ensure window.galleries is synchronized
8080    if (typeof window.galleries === 'undefined') {
8081      window.galleries = galleries;
8082    } else {
8083      // Make sure the page is in window.galleries too
8084      const existingIndex = window.galleries.findIndex(g => g.id === id);
8085      if (existingIndex >= 0) {
8086        window.galleries[existingIndex] = page;
8087      } else {
8088        window.galleries.push(page);
8089      }
8090    }
8091
8092    // Initialize empty page elements array in PageManager
8093    if (window.PageManager) {
8094      window.PageManager.elements[pageId] = [];
8095    }
8096
8097    // Update UI first
8098    renderGalleries();
8099    clearAddForm();
8100    toggleAddForm();
8101
8102    // Set as active page
8103    console.log(`Setting new page "${page.title}" as active`);
8104    activeGalleryId = id;
8105    window.activeGalleryId = id;
8106    
8107    // Update active states in the menu
8108    updateActiveStates();
8109
8110    // Save to server
8111    saveGalleries().then(() => {
8112      console.log(`Page "${page.title}" saved successfully`);
8113      
8114      // Double check that our gallery is still in the array
8115      const pageExists = galleries.some(g => g.id === id);
8116      console.log(`Page exists in galleries array: ${pageExists}`);
8117      
8118      if (pageExists) {
8119        // Load the new page after a slight delay to ensure DOM is updated
8120        setTimeout(() => {
8121          console.log(`Loading new page "${page.title}" with ID: ${id}`);
8122          loadPage(id);
8123        }, 50);
8124      } else {
8125        console.error(`Page with ID ${id} not found in galleries array after save`);
8126      }
8127    }).catch(error => {
8128      alert('Error saving page. Please try again.');
8129    });
8130
8131    return id;
8132  } catch (error) {
8133    console.error('Error adding page:', error);
8134    alert('Error adding page. Please try again.');
8135    return null;
8136  }
8137}
8138
8139/**
8140 * Ensures a slug is unique among existing galleries
8141 * @param {string} slug - The base slug to check
8142 * @param {Array} existingGalleries - Array of existing gallery configs
8143 * @returns {string} A unique slug
8144 */
8145function ensureUniqueSlug(slug, existingGalleries) {
8146  // Guard against invalid slugs
8147  if (!slug || slug === '-') {
8148    slug = 'page-' + Date.now();
8149  }
8150
8151  let finalSlug = slug;
8152  let counter = 1;
8153  
8154  // Check if slug already exists
8155  while (existingGalleries.some(g => g.slug === finalSlug)) {
8156    finalSlug = `${slug}-${counter}`;
8157    counter++;
8158  }
8159  
8160  return finalSlug;
8161}
8162
8163/**
8164 * Adds a new gallery based on the selected type
8165 * @returns {number|null} The new gallery ID or null if error
8166 */
8167function addGallery() {
8168  try {
8169    // Get the gallery title
8170    const titleInput = document.getElementById('galleryTitle');
8171    if (!titleInput) {
8172      console.error('Gallery title input not found');
8173      return;
8174    }
8175    const title = titleInput.value.trim();
8176    
8177    // Get the selected gallery type
8178    const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked');
8179    if (!galleryTypeRadio) {
8180      console.error('No gallery type selected');
8181      alert('Please select a gallery type');
8182      return;
8183    }
8184
8185    const galleryType = galleryTypeRadio.value;
8186    console.log("Selected gallery type:", galleryType);
8187
8188    // If this is a page, use the addPage function
8189    if (galleryType === 'page') {
8190      console.log("Creating page instead of gallery");
8191      return addPage();
8192    }
8193
8194    // For spacer type, we don't require a title
8195    if (galleryType !== 'spacer' && !title) {
8196      alert('Please enter a gallery title');
8197      return;
8198    }
8199
8200    // Continue with gallery creation logic
8201    // Generate unique ID
8202    const id = Date.now();
8203    
8204    // Get parent gallery if selected
8205    const parentSelect = document.getElementById('galleryParent');
8206    const parentId = parentSelect.value ? parseInt(parentSelect.value) : null;
8207    
8208    // Get URL if this is an external gallery
8209    const galleryUrl = document.getElementById('galleryUrl');
8210    const url = galleryTypeRadio.value === 'external' && galleryUrl 
8211      ? galleryUrl.value.trim() 
8212      : "";
8213    
8214    // Set basic properties
8215    const gallery = {
8216      id: id,
8217      title: title || "Spacer", // Use "Spacer" as internal title if no title provided
8218      parentId: parentId,
8219      visible: true
8220    };
8221    
8222    // Set type-specific properties
8223    if (galleryTypeRadio.value === 'spacer') {
8224      // Spacer properties
8225      gallery.isSpacer = true;
8226      gallery.title = title || "Spacer"; // For internal reference
8227    } else if (galleryTypeRadio.value === 'integrated') {
8228      // Generate pageId for integrated galleries
8229      gallery.isIntegrated = true;
8230      gallery.pageId = generatePageId();
8231      
8232      // Generate and ensure unique slug
8233      const baseSlug = slugify(title);
8234      gallery.slug = ensureUniqueSlug(baseSlug, galleries);
8235      gallery.url = "/" + gallery.slug;
8236      
8237      // Get the siteId from multiple possible sources
8238      const siteId = window.Parameters?.siteId || 
8239                    window.siteId || 
8240                    document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') ||
8241                    '';
8242      
8243      // Add galleryOptions with GUID=siteId as the manualCollectionName
8244      gallery.galleryOptions = {
8245        manualCollectionName: `GUID=${siteId}`,
8246        isIntegratedGallery: true,
8247        pageId: gallery.pageId,  // Store pageId for reference
8248        siteId: siteId
8249      };
8250    } else if (galleryTypeRadio.value === 'folder') {
8251      gallery.isFolder = true;
8252      gallery.isCollapsed = true; // Make folders closed by default
8253    } else if (galleryTypeRadio.value === 'external') {
8254      // External URL
8255      gallery.url = url;
8256      gallery.isExternal = true; // Add this flag to identify external URLs
8257    }
8258    
8259    console.log(`Creating new gallery with ID: ${id}, title: "${title}"`);
8260    
8261    // Add to galleries array
8262    galleries.push(gallery);
8263    
8264    // CRITICAL: Ensure window.galleries is synchronized
8265    if (typeof window.galleries === 'undefined') {
8266      window.galleries = galleries;
8267    } else {
8268      // Make sure the gallery is in window.galleries too
8269      const existingIndex = window.galleries.findIndex(g => g.id === id);
8270      if (existingIndex >= 0) {
8271        window.galleries[existingIndex] = gallery;
8272      } else {
8273        window.galleries.push(gallery);
8274      }
8275    }
8276    
8277    // Update UI first
8278    renderGalleries();
8279    clearAddForm();
8280    toggleAddForm();
8281    
8282    // Save galleries after UI update
8283    saveGalleries().then(() => {
8284      console.log(`Gallery "${gallery.title}" saved successfully`);
8285      
8286      // Double check that our gallery is still in the array
8287      const galleryExists = galleries.some(g => g.id === id);
8288      console.log(`Gallery exists in galleries array: ${galleryExists}`);
8289      
8290      // For non-folder types, load the new gallery content after a slight delay
8291      if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) {
8292        setTimeout(() => {
8293          console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`);
8294          if (gallery.isIntegrated) {
8295            loadGallery(id);
8296          } else if (gallery.isExternal && gallery.url) {
8297            // For external URLs, set the iframe src and mark active after navigation target is ready
8298            activeGalleryId = id;
8299            window.activeGalleryId = id;
8300            updateActiveStates();
8301            const frame = document.getElementById('galleryFrame');
8302            if (frame) frame.src = gallery.url;
8303          }
8304        }, 50);
8305      }
8306    }).catch(error => {
8307      console.error(`Error saving gallery "${gallery.title}":`, error);
8308      alert('Error saving gallery. Please try again.');
8309    });
8310    
8311    // Return the new gallery ID
8312    return id;
8313  } catch (error) {
8314    console.error('Error adding gallery:', error);
8315    alert('Error adding gallery. Please try again.');
8316  }
8317}
8318
8319function debugDOMState(message) {
8320console.log(`[DEBUG] ${message}`);
8321const galleryContainer = document.querySelector('.gallery-container');
8322const iframe = document.getElementById('galleryFrame');
8323const pageContainers = document.querySelectorAll('.page-container');
8324
8325console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`);
8326console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`);
8327console.log(`- Page containers: ${pageContainers.length} found`);
8328
8329pageContainers.forEach((container, index) => {
8330console.log(`  - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`);
8331});
8332}
8333
8334
8335/**
8336* Global helper: whether edit tools are explicitly toggled open.
8337* Do not use Parameters.isInEditor — that is also true for logged-in admins (cache bypass).
8338* Do not infer from sidebar/page .editing alone (can desync from the toggle).
8339*/
8340window.isInEditMode = function() {
8341  if (document.body.classList.contains('edit-mode-active')) {
8342    return true;
8343  }
8344  if (typeof window.isEditing === 'boolean') {
8345    return window.isEditing;
8346  }
8347  return false;
8348};
8349
8350/**
8351* This function synchronizes edit mode state across all components
8352*/
8353window.updateGlobalEditState = function(isEditing) {
8354console.log('Updating global edit state:', isEditing);
8355
8356// Update global variable
8357window.isEditing = isEditing;
8358
8359// Update Parameters object if it exists
8360if (window.Parameters) {
8361window.Parameters.isInEditor = isEditing;
8362}
8363
8364// Update sidebar class
8365const sidebar = document.querySelector('.sidebar');
8366if (sidebar) {
8367if (isEditing) {
8368  sidebar.classList.add('editing');
8369} else {
8370  sidebar.classList.remove('editing');
8371}
8372}
8373
8374// Update page containers
8375const pageContainers = document.querySelectorAll('.page-container');
8376pageContainers.forEach(container => {
8377if (isEditing) {
8378  container.classList.add('editing');
8379} else {
8380  container.classList.remove('editing');
8381}
8382});
8383
8384// Notify all components via event
8385document.dispatchEvent(new CustomEvent('edit-mode-changed', {
8386detail: { editing: isEditing }
8387}));
8388
8389if (document.body.classList.contains('menu-layout-horizontal') && typeof window.syncContentMarginToHorizontalHeader === 'function') {
8390  window.syncContentMarginToHorizontalHeader();
8391}
8392
8393if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8394  window.syncGalleryOptionsButtonVisibility();
8395}
8396
8397// Gallery Options panel styling depends on body.edit-mode-active — hide it when leaving edit mode
8398if (!isEditing && typeof window.closeOptionsPanel === 'function') {
8399  window.closeOptionsPanel();
8400}
8401};
8402
8403/** Show or hide gallery options (#editorButton) based on edit mode and active content type */
8404window.syncGalleryOptionsButtonVisibility = function() {
8405  const btn = document.getElementById('editorButton');
8406  if (!btn) return;
8407
8408  const inEditMode = document.body.classList.contains('edit-mode-active');
8409  // When content type is explicitly gallery, ignore leftover .page-container nodes
8410  // (common when Help sandbox switches from a page to Example Gallery).
8411  const onPageView = window._activeContentType === 'gallery'
8412    ? false
8413    : (window._activeContentType === 'page' ||
8414      !!document.querySelector('.gallery-container .page-container'));
8415  let isActiveGallery = false;
8416  const activeId = window.activeGalleryId;
8417  const list = window.galleries || (typeof galleries !== 'undefined' ? galleries : null);
8418  const activeItem = activeId != null && list && typeof findGalleryById === 'function'
8419    ? findGalleryById(list, activeId)
8420    : (Array.isArray(list) ? list.find(function(g) { return g && g.id === activeId; }) : null);
8421  if (activeId != null && inEditMode && !onPageView) {
8422    isActiveGallery = !!(activeItem && !isPageMenuItem(activeItem) && !activeItem.isFolder && !activeItem.isSpacer && !activeItem.isExternal);
8423  }
8424
8425  const shouldShow = inEditMode && !onPageView && isActiveGallery;
8426  if (shouldShow) {
8427    if (window.Parameters) {
8428      window.Parameters.isInEditor = true;
8429    }
8430    btn.classList.remove('gallery-options-force-hidden');
8431    btn.style.removeProperty('display');
8432    btn.style.setProperty('display', 'flex', 'important');
8433    btn.style.pointerEvents = 'auto';
8434  } else {
8435    btn.classList.add('gallery-options-force-hidden');
8436    btn.style.setProperty('display', 'none', 'important');
8437    btn.style.pointerEvents = '';
8438  }
8439};
8440
8441document.addEventListener('gallery-loaded', function() {
8442  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8443    window.syncGalleryOptionsButtonVisibility();
8444  }
8445});
8446
8447document.addEventListener('page-loaded', function() {
8448  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8449    window.syncGalleryOptionsButtonVisibility();
8450  }
8451});
8452
8453/**
8454 * Wait for PageManager after CDN→proxy late script load (ISP/Firefox clients).
8455 * Home/page bootstrap can run before the proxy copy of page-manager-v260720-021.js executes.
8456 */
8457function whenPageManagerReady(callback, options) {
8458  const maxAttempts = (options && options.maxAttempts) || 50;
8459  const intervalMs = (options && options.intervalMs) || 100;
8460  let attempts = 0;
8461  const run = function() {
8462    if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8463      callback(window.PageManager);
8464      return;
8465    }
8466    attempts += 1;
8467    if (attempts >= maxAttempts) {
8468      console.error('❌ [loadPage] Timed out waiting for PageManager after', attempts, 'attempts');
8469      if (options && typeof options.onTimeout === 'function') options.onTimeout();
8470      return;
8471    }
8472    if (attempts === 1 || attempts % 10 === 0) {
8473      console.warn('⏳ [loadPage] Waiting for PageManager (CDN proxy late load?) attempt', attempts + '/' + maxAttempts);
8474    }
8475    setTimeout(run, intervalMs);
8476  };
8477  run();
8478}
8479
8480/**
8481 * loadPage function that safely handles undefined galleries
8482 * @param {number} id - The gallery ID
8483 * @param {Event} event - Optional event object
8484 */
8485function loadPage(id, event){
8486  if (event && event.target && event.target.isConnected === false) {
8487    console.log('loadPage: Ignoring navigation from detached element');
8488    return;
8489  }
8490  if (event) {
8491    event.preventDefault();
8492    event.stopPropagation();
8493    const now = Date.now();
8494    const lastCallTime = window._lastPageLoadTime || 0;
8495    window._lastPageLoadTime = now;
8496    if (now - lastCallTime < 100) {
8497      console.log('Ignoring duplicate loadPage call');
8498      return;
8499    }
8500  }
8501  console.log('loadPage: Loading page with gallery ID:', id);
8502
8503  stopAutoAdvanceTimer();
8504  if (typeof dismissGalleryPasswordOverlay === 'function') {
8505    dismissGalleryPasswordOverlay();
8506  }
8507
8508  let galleriesData = window.galleries || galleries;
8509  const gallery = findGalleryById(galleriesData, id);
8510
8511  if (!gallery) {
8512    console.warn('No gallery found with ID:', id, 'for page load.');
8513    return;
8514  }
8515
8516  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
8517
8518  window.activeGalleryId = id;
8519  activeGalleryId = id;
8520  window._activeContentType = 'page';
8521  if (window.EnhancedGallerySystem) {
8522    window.EnhancedGallerySystem.currentGalleryId = null;
8523  }
8524
8525  if (typeof window.removeGalleryScriptsWithPause === 'function') {
8526    window.removeGalleryScriptsWithPause();
8527  }
8528
8529  const galleryContainer = document.querySelector('.gallery-container');
8530  if (galleryContainer) {
8531    galleryContainer.innerHTML = '';
8532  }
8533
8534  if (!gallery.pageId) gallery.pageId = `page_${id}`;
8535  if (!gallery.isPage) gallery.isPage = true;
8536
8537  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8538    window.syncGalleryOptionsButtonVisibility();
8539  }
8540
8541  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
8542  window._pageIdToGalleryId[gallery.pageId] = id;
8543
8544  if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
8545    if (window.PageManager && window.PageManager.elements) {
8546      window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8547    }
8548  } else if (window.PageManager && window.PageManager.elements &&
8549             window.PageManager.elements[gallery.pageId] &&
8550             window.PageManager.elements[gallery.pageId].length > 0) {
8551    gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
8552  }
8553
8554  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8555    try {
8556      window.PageManager.loadPage(gallery.pageId);
8557      const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
8558      if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
8559        window.PageManager.setEditMode(isCurrentlyEditing);
8560      }
8561    } catch (error) {
8562      console.error('Error loading page with PageManager:', error);
8563    }
8564  } else {
8565    console.error('PageManager not found yet — waiting for CDN proxy script');
8566    whenPageManagerReady(function() {
8567      try {
8568        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
8569          console.log('loadPage: Skipping stale PageManager retry for', gallery.pageId);
8570          return;
8571        }
8572        if (gallery.pageElements && Array.isArray(gallery.pageElements) && window.PageManager.elements) {
8573          window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8574        }
8575        window.PageManager.loadPage(gallery.pageId);
8576        console.log('loadPage: PageManager became available — page render retried');
8577      } catch (error) {
8578        console.error('Error loading page with PageManager after wait:', error);
8579      }
8580    });
8581  }
8582
8583  // Apply menu visibility
8584  const bodyEl = document.body;
8585  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
8586      bodyEl.classList.add('menu-hidden-on-page');
8587  } else {
8588      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
8589  }
8590  applyMenuOverlayClassForGallery(gallery);
8591
8592  if (typeof updateActiveStates === 'function') updateActiveStates();
8593  if (typeof updateMobileTitle === 'function') updateMobileTitle();
8594  if (typeof closeMobileMenu === 'function') closeMobileMenu();
8595  // Close the gallery options panel when navigating to a different page
8596  if (typeof window.closeOptionsPanel === 'function') {
8597    window.closeOptionsPanel();
8598  }
8599  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
8600  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8601    window.syncGalleryOptionsButtonVisibility();
8602  }
8603  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
8604    window.dispatchEvent(new CustomEvent('page-loaded', {
8605      detail: { pageId: gallery.pageId, galleryId: id }
8606    }));
8607  }
8608}
8609
8610
8611
8612
8613
8614function generatePageId() {
8615const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
8616let result = '';
8617const charactersLength = characters.length;
8618for (let i = 0; i < 8; i++) {
8619  result += characters.charAt(Math.floor(Math.random() * charactersLength));
8620}
8621return result;
8622};
8623
8624
8625function normalizeNameForComparison(name) {
8626if (!name) {
8627console.log("Empty name passed to normalization");
8628return '';
8629}
8630
8631console.log(`Normalizing name: "${name}"`);
8632
8633// Step 1: Convert to lowercase
8634let normalized = name.toLowerCase();
8635console.log(`After lowercase: "${normalized}"`);
8636
8637// Step 2: Replace spaces with hyphens (without regex)
8638normalized = normalized.split(' ').join('-');
8639console.log(`After space replacement: "${normalized}"`);
8640
8641// Step 3: Filter out unwanted characters (without regex)
8642let filtered = '';
8643for (let i = 0; i < normalized.length; i++) {
8644const char = normalized.charAt(i);
8645// Keep only a-z, 0-9, and hyphens
8646if ((char >= 'a' && char <= 'z') || 
8647    (char >= '0' && char <= '9') || 
8648    char === '-') {
8649  filtered += char;
8650}
8651}
8652normalized = filtered;
8653console.log(`After character filtering: "${normalized}"`);
8654
8655// Step 4: Clean up multiple hyphens (without regex)
8656while (normalized.indexOf('--') !== -1) {
8657normalized = normalized.split('--').join('-');
8658}
8659
8660// Step 5: Remove leading and trailing hyphens (without regex)
8661if (normalized.charAt(0) === '-') {
8662normalized = normalized.substring(1);
8663}
8664if (normalized.charAt(normalized.length - 1) === '-') {
8665normalized = normalized.substring(0, normalized.length - 1);
8666}
8667
8668// Check if the result is valid
8669if (!normalized || normalized === '-') {
8670console.log(`WARNING: Normalization produced invalid result: "${normalized}"`);
8671} else {
8672console.log(`Final normalized slug: "${normalized}"`);
8673}
8674
8675return normalized;
8676}
8677
8678function ensureUniqueSlug(slug, existingGalleries) {
8679if (!slug) return 'gallery';
8680
8681let finalSlug = slug;
8682let counter = 1;
8683
8684// Check if slug exists in any gallery
8685const slugExists = function(s) {
8686for (let i = 0; i < existingGalleries.length; i++) {
8687  if (existingGalleries[i].slug === s) {
8688    return true;
8689  }
8690}
8691return false;
8692};
8693
8694// Keep incrementing counter until unique
8695while (slugExists(finalSlug)) {
8696finalSlug = slug + '-' + counter;
8697counter++;
8698}
8699
8700return finalSlug;
8701}
8702
8703/**
8704 * Gathers the current complete site configuration from the client-side state
8705 * and triggers a download of the data as a single JSON file.
8706 * This provides a simple way to back up or export the current site structure.
8707 */
8708function downloadCurrentSiteJSON() {
8709  try {
8710    console.log('Gathering current site data for download...');
8711
8712    // Get the current hostname to use in the filename.
8713    const hostname = window.location.hostname.replace('www.', '');
8714
8715    // --- FIX: Ensure we get the most up-to-date menu styles ---
8716    // The MenuStyleCustomizer.settings object is the most reliable source
8717    // for the current styles being used on the client.
8718    const currentMenuStyles = window.MenuStyleCustomizer?.settings || {};
8719
8720    // Get current galleries
8721    const currentGalleries = window.galleries || galleries || [];
8722
8723    // --- STREAMLINED: Process galleries without including image data ---
8724    const enhancedGalleries = currentGalleries.map(gallery => {
8725      // If this is a Classic collection gallery, add note about manual retrieval
8726      if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) {
8727        const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5);
8728        console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`);
8729        
8730        return {
8731          ...gallery,
8732          _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`,
8733          _classicGuid: classicGuid,
8734          _requiresClassicData: true
8735        };
8736      }
8737      
8738      return gallery;
8739    });
8740
8741    // Assemble the complete site configuration object from global state variables.
8742    // This structure mirrors what the `save-config` API endpoint expects, ensuring
8743    // the exported file is a complete and re-importable replica.
8744    const siteData = {
8745      // The core menu/page structure with enhanced Classic collection data.
8746      galleries: enhancedGalleries,
8747      
8748      // Admin emails are managed server-side for security. We export the list if it's
8749      // available on the client from a previous check, but the server remains the
8750      // source of truth. A re-import will not overwrite the admin list.
8751      adminEmails: window.siteConfig?.adminEmails || [],
8752      
8753      // The unique ID for the site.
8754      siteId: window.siteId || window.Parameters?.siteId || '',
8755      
8756      // Custom styling rules for the menu.
8757      menuStyles: currentMenuStyles,
8758      
8759      // All elements configured to appear in the sidebar.
8760      sidebarElements: window.SidebarManager?.elements || [],
8761      
8762      // Global site metadata for SEO and analytics.
8763      siteMetadata: window.siteMetadata || {},
8764      
8765      // Add metadata about Classic collections that need manual data retrieval
8766      _classicCollections: enhancedGalleries
8767        .filter(g => g._requiresClassicData)
8768        .map(g => ({
8769          title: g.title,
8770          guid: g._classicGuid,
8771          note: g._note
8772        }))
8773    };
8774
8775    console.log('Enhanced site data collected for export:', siteData);
8776
8777    // Convert the JavaScript object to a formatted JSON string.
8778    const jsonString = JSON.stringify(siteData, null, 2);
8779
8780    // Create a Blob, which is a file-like object of immutable, raw data.
8781    const blob = new Blob([jsonString], { type: 'application/json' });
8782
8783    // Create a temporary link element to trigger the browser's download functionality.
8784    const url = URL.createObjectURL(blob);
8785    const a = document.createElement('a');
8786    a.href = url;
8787    a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json'
8788    document.body.appendChild(a);
8789    a.click();
8790
8791    // Clean up by removing the temporary link and revoking the object URL to free up memory.
8792    document.body.removeChild(a);
8793    URL.revokeObjectURL(url);
8794
8795    console.log('Enhanced JSON download initiated successfully.');
8796
8797    // Show a helpful message about Classic collections
8798    const classicCount = siteData._classicCollections?.length || 0;
8799    
8800    if (classicCount > 0) {
8801      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 
8801need to manually fetch the Classic collection JSON files.`);
8802    }
8803
8804  } catch (error) {
8805    console.error('Error creating JSON download:', error);
8806    alert('An error occurred while preparing the download. Please check the console.');
8807  }
8808}
8809
8810// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array)
8811function buildHydraClassicNdjson(classicGuid, images) {
8812  const header = {
8813    title: 'Untitled',
8814    description: '',
8815    version: '1',
8816    isClassic: true,
8817    isClassicCollection: true,
8818    classicGuid: 'GUID=' + String(classicGuid || '')
8819  };
8820
8821  const imageArray = Array.isArray(images) ? images.map(function(img, idx) {
8822    var path = '';
8823    if (img) {
8824      path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || '';
8825    }
8826    // Derive filename base from any provided path/filename and normalize extension to .jpg
8827    var fnameBase = '';
8828    if (typeof path === 'string' && path.length > 0) {
8829      var slash = path.lastIndexOf('/');
8830      var raw = slash >= 0 ? path.substring(slash + 1) : path;
8831      // remove query/hash
8832      var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q);
8833      var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h);
8834      // strip extension if present
8835      var dot = raw.lastIndexOf('.');
8836      fnameBase = dot > 0 ? raw.substring(0, dot) : raw;
8837    } else if (img && typeof img.filename === 'string') {
8838      var rf = img.filename;
8839      var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2);
8840      var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2);
8841      var d2 = rf.lastIndexOf('.');
8842      fnameBase = d2 > 0 ? rf.substring(0, d2) : rf;
8843    }
8844    var ext = (img && img.metadata && typeof img.metadata.extension === 'string' && img.metadata.extension.startsWith('.'))
8845      ? img.metadata.extension : '.jpg';
8846    var filename = fnameBase ? (fnameBase + ext) : '';
8847    // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename}
8848    // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path
8849    var guidRaw = String(classicGuid || '').replace(/^GUID=/, '');
8850    var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path;
8851    var rec = {
8852      caption: (img && (img.caption || img.description)) || '',
8853      description: (img && (img.description || img.caption)) || '',
8854      isPreview: idx === 1,
8855      title: (img && img.title) || '',
8856      'alt-text': (img && (img['alt-text'] || img.altText || '')) || '',
8857      image: cdnImageUrl || '',
8858      link: '',
8859      'link-text': '',
8860      filename: filename,
8861      isClassic: true,
8862      isClassicCollection: true,
8863      classicGuid: 'GUID=' + String(classicGuid || '')
8864    };
8865    return rec;
8866  }) : [];
8867
8868  // Build NDJSON as: header line, imageMetadata array line, then one line per full image record
8869  var body = { imageMetadata: imageArray };
8870  var out = [JSON.stringify(header), JSON.stringify(body)];
8871  for (var k = 0; k < imageArray.length; k++) {
8872    out.push(JSON.stringify(imageArray[k]));
8873  }
8874  return out.join('\n') + '\n';
8875}
8876
8877async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) {
8878  // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB)
8879  const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson';
8880  const flyUrl = 'https://hydra-press-v2.fly.dev/publish';
8881  const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys';
8882  
8883  console.error('Starting dual NDJSON upload for:', fileName);
8884  console.error('Content length:', ndjsonContent.length, 'characters');
8885  
8886  // COMPRESSION: Compress for Irys only (not Tigris)
8887  console.error('🔄 Compressing NDJSON with gzip for Irys...');
8888  const encoder = new TextEncoder();
8889  const ndjsonBytes = encoder.encode(ndjsonContent);
8890  const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip'));
8891  const compressedBlob = await new Response(compressedStream).blob();
8892  const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer());
8893  
8894  const originalSize = ndjsonBytes.length;
8895  const compressedSize = compressedBytes.length;
8896  const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2);
8897  
8898  console.error('📊 Compression results:', {
8899    original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)',
8900    compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)',
8901    ratio: compressionRatio + '%'
8902  });
8903  
8904  const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload
8905  const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT;
8906  
8907  console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)');
8908  
8909  // STEP 1: Upload to Irys (non-blocking, compressed if <800KB)
8910  let irysResult = null;
8911  if (shouldUploadToIrys) {
8912    try {
8913      console.error('🔄 Starting Irys upload (compressed, non-blocking)...');
8914      
8915      // Convert compressed bytes to base64
8916      const compressedBase64 = btoa(String.fromCharCode(...compressedBytes));
8917      
8918      const irysResp = await fetch(irysUrl, {
8919        method: 'POST',
8920        headers: { 'content-type': 'application/json' },
8921        body: JSON.stringify({ 
8922          compressedData: compressedBase64,
8923          originalSize: originalSize,
8924          compressedSize: compressedSize,
8925          siteId: siteId,
8926          pageId: pageId
8927        })
8928      });
8929      
8930      if (irysResp.ok) {
8931        irysResult = await irysResp.json();
8932        console.error('✅ Irys upload successful:', irysResult.txId);
8933        console.error('   Irys Gateway:', irysResult.urls?.irys);
8934        console.error('   ar.io Gateway:', irysResult.urls?.arweave);
8935        
8936        // Log wallet balance and cost
8937        if (irysResult.walletBalance !== undefined) {
8938          console.error('💰 Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR');
8939        }
8940        if (irysResult.uploadCost !== undefined) {
8941          const costUSD = (irysResult.uploadCost * 20).toFixed(4);
8942          if (irysResult.uploadCost === 0) {
8943            console.error('💸 Upload Cost: FREE (0 AR)');
8944          } else {
8945            console.error('💸 Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)');
8946          }
8947        }
8948      } else {
8949        const errorText = await irysResp.text().catch(() => 'Unknown error');
8950        console.warn('⚠️ Irys upload failed (non-critical):', irysResp.status, errorText);
8951      }
8952    } catch (irysError) {
8953      console.warn('⚠️ Irys upload error (non-critical):', irysError.message);
8954      // Continue - Irys failure doesn't block Fly.io upload
8955    }
8956  } else {
8957    console.error('⏭️ Skipping Irys upload (compressed size exceeds 800KB limit)');
8958  }
8959  
8960  // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed)
8961  // Hydra sites require Bearer auth - get token from TokenManager/localStorage (same as Edition Publisher)
8962  try {
8963    console.error('🔄 Starting Fly.io upload (uncompressed)...');
8964    const flyHeaders = { 'content-type': 'application/json' };
8965    let authToken = null;
8966    try {
8967      if (window.TokenManager) {
8968        if (typeof window.TokenManager.getHydraAuthToken === 'function') {
8969          try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
8970        }
8971        if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
8972          try {
8973            const tw = window.TokenManager.getBestToken();
8974            authToken = tw && tw.startsWith('Bearer ') ? tw.substring(7) : tw;
8975          } catch (e) {}
8976        }
8977      }
8978      if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
8979      if (!authToken) authToken = window.authToken;
8980      if (authToken) {
8981        flyHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
8982          ? 'Bearer ' + authToken
8983          : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
8984        console.error('🔐 Fly.io auth: token attached');
8985      } else {
8986        console.warn('⚠️ No auth token - Fly.io may reject Hydra site upload (401)');
8987      }
8988    } catch (authErr) {
8989      console.warn('⚠️ Auth token lookup failed:', authErr.message);
8990    }
8991    const flyResp = await fetch(flyUrl, {
8992      method: 'POST',
8993      headers: flyHeaders,
8994      body: JSON.stringify({ ndjsonContent, dataFileName: fileName })
8995    });
8996    
8997    console.log('Server response status:', flyResp.status);
8998    
8999    if (!flyResp.ok) {
9000      const errorText = await flyResp.text().catch(() => 'Unknown error');
9001      console.error('❌ Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText);
9002      if (flyResp.status === 401) {
9003        console.error('💡 Tip: Open the Edition Publisher (or log in as admin) first to authenticate for Fly.io uploads.');
9004      }
9005      return { success: false, irysResult: null };
9006    }
9007    
9008    const flyData = await flyResp.json().catch(() => null);
9009    if (flyData && flyData.urls) {
9010      console.log('✅ Fly.io upload successful:', flyData.urls);
9011      console.log('   Tigris URL:', flyData.urls.urlTigris);
9012      
9013      // Return comprehensive result
9014      const result = {
9015        success: true,
9016        flyUrls: flyData.urls,
9017        irysResult: irysResult,
9018        uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'],
9019        fileSize: compressedSize, // Irys compressed size
9020        originalFileSize: originalSize // Original uncompressed size
9021      };
9022      
9023      console.log('📦 Upload complete:', {
9024        destinations: result.uploadedTo.join(' + '),
9025        txId: irysResult?.txId || 'N/A',
9026        flyUrl: flyData.urls.urlTigris
9027      });
9028      
9029      return result;
9030    } else {
9031      console.warn('Fly.io upload response missing URLs:', flyData);
9032      return { success: false, irysResult: null };
9033    }
9034  } catch (flyError) {
9035    console.error('❌ Fly.io upload error for', fileName + ':', flyError);
9036    return { success: false, irysResult: null };
9037  }
9038}
9039
9040async function importClassicGalleries() {
9041  // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS
9042  function cleanDescriptionLeadingLines(text) {
9043    if (!text || typeof text !== 'string') return text;
9044
9045    let result = text;
9046
9047    // Remove leading empty TEXTFORMAT blocks: <TEXTFORMAT><P><FONT></FONT></P></TEXTFORMAT>
9048    result = result.replace(
9049      new RegExp(
9050        '^(\s*\<TEXTFORMAT[^\>]*\>\s*\<P[^\>]*\>\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+',
9051        'i'
9052      ),
9053      ''
9054    );
9055
9056    // Remove leading empty P tags
9057    result = result.replace(
9058      new RegExp('^(\s*\<P[^\>]*\>\s*\<\/P\>\s*)+', 'i'),
9059      ''
9060    );
9061
9062    // Remove leading empty FONT tags
9063    result = result.replace(
9064      new RegExp('^(\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*)+', 'i'),
9065      ''
9066    );
9067
9068    // Remove leading BR tags
9069    result = result.replace(
9070      new RegExp('^(\s*\<br\s*\/\?>\s*)+', 'i'),
9071      ''
9072    );
9073
9074    // Remove any remaining leading whitespace
9075    return result.replace(new RegExp('^\s+', 'i'), '').trim();
9076  }
9077
9078  // Browser-scoped helper: fix multi-line links (template-safe escaped regex)
9079  function fixMultilineLinks(text) {
9080    if (!text || typeof text !== 'string') return text;
9081    return text.replace(
9082      new RegExp('<a[^>]*>.*?Click.*?<\/a><a[^>]*>.*?HERE.*?<\/a><a[^>]*>.*?to Add to Cart.*?<\/a>', 'gis'),
9083      function (match) {
9084        const first = match.match(new RegExp('<a href="([^"]*)" target="([^"]*)"', 'i'));
9085        if (first) {
9086          return '<a href="' + first[1] + '" target="' + first[2] + '">
9086Click HERE to Add to Cart</a>';
9087        }
9088        return match;
9089      }
9090    );
9091  }
9092
9093  // Browser-scoped helper: convert plain text link phrase to HTML link
9094  function convertPlainTextLinks(text) {
9095    if (!text || typeof text !== 'string') return text;
9096    return text.replace(
9097      new RegExp('Click HERE to Add to Cart', 'g'),
9098      '<a href="#" target="_blank">Click HERE to Add to Cart</a>'
9099    );
9100  }
9101  // Get input elements, including the new checkbox
9102  const guidInputEl = document.getElementById('classicGuid');
9103  const pastedJsonEl = document.getElementById('pastedJson');
9104  const parentIdInputEl = document.getElementById('importParent');
9105  const createSubmenuCheckboxEl = document.getElementById('createSubmenu');
9106  const submenuTitleInputEl = document.getElementById('submenuTitle');
9107  const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox');
9108
9109  // Get values from inputs
9110  const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : '';
9111  const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : '';
9112  const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false;
9113  
9114  let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null;
9115  let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false;
9116  let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : '';
9117
9118  // If replacing the menu, ignore parent/submenu settings
9119  if (replaceMenu) {
9120      parentId = null;
9121      createSubmenu = false;
9122  }
9123
9124  // Validate submenu title if checkbox is checked
9125  if (createSubmenu && !submenuTitle) {
9126    alert('Please provide a title for the submenu');
9127    return;
9128  }
9129
9130  // Show status display
9131  const statusEl = document.getElementById('importStatus');
9132  const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null;
9133  const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null;
9134  const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null;
9135  const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null;
9136
9137  if (statusEl && progressBar && statusMessage) {
9138    statusEl.style.display = 'block';
9139    statusEl.classList.add('importing');
9140    progressBar.style.width = '10%';
9141    statusMessage.textContent = 'Preparing to import...';
9142  }
9143
9144  try {
9145    let classicData;
9146    let classicGuid; // Will hold the primary GUID for image path lookups
9147
9148    // --- LOGIC TO GET DATA: Prioritize Pasted JSON ---
9149    if (pastedJson) {
9150      if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...';
9151      classicData = JSON.parse(pastedJson);
9152
9153      if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9154        throw new Error('Pasted JSON is invalid: "galleries" array not found.');
9155      }
9156      // Use the first available GUID from the pasted data as the default for images
9157      classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId;
9158
9159    } else if (guidInput) {
9160      // Check if this is the new import_ format
9161      if (guidInput.startsWith('import_')) {
9162        if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...';
9163        const importGuid = guidInput.substring(7); // Remove 'import_' prefix
9164        
9165        const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`;
9166        const response = await fetch(jsonUrl);
9167        if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
9168        
9169        classicData = await response.json();
9170        
9171        if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9172          throw new Error('Fetched import JSON is invalid: "galleries" array not found.');
9173        }
9174        
9175        // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata
9176        if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...';
9177        
9178        const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`;
9179        
9180        try {
9181          const originalResponse = await fetch(originalJsonUrl);
9182          if (originalResponse.ok) {
9183            const originalData = await originalResponse.json();
9184            
9185            // Get the default language from site configuration
9186            let defaultLanguageID = 'en'; // fallback
9187            if (originalData.languages && Array.isArray(originalData.languages)) {
9188              const defaultLang = originalData.languages.find(lang => lang.isDefault === true);
9189              if (defaultLang) {
9190                defaultLanguageID = defaultLang.id;
9191              }
9192            }
9193            
9194            // Merge image metadata from original JSON into import JSON galleries
9195            if (originalData.galleries && Array.isArray(originalData.galleries)) {
9196              classicData.galleries = classicData.galleries.map(importGallery => {
9197                // Find matching gallery in original data by name or categoryID
9198                
9199                const originalGallery = originalData.galleries.find(orig => 
9200                  orig.title === importGallery.title || // Primary match by title
9201                  orig.name === importGallery.title ||  // Fallback: orig.name matches import.title
9202                  orig.categoryID === importGallery.categoryID // Fallback: categoryID match
9203                );
9204                
9205                
9206                if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) {
9207                  
9208                  // Process all images from original gallery with metadata extraction
9209                    const processedImages = originalGallery.images.map(originalImage => {
9210                    // Apply EXACT same metadata processing logic as Edition Publisher
9211                    let title = originalImage.title || '';
9212                    let dateline = originalImage.dateline || '';
9213                    let caption = originalImage.caption || '';
9214                    
9215                    // Check languageVersions array for metadata (this is where most data is stored)
9216                    if (originalImage.languageVersions && originalImage.languageVersions.length > 0) {
9217                      // Look for site's default language first, then fall back to first available
9218                      let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID);
9219                      if (!langData) {
9220                        langData = originalImage.languageVersions[0];
9221                      }
9222                      
9223                      // For language-enabled JSON files, prioritize languageVersions data
9224                      // Only fall back to top-level if languageVersions data is empty
9225                      if (!title || title.trim() === '') title = langData.title || '';
9226                      if (!dateline || dateline.trim() === '') dateline = langData.dateline || '';
9227                      if (!caption || caption.trim() === '') caption = langData.caption || '';
9228                    }
9229                    
9230                    // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals)
9231                    let description = caption;
9232                    if (caption && caption.includes('TEXTFORMAT')) {
9233                      try {
9234                        // Use properly escaped HTML characters for template literal context
9235                        description = caption
9236                          .replace(/\<TEXTFORMAT[^\>]*\>/g, '')
9237                          .replace(/\<\/TEXTFORMAT\>/g, '')
9238                          .replace(/\<P[^\>]*\>/g, '\<p\>')
9239                          .replace(/\<\/P\>/g, '\<\/p\>')
9240                          .replace(/\<FONT[^\>]*\>/g, '')
9241                          .replace(/\<\/FONT\>/g, '')
9242                          .replace(/\<A HREF="([^"]*)"[^\>]*\>/g, '\<a href="$1" target="_blank"\>')
9243                          .replace(/\<\/A\>/g, '\<\/a\>')
9244                          .replace(/\<U\>/g, '\<u\>')
9245                          .replace(/\<\/U\>/g, '\<\/u\>')
9246                          .replace(/\<B\>/g, '\<strong\>')
9247                          .replace(/\<\/B\>/g, '\<\/strong\>')
9248                          .replace(/\<I\>/g, '\<em\>')
9249                          .replace(/\<\/I\>/g, '\<\/em\>')
9250                          .replace(/&quot;/g, '"')
9251                          .replace(/&apos;/g, "'")
9252                          .replace(/\s+/g, ' ')
9253                          .trim();
9254                      } catch (error) {
9255                        console.warn('TEXTFORMAT conversion failed, using original caption:', error);
9256                        description = caption;
9257                      }
9258                    }
9259                    
9260                    // Prepend dateline to description if it exists (convert to uppercase)
9261                    if (dateline) {
9262                      const uppercaseDateline = dateline.toUpperCase();
9263                      description = `${uppercaseDateline} ${description}`.trim();
9264                    }
9265                    
9266                    // Apply description fixes
9267                    description = cleanDescriptionLeadingLines(description);
9268                    description = fixMultilineLinks(description);
9269                    description = convertPlainTextLinks(description);
9270                    
9271                  const imageEntry = {
9272                        // Match EXACT structure from Edition Publisher hydraImageEntry
9273                        image: originalImage.image || originalImage.urls?.original || '',
9274                        caption: description, // Use processed description as caption
9275                        description: description, // ALSO set description field (like Edition Publisher)
9276                        title: title,
9277                        'alt-text': title, // Use title as alt-text like Edition Publisher
9278                        link: originalImage.urls?.original || '',
9279                        'link-text': originalImage.urls?.original || '',
9280                        isPreview: false,
9281                        filename: originalImage.filename || '',
9282                        originalUrl: originalImage.image || originalImage.urls?.original || '',
9283                        dateline: dateline
9284                  };
9285
9286                  return imageEntry;
9287                  });
9288                  
9289                  return {
9290                    ...importGallery, // Keep import gallery structure (order, settings, etc.)
9291                    images: processedImages // Replace with processed original images
9292                  };
9293                }
9294                return importGallery;
9295              });
9296            }
9297          } else {
9298            console.warn('Could not fetch original JSON for metadata, using import data only');
9299          }
9300        } catch (error) {
9301          console.warn('Error fetching original JSON for metadata:', error);
9302        }
9303        
9304        // Extract original GUID for image path lookups
9305        // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID
9306        // GUIDs are typically 13 characters, so split on common separators
9307        let baseGuid = importGuid;
9308        const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i);
9309        if (guidMatch) {
9310          baseGuid = guidMatch[1];
9311        } else {
9312          // Fallback: try to extract first 13-character hex string
9313          const hexMatch = importGuid.match(/^([a-f0-9]{13})/i);
9314          if (hexMatch) {
9315            baseGuid = hexMatch[1];
9316          }
9317        }
9318        
9319        // Use the base GUID for image path lookups (e.g., /images/filename.jpg)
9320        classicGuid = baseGuid;
9321        console.log('Successfully loaded hybrid import data with import GUID:', importGuid);
9322        console.log('Using base GUID for image paths:', classicGuid);
9323        
9324             } else {
9325         // Standard GUID format - existing logic
9326         if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...';
9327         classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput;
9328         console.log('Detected standard GUID format, GUID:', classicGuid);
9329         
9330         const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`;
9331         console.log('Fetching classic JSON from:', jsonUrl);
9332         const response = await fetch(jsonUrl);
9333
9334         if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
9335         classicData = await response.json();
9336         
9337         if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9338           throw new Error('Fetched JSON is invalid: "galleries" array not found.');
9339         }
9340         
9341         console.log('Successfully loaded classic data with GUID:', classicGuid);
9342       }
9343
9344    } else {
9345      alert('Please either paste site JSON or enter a Classic GUID to import.');
9346      if (statusEl) statusEl.style.display = 'none';
9347      return;
9348    }
9349    
9350    const galleriesForImport = classicData.galleries;
9351    if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`;
9352
9353    // import_ format: galleries come from DB/import JSON, not classic site - treat as new Hydra NDJSON galleries
9354    const isImportFormat = !!(guidInput && guidInput.startsWith('import_'));
9355
9356    // --- HIERARCHY RECONSTRUCTION LOGIC ---
9357    
9358    const currentSiteId = window.Parameters?.siteId || siteId;
9359    let submenuParentId = parentId;
9360    const tempNewItems = [];
9361    const originalIdToNewIdMap = {};
9362
9363    // Create the optional top-level submenu if requested
9364    if (createSubmenu) {
9365        const submenuItem = {
9366            id: Date.now(),
9367            title: submenuTitle,
9368            isSubmenu: true,
9369            isCollapsed: true,
9370            visible: true,
9371            parentId: parentId,
9372            siteId: currentSiteId,
9373            children: []
9374        };
9375        galleries.push(submenuItem);
9376        submenuParentId = submenuItem.id;
9377    }
9378
9379    // --- Pass 1: Create new items with correct data and map old IDs to new IDs ---
9380    for (let index = 0; index < galleriesForImport.length; index++) {
9381        const classicItem = galleriesForImport[index];
9382        const newItemId = Date.now() + index + 100;
9383        originalIdToNewIdMap[classicItem.id] = newItemId;
9384
9385        if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`;
9386        if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`;
9387
9388        const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`;
9389        
9390 let slug;
9391        // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean.
9392        // Otherwise (for GUID import), trust the existing slug if it's there.
9393        if (pastedJson) {
9394            slug = normalizeNameForComparison(title);
9395        } else if (classicItem.slug && classicItem.slug.trim() !== '') {
9396            slug = classicItem.slug;
9397        } else {
9398            slug = normalizeNameForComparison(title);
9399        }
9400const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness
9401
9402        // Determine the GUID to use for this specific item's image paths
9403        const itemGuid = classicItem.classicGuid || classicGuid;
9404
9405     let newItem;
9406
9407        // --- Check if it's an External URL ---
9408        if (classicItem.isExternal === true && classicItem.url) {
9409            console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`);
9410            newItem = {
9411                id: newItemId, 
9412                title, 
9413                url: classicItem.url, 
9414                isExternal: true, 
9415                visible: classicItem.visible !== false, 
9416                parentId: null,
9417                siteId: currentSiteId, 
9418                slug: finalSlug,
9419                importSource: 'external-url'
9420            };
9421        }
9422        // --- Check if it's a Page and build its elements ---
9423        else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) {
9424            const pageUniqueId = `page_${newItemId}`;
9425            
9426            let pageElements = [];
9427            
9428            // =================================================================
9429            // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format
9430            // =================================================================
9431            if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) {
9432                console.log(`Using existing pageElements for page: "${title}"`);
9433                
9434                // Check if we have both pageElements and images - create two-column layout
9435                if (classicItem.images && classicItem.images.length > 0) {
9436                    console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`);
9437                    
9438                    // Create metadata element
9439                    pageElements.push({
9440                        id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9441                        metaTitle: classicItem.meta?.title || title,
9442                        metaDescription: classicItem.meta?.description || "",
9443                        metaKeywords: classicItem.meta?.keywords || ""
9444                    });
9445                    
9446                    // Create two-column container
9447                    const leftColumnElements = [];
9448                    const rightColumnElements = [];
9449                    
9450                    // Add images to left column
9451                    const slides = classicItem.images.map((img) => {
9452                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9453                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9454                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9455                        
9456                        return { 
9457                            imageUrl, 
9458                            imageKey, 
9459                            imageFilename, 
9460                            imageType: img.metadata?.mimetype || 'image/jpeg',
9461                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9462                            title: img.title || '',
9463                            byline: img.byline || '',
9464                            dateline: img.dateline || '',
9465                            altText: img.metadata?.alt_text || ''
9466                        };
9467                    });
9468                    
9469                    leftColumnElements.push({
9470                        id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0,
9471                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9472                    });
9473                    
9474                    // Add text content to right column
9475                    classicItem.pageElements.forEach((el, elIndex) => {
9476                        if (el.type === 'text' && el.content) {
9477                            rightColumnElements.push({
9478                                id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex,
9479                                textContent: el.content, textWidth: 70
9480                            });
9481                        }
9482                    });
9483                    
9484                    const columns = [
9485                        { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements },
9486                        { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }
9487                    ];
9488                    
9489                    pageElements.push({
9490                        id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1,
9491                        columns,
9492                        backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9493                        backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9494                    });
9495                    
9496                } else {
9497                    // No images, just transform the pageElements as before
9498                    pageElements = classicItem.pageElements.map((el, elIndex) => {
9499                        const elementId = Date.now() + index + 1000 + elIndex;
9500                        
9501                        // Handle import_guid.json format (type + content)
9502                        if (el.type && el.content) {
9503                            if (el.type === 'text') {
9504                                return {
9505                                    id: elementId,
9506                                    type: 'text',
9507                                    title: 'Text Block',
9508                                    visible: true,
9509                                    position: elIndex,
9510                                    textContent: el.content,
9511                                    textWidth: 100
9512                                };
9513                            } else if (el.type === 'metadata') {
9514                                return {
9515                                    id: elementId,
9516                                    type: 'metadata',
9517                                    title: 'Metadata',
9518                                    visible: true,
9519                                    position: elIndex,
9520                                    metaTitle: el.metaTitle || title,
9521                                    metaDescription: el.metaDescription || '',
9522                                    metaKeywords: el.metaKeywords || ''
9523                                };
9524                            }
9525                        }
9526                        
9527                        // Handle existing format (already has id, position, etc.)
9528                        return {
9529                            ...el,
9530                            id: elementId,
9531                            position: el.position !== undefined ? el.position : elIndex
9532                        };
9533                    });
9534                }
9535
9536            } else {
9537                // Otherwise (for GUID-based import), generate the page structure from scratch.
9538                console.log(`Generating new pageElements for page: "${title}"`);
9539                pageElements.push({
9540                  id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9541                  metaTitle: classicItem.meta?.title || title,
9542                  metaDescription: classicItem.meta?.description || "",
9543                  metaKeywords: classicItem.meta?.keywords || ""
9544                });
9545
9546                // Add images if they exist in the page
9547                if (classicItem.images && classicItem.images.length > 0) {
9548                    const slides = classicItem.images.map((img) => {
9549                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9550                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9551                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9552                        
9553                        return { 
9554                            imageUrl, 
9555                            imageKey, 
9556                            imageFilename, 
9557                            imageType: img.metadata?.mimetype || 'image/jpeg',
9558                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9559                            title: img.title || '',
9560                            byline: img.byline || '',
9561                            dateline: img.dateline || '',
9562                            altText: img.metadata?.alt_text || ''
9563                        };
9564                    });
9565                    
9566                    pageElements.push({
9567                        id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1,
9568                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true
9569                    });
9570                }
9571
9572                pageElements.push({
9573                  id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2,
9574                  textContent: "<p>&nbsp;</p><p><br></p>", textWidth: 100
9575                });
9576
9577                const columns = [];
9578                const leftColumnElements = [];
9579                const rightColumnElements = [];
9580
9581                if (classicItem.images && classicItem.images.length > 0) {
9582                  const slides = classicItem.images.map((img) => {
9583                    const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9584                    const imageKey = `${itemGuid}/images/${imageFilename}`;
9585                    const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9586                    
9587                    // Preserve all image metadata including captions, titles, etc.
9588                    return { 
9589                      imageUrl, 
9590                      imageKey, 
9591                      imageFilename, 
9592                      imageType: img.metadata?.mimetype || 'image/jpeg',
9593                      // Preserve caption information - handle both direct and language version storage patterns
9594                      caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))),
9595                      title: img.title || img.languageVersions?.[0]?.title || '',
9596                      byline: img.byline || img.languageVersions?.[0]?.byline || '',
9597                      dateline: img.dateline || img.languageVersions?.[0]?.dateline || '',
9598                      altText: img.metadata?.alt_text || '',
9599                      // Preserve any additional metadata
9600                      metaTitle: img.metadata?.meta_title || '',
9601                      metaDescription: img.metadata?.meta_description || '',
9602                      metaKeywords: img.metadata?.meta_keywords || '',
9603                      // Preserve gallery-specific data
9604                      sortOrder: img.galleryData?.sortOrder || 0,
9605                      linkURL: img.galleryData?.linkURL || null,
9606                      linkTarget: img.galleryData?.linkTarget || '_self'
9607                    };
9608                  });
9609                  leftColumnElements.push({
9610                    id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0,
9611                    slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9612                  });
9613                }
9614
9615                let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || "";
9616                if (rawDetails && typeof rawDetails === 'string') {
9617                    // =================================================================
9618                    // CHANGE 2: Fix for "Invalid regular expression flags" error.
9619                    // Changed from /.../gi literal to new RegExp('...', 'gi') constructor
9620                    // to prevent syntax errors.
9621                    // =================================================================
9622                    let processedText = rawDetails.replace(new RegExp('<TEXTFORMAT.*?>', 'gi'), '').replace(new RegExp('</TEXTFORMAT>', 'gi'), '');
9623                    processedText = processedText.replace(new RegExp('<P ALIGN=".*?"', 'gi'), '<p').replace(new RegExp('</P>', 'gi'), '</p>');
9624                    processedText = processedText.replace(new RegExp('<FONT.*?>', 'gi'), '').replace(new RegExp('</FONT>', 'gi'), '');
9625                    processedText = processedText.replace(new RegExp('<B>
9625', 'gi'), '<strong>').replace(new RegExp('</B>', 'gi'), '</strong>');
9626                    processedText = processedText.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
9627                    
9628                    // Apply description fixes to processed text
9629                    processedText = cleanDescriptionLeadingLines(processedText);
9630                    processedText = fixMultilineLinks(processedText);
9631                    processedText = convertPlainTextLinks(processedText);
9632                    
9633                    rightColumnElements.push({
9634                      id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0,
9635                      textContent: processedText, textWidth: 70
9636                    });
9637                }
9638                
9639                columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements });
9640                columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements });
9641
9642                pageElements.push({
9643                  id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2,
9644                  columns,
9645                  backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9646                  backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9647                });
9648            }
9649
9650            newItem = {
9651                id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null,
9652                siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements,
9653                classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page'
9654            };
9655                } else if (classicItem.isFolder === true) {
9656            // --- Handle folders from import_guid.json format ---
9657            console.log(`Creating folder: "${title}"`);
9658            newItem = {
9659                id: newItemId, 
9660                title, 
9661                isFolder: true, 
9662                isCollapsed: classicItem.isCollapsed !== false, 
9663                visible: classicItem.visible !== false, 
9664                parentId: null,
9665                siteId: currentSiteId, 
9666                slug: finalSlug,
9667                importSource: 'folder'
9668            };
9669        } else {
9670            // --- This is a Gallery ---
9671            const galleryPageId = generatePageId();
9672            
9673            // --- ENHANCED: Try to fetch and include actual image data with captions ---
9674            let enhancedGalleryOptions = {
9675                manualCollectionName: `GUID=${itemGuid}`, 
9676                categoryId: classicItem.categoryID, 
9677                importedFromClassic: true,
9678                siteId: currentSiteId, 
9679                pageId: galleryPageId
9680            };
9681            
9682            // --- ENHANCED: Preserve gallery display settings from Classic JSON ---
9683            if (classicItem.galleryOptions) {
9684                console.log(`Preserving gallery settings for: ${title}`);
9685                
9686                // Preserve layout and display settings
9687                const preservedSettings = [
9688                    'startInSingles', 'layoutType', 'columns', 'spacing',
9689                    'showDescription', 'showFilename', 'displayAllInfo',
9690                    'navigationMode', 'showTextBlock', 'fixedHeroImage',
9691                    'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration',
9692                    'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor',
9693                    'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity',
9694                    'lightboxCloseColor', 'lightboxArrowColor', 'useTitles',
9695                    'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign',
9696                    'showDescriptionInOverlay', 'includeRolloverImageInOverlay',
9697                    'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles',
9698                    'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames',
9699                    'openMultipleLightboxes', 'batchSize', 'autoplaySingles',
9700                    'autoplayDuration', 'autoplayTransition', 'autoplayRandomStart'
9701                ];
9702                
9703                preservedSettings.forEach(setting => {
9704                    if (classicItem.galleryOptions[setting] !== undefined) {
9705                        enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting];
9706                    }
9707                });
9708                
9709                // Preserve text content
9710                if (classicItem.galleryOptions.horizontalScrollerText) {
9711                    enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText;
9712                }
9713                
9714                // Preserve JSON collections if present
9715                if (classicItem.galleryOptions.jsonCollections) {
9716                    enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections;
9717                } else {
9718                    // Create jsonCollections structure with processed image metadata
9719                    const collectionKey = 'GUID=' + itemGuid;
9720                    const processedImages = classicItem.images || [];
9721                    
9722                    enhancedGalleryOptions.jsonCollections = {
9723                        [collectionKey]: {
9724                            isClassicCollection: true,
9725                            guid: collectionKey,
9726                            metadata: processedImages,
9727                            totalItems: processedImages.length
9728                        }
9729                    };
9730                    
9731                    console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images');
9732
9733                    // Build and try to upload NDJSON for this gallery.
9734                    // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON.
9735                    // Skip NDJSON generation for galleries with no images to avoid creating empty files
9736                    if (processedImages.length > 0) {
9737                      try {
9738                        console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)');
9739                        const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9740                        console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters');
9741                        
9742                        const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9743                      if (uploadResult && uploadResult.success) {
9744                        try {
9745                          // import_ format: treat as new Hydra NDJSON gallery (not classic-dependent)
9746                          if (isImportFormat) {
9747                            const hydraCollectionKey = 'GUID=' + currentSiteId;
9748                            enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9749                            enhancedGalleryOptions.jsonCollections = {
9750                              [hydraCollectionKey]: {
9751                                isClassicCollection: false,
9752                                guid: hydraCollectionKey,
9753                                metadata: {},
9754                                totalItems: processedImages.length
9755                              }
9756                            };
9757                            enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9758                            enhancedGalleryOptions.initialPageUuid = galleryPageId;
9759                            enhancedGalleryOptions.isHydra = true;
9760                            enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9761                            console.log('NDJSON uploaded for import_ gallery "' + title + '" - configured as Hydra NDJSON (not classic)');
9762                          } else {
9763                            enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {};
9764                            enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length;
9765                          }
9766                          
9767                          // Store dual upload metadata if Irys was successful
9768                          if (uploadResult.irysResult && uploadResult.irysResult.txId) {
9769                            enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9770                            enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing
9771                            enhancedGalleryOptions.isPerma = true;
9772                            enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo;
9773                            enhancedGalleryOptions.fileSize = uploadResult.fileSize;
9774                            enhancedGalleryOptions.siteId = currentSiteId;
9775                            enhancedGalleryOptions.pageId = galleryPageId;
9776                            console.log('✅ Dual upload complete - Irys txId:', uploadResult.irysResult.txId);
9777                            console.log('   Uploaded to:', uploadResult.uploadedTo.join(' + '));
9778                          } else {
9779                            console.log('✅ Single upload (Fly.io only) - No Irys data');
9780                          }
9781                        } catch (applyErr) {
9782                          console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr);
9783                        }
9784                      } else {
9785                        console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array');
9786                      }
9787                      } catch (e) {
9788                        console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e);
9789                      }
9790                    } else {
9791                      console.log('Skipping NDJSON generation for "' + title + '" - no images to process');
9792                    }
9793                }
9794                
9795                // Preserve additional metadata
9796                if (classicItem.galleryOptions.siteAlias) {
9797                    enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias;
9798                }
9799                if (classicItem.galleryOptions.initialPageUuid) {
9800                    enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid;
9801                }
9802                if (classicItem.galleryOptions.initialPageAlias) {
9803                    enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias;
9804                }
9805                if (classicItem.galleryOptions.timestamp) {
9806                    enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp;
9807                }
9808            } else if (isImportFormat && (classicItem.images || []).length > 0) {
9809                // import_ format without galleryOptions: still create NDJSON for new Hydra galleries
9810                const processedImages = classicItem.images || [];
9811                const collectionKey = 'GUID=' + itemGuid;
9812                enhancedGalleryOptions.jsonCollections = {
9813                  [collectionKey]: {
9814                    isClassicCollection: true,
9815                    guid: collectionKey,
9816                    metadata: processedImages,
9817                    totalItems: processedImages.length
9818                  }
9819                };
9820                console.log('Created jsonCollections for import_ gallery (no galleryOptions):', title, '(' + processedImages.length + ' images)');
9821                try {
9822                  const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9823                  const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9824                  if (uploadResult && uploadResult.success) {
9825                    const hydraCollectionKey = 'GUID=' + currentSiteId;
9826                    enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9827                    enhancedGalleryOptions.jsonCollections = {
9828                      [hydraCollectionKey]: {
9829                        isClassicCollection: false,
9830                        guid: hydraCollectionKey,
9831                        metadata: {},
9832                        totalItems: processedImages.length
9833                      }
9834                    };
9835                    enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9836                    enhancedGalleryOptions.initialPageUuid = galleryPageId;
9837                    enhancedGalleryOptions.isHydra = true;
9838                    enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9839                    if (uploadResult.irysResult?.txId) {
9840                      enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9841                      enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId;
9842                      enhancedGalleryOptions.isPerma = true;
9843                      enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo || ['fly'];
9844                    }
9845                    console.log('NDJSON uploaded for import_ gallery "' + title + '" (no galleryOptions) - configured as Hydra NDJSON');
9846                  }
9847                } catch (e) {
9848                  console.warn('NDJSON build/upload skipped for import_ gallery "' + title + '":', e);
9849                }
9850            }
9851            
9852            // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions
9853            // Image data is only included when importing from Classic GUID directly
9854            console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`);
9855            
9856            newItem = {
9857                id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null,
9858                siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid,
9859                importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug,
9860                galleryOptions: enhancedGalleryOptions
9861            };
9862        }
9863
9864        tempNewItems.push(newItem);
9865    }
9866
9867    // --- Pass 2: Link children to parents using the ID map ---
9868    tempNewItems.forEach(newItem => {
9869        const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id);
9870        const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId));
9871
9872        if (originalItem && originalItem.parentId) {
9873            const newParentId = originalIdToNewIdMap[originalItem.parentId];
9874            if (newParentId) {
9875                newItem.parentId = newParentId;
9876                const parentItem = tempNewItems.find(p => p.id === newParentId);
9877                if (parentItem) {
9878                    parentItem.isSubmenu = true;
9879                    parentItem.isCollapsed = true;
9880                }
9881            } else {
9882                newItem.parentId = submenuParentId;
9883            }
9884        } else {
9885            newItem.parentId = submenuParentId;
9886        }
9887    });
9888
9889    if (replaceMenu) {
9890      console.log("Replacing existing menu structure.");
9891      // If replacing, overwrite the entire galleries array.
9892      galleries = tempNewItems;
9893    } else {
9894      console.log("Appending to existing menu structure.");
9895      // If not replacing, push the new items to the existing array.
9896      galleries.push(...tempNewItems);
9897    }
9898
9899
9900    
9901    if (progressBar) progressBar.style.width = '90%';
9902    if (statusMessage) statusMessage.textContent = 'Saving changes...';
9903
9904    // Pass import flag to skip localStorage for large import operations
9905    await saveGalleries({ isImport: true });
9906
9907    if (progressBar) progressBar.style.width = '100%';
9908    if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`;
9909    if (statusEl) statusEl.classList.remove('importing');
9910
9911    setTimeout(() => {
9912        renderGalleries();
9913        toggleImportClassicForm();
9914        if (progressBar) progressBar.style.width = '0';
9915        if (statusEl) statusEl.style.display = 'none';
9916        if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) {
9917            window.location.reload();
9918        }
9919    }, 1000);
9920
9921  } catch (error) {
9922    console.error('Error importing classic galleries:', error);
9923    if (statusMessage) statusMessage.textContent = `Error: ${error.message}`;
9924    if (progressBar) {
9925      progressBar.style.width = '100%';
9926      progressBar.style.backgroundColor = '#dc3545';
9927    }
9928    if (statusEl) statusEl.classList.remove('importing');
9929  }
9930}
9931
9932
9933
9934/**
9935 * Improved toggleImportClassicForm - Closes other forms first
9936 */
9937function toggleImportClassicForm() {
9938  // First check if user is logged in and in edit mode
9939  if (!isAuthenticated || !isAdmin || !window.isInEditMode()) {
9940    alert('You must be logged in as an admin and in edit mode to import galleries.');
9941    return;
9942  }
9943
9944  const formId = 'importClassicForm';
9945  const form = document.getElementById(formId);
9946  
9947  if (!form) {
9948    console.error('Import form element not found');
9949    return;
9950  }
9951  
9952  // If this form is already open, just close everything
9953  if (window.currentOpenForm === formId) {
9954    closeAllForms();
9955    return;
9956  }
9957  
9958  // Otherwise close all forms and open this one
9959  closeAllForms();
9960  
9961  // Show the form
9962  form.style.display = 'block';
9963  
9964  // Use setTimeout to allow the display change to take effect before adding the class
9965  setTimeout(() => {
9966    form.classList.add('visible');
9967  }, 10);
9968
9969  // Populate parent options
9970  const parentSelect = document.getElementById('importParent');
9971  if (parentSelect) {
9972    parentSelect.innerHTML = '<option value="">Top Level (No Parent)</option>';
9973    
9974    // Add all galleries that could be parents (including submenus)
9975    galleries.forEach(gallery => {
9976      // Only include items that are not already marked as imported
9977      if (!gallery.importSource) {
9978        parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
9979      }
9980    });
9981  }
9982
9983  // Set default values
9984  const submenuTitle = document.getElementById('submenuTitle');
9985  if (submenuTitle && !submenuTitle.value) {
9986    submenuTitle.value = 'Classic Galleries';
9987  }
9988
9989  // Reset any previous import status
9990  const statusEl = document.getElementById('importStatus');
9991  if (statusEl) {
9992    statusEl.style.display = 'none';
9993    const progressBar = statusEl.querySelector('.progress-bar');
9994    if (progressBar) {
9995      progressBar.style.width = '0';
9996      progressBar.style.backgroundColor = '#4682B4'; // Reset to blue
9997    }
9998  }
9999  
10000  // Set this as the current open form
10001  window.currentOpenForm = formId;
10002}
10003
10004// Add event listener for the radio buttons to adjust height when page is selected
10005document.addEventListener('DOMContentLoaded', function() {
10006  const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]');
10007  galleryTypeRadios.forEach(radio => {
10008    radio.addEventListener('change', function() {
10009      const addForm = document.getElementById('addForm');
10010      if (addForm && addForm.classList.contains('visible')) {
10011        const isPageSelected = this.value === 'page';
10012        
10013        // Give more height for page type
10014        if (isPageSelected) {
10015          addForm.style.maxHeight = '600px';
10016        } else {
10017          // For other types, measure content height
10018          const contentHeight = addForm.scrollHeight + 30;
10019          addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px';
10020        }
10021      }
10022    });
10023  });
10024});
10025
10026
10027/**
10028 * Improved toggleStyleEditor - Closes other forms first
10029 */
10030function toggleStyleEditor(display = false, event) {
10031  const formId = 'menuStyleEditor';
10032  const overlayOpen = window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen;
10033
10034  if (overlayOpen && window.MenuStyleCustomizer._overlayMinimized && display !== false) {
10035    if (typeof window.MenuStyleCustomizer._toggleOverlayMinimized === 'function') {
10036      window.MenuStyleCustomizer._toggleOverlayMinimized(false);
10037    }
10038    return;
10039  }
10040
10041  if (display === false || window.currentOpenForm === formId || overlayOpen) {
10042    closeAllForms();
10043    return;
10044  }
10045
10046  closeAllForms();
10047
10048  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.toggleOverlay === 'function') {
10049    window.MenuStyleCustomizer.toggleOverlay();
10050  }
10051}
10052
10053
10054/**
10055 * This function closes all forms when escape key is pressed
10056 */
10057document.addEventListener('keydown', function(e) {
10058  if (e.key === 'Escape' && window.currentOpenForm) {
10059    if (closeTopmostStyleCustomizerChildOverlay()) {
10060      return;
10061    }
10062    if (window.MenuStyleCustomizer &&
10063        window.MenuStyleCustomizer._overlayOpen &&
10064        window.MenuStyleCustomizer._overlayMinimized) {
10065      return;
10066    }
10067    closeAllForms();
10068  }
10069});
10070
10071/**
10072 * This function closes all forms when clicking outside any form
10073 */
10074document.addEventListener('click', function(e) {
10075  if (!window.currentOpenForm) return;
10076
10077  if (window.MenuStyleCustomizer &&
10078      window.MenuStyleCustomizer._overlayOpen &&
10079      window.MenuStyleCustomizer._overlayMinimized) {
10080    return;
10081  }
10082
10083  // Keep Style Customizer and its nested panels open during in-overlay interactions.
10084  if (e.target.closest(
10085    '.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'
10086  )) {
10087    return;
10088  }
10089
10090  if (e.target.closest('.image-uploader-overlay')) {
10091    return;
10092  }
10093
10094  const currentForm = document.getElementById(window.currentOpenForm);
10095  if (!currentForm) {
10096    if (e.target.closest(
10097      '.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'
10098    )) {
10099      return;
10100    }
10101    closeAllForms();
10102    return;
10103  }
10104
10105  const isInsideForm = currentForm.contains(e.target);
10106  const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null;
10107
10108  if (isInsideForm || isFormToggleButton) {
10109    return;
10110  }
10111
10112  closeAllForms();
10113});
10114
10115// Override the original functions with our improved versions
10116window.toggleAddForm = toggleAddForm;
10117window.toggleStyleEditor = toggleStyleEditor;
10118window.toggleMetadataEditor = toggleMetadataEditor;
10119window.toggleSidebarElementForm = toggleSidebarElementForm;
10120window.toggleImportClassicForm = toggleImportClassicForm;
10121window.closeAllForms = closeAllForms;
10122window.closeTopmostStyleCustomizerChildOverlay = closeTopmostStyleCustomizerChildOverlay;
10123
10124
10125/**
10126 * Opens the edit form for a gallery item.
10127 * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu)
10128 * @param {number} id - The ID of the gallery item.
10129 * @param {Event} event - The click event.
10130 */
10131function editGallery(id, event) {
10132  event.stopPropagation(); // Prevent event bubbling
10133  const gallery = findGalleryById(galleries, id);
10134  const li = getGalleryTreeListItem(id, event);
10135
10136  if (gallery && li) {
10137    const showUrlField = gallery.isPage || gallery.isIntegrated;
10138    const showExternalUrlField = gallery.isExternal;
10139    const showMenuParentOption = !gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && !gallery.isExternal &&
10140      (isPageMenuItem(gallery) || gallery.isIntegrated === true);
10141    /** Sidebar meta title/description apply to gallery routes only; pages use the page metadata element. */
10142    const showSidebarSeoFields = !gallery.isPage;
10143    const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
10144    const currentExternalUrl = gallery.url || '';
10145
10146    // Clear existing content and rebuild form to ensure event listeners are fresh if needed
10147    li.innerHTML = ''; // Clear previous form if any
10148
10149    const formDiv = document.createElement('div');
10150    formDiv.className = 'edit-form';
10151    formDiv.innerHTML = `
10152        <div class="form-group">
10153           <label>Title:</label>
10154           <input type="text" value="${gallery.title || ''}" class="edit-title">
10155        </div>
10156
10157        ${showUrlField ? `
10158        <div class="form-group">
10159           <label>URL Slug:</label>
10160           <input type="text" value="${currentSlug}" class="edit-slug" placeholder="e.g., my-gallery">
10161           <small>Path after domain name (e.g., yoursite.com<b>/my-gallery</b>). Use lowercase letters, numbers, and hyphens.</small>
10162        </div>
10163        ` : ''}
10164
10165        ${showExternalUrlField ? `
10166        <div class="form-group">
10167           <label>External URL:</label>
10168           <input type="text" value="${currentExternalUrl}" class="edit-external-url" placeholder="https://example.com">
10169           <small>Full URL for the external link (e.g., https://example.com/page).</small>
10170        </div>
10171        ` : ''}
10172
10173        ${showSidebarSeoFields ? `
10174        <div class="form-group">
10175           <label>Meta Title (Optional):</label>
10176           <input type="text" value="${gallery.metaTitle || ''}" class="edit-meta-title" placeholder="Title for search results (if different from main title)">
10177           <small>Overrides site title for this item in search results/browser tabs.</small>
10178        </div>
10179
10180        <div class="form-group">
10181           <label>Meta Description (Optional):</label>
10182           <textarea class="edit-meta-description" placeholder="Description for search results (150-160 chars recommended)">${gallery.metaDescription || ''}</textarea>
10183           <small>Overrides site description for this item in search results.</small>
10184        </div>
10185        ` : `
10186        <div class="form-group">
10187           <p class="text-muted" style="margin: 0 0 8px 0; font-size: 12px; line-height: 1.4;">
10188             Search title and description for this page are set in <strong>Page → metadata</strong> (the metadata block on the page), not here.
10189           </p>
10190        </div>
10191        `}
10192
10193        <div class="form-group">
10194          <label style="display: flex; align-items: center; gap: 8px;">
10195            <input type="checkbox" class="edit-hide-menu" ${gallery.hideMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10196            Hide menu when viewing this page
10197          </label>
10198          <small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.</small>
10199        </div>
10200
10201        <div class="form-group">
10202          <label style="display: flex; align-items: center; gap: 8px;">
10203            <input type="checkbox" class="edit-overlay-menu" ${gallery.overlayMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10204            Show menu as overlay on this page
10205          </label>
10206          <small>Keeps menu items visible over a full-bleed page. Menu background is hidden. Solid menu remains in edit mode.</small>
10207        </div>
10208
10209        ${showMenuParentOption ? `
10210        <div class="form-group">
10211          <label style="display: flex; align-items: center; gap: 8px;">
10212            <input type="checkbox" class="edit-acts-as-menu-parent" ${gallery.actsAsMenuParent === true ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10213            Allow nested menu items
10214          </label>
10215          <small>Keeps this ${isPageMenuItem(gallery) ? 'page' : 'gallery'} clickable while allowing it to expand and contain sub-items.</small>
10216        </div>
10217        ` : ''}
10218
10219        <div class="form-group">
10220          <label style="display: flex; align-items: center; gap: 8px;">
10221            <input type="checkbox" class="edit-password-protected" ${gallery.passwordProtected ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10222            Password protect this page
10223          </label>
10224          <small>Visitors will need to enter a password to access this page.</small>
10225        </div>
10226
10227        <div class="form-group password-field" style="display: ${gallery.passwordProtected ? 'block' : 'none'};">
10228          <label>Password:</label>
10229          <input type="password" value="${gallery.password || ''}" class="edit-password" placeholder="Enter password">
10230          <small>Password visitors must enter to access this page.</small>
10231        </div>
10232
10233        <div class="form-group">
10234          <label style="display: flex; align-items: center; gap: 8px;">
10235            <input type="checkbox" class="edit-no-index" ${(() => {
10236              // Explicit setting takes priority;
10236 otherwise default to checked for hidden non-home pages
10237              if (gallery.noIndex === true) return 'checked';
10238              if (gallery.noIndex === false) return '';
10239              return (gallery.visible === false && !gallery.isHomePage) ? 'checked' : '';
10240            })()} style="width: auto; margin-bottom: 0;">
10241            Hide from search engines (noindex)
10242          </label>
10243          <small>When checked, search engines will not index this page. Automatically enabled for hidden pages — uncheck to allow indexing even when hidden.</small>
10244        </div>
10245
10246        <div class="edit-form-actions"> 
10247          <button class="btn btn-primary" id="saveEditBtn-${id}">Save</button> 
10248          <button class="btn" id="cancelEditBtn-${id}">Cancel</button> 
10249        </div>
10250        <br>
10251    `;
10252    li.appendChild(formDiv); // Append the new form
10253
10254    // Add event listeners programmatically
10255    const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`);
10256    if (saveButton) {
10257        saveButton.addEventListener('click', (e) => saveEdit(id, e));
10258    }
10259
10260    const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`);
10261    if (cancelButton) {
10262        cancelButton.addEventListener('click', () => renderGalleries());
10263    }
10264
10265    // Add password protection toggle functionality
10266    const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected');
10267    const passwordField = formDiv.querySelector('.password-field');
10268    if (passwordProtectedCheckbox && passwordField) {
10269        passwordProtectedCheckbox.addEventListener('change', function() {
10270            passwordField.style.display = this.checked ? 'block' : 'none';
10271        });
10272    }
10273
10274    li.classList.add('edit-mode');
10275  } else {
10276    console.error("Could not find gallery or list item for ID:", id);
10277  }
10278}
10279
10280/**
10281 * Resolve the correct sidebar #galleryTree li for an id. Horizontal dropdowns also use
10282 * li[data-id] (.horizontal-submenu-item); document.querySelector would return that node first
10283 * and break nested folder toggles in edit mode while horizontal layout is on.
10284 * @param {number} id - Gallery ID
10285 * @param {Event|null} event - Optional click event (preferred: closest li inside #galleryTree)
10286 * @returns {HTMLElement|null}
10287 */
10288function getGalleryTreeListItem(id, event) {
10289  var numericId = typeof id === 'number' ? id : parseInt(String(id), 10);
10290  if (isNaN(numericId)) return null;
10291  var galleryTreeEl = document.getElementById('galleryTree');
10292  if (event && event.target && galleryTreeEl) {
10293    var clicked = event.target.closest('li[data-id]');
10294    if (clicked && galleryTreeEl.contains(clicked)) {
10295      var cid = parseInt(clicked.getAttribute('data-id') || '', 10);
10296      if (cid === numericId) return clicked;
10297    }
10298  }
10299  if (galleryTreeEl) {
10300    var scoped = galleryTreeEl.querySelector('li[data-id="' + numericId + '"]');
10301    if (scoped) return scoped;
10302  }
10303  return document.querySelector('li[data-id="' + numericId + '"]');
10304}
10305
10306/**
10307 * Handles click on a folder item to show/hide its contents
10308 * @param {number} id - The gallery ID
10309 * @param {Event} event - The click event
10310 */
10311function toggleFolder(id, event) {
10312  // Ensure the event doesn't interfere with drag operations
10313  if (event) {
10314    event.stopPropagation();
10315  }
10316  
10317  
10318  const gallery = galleries.find(g => g.id === id);
10319  if (!gallery) {
10320    return;
10321  }
10322
10323  if (gallery.isFolder !== true && gallery.isSubmenu !== true && !isNavigableMenuParent(gallery)) {
10324    return;
10325  }
10326  
10327  
10328  // Toggle the collapsed state
10329  gallery.isCollapsed = !gallery.isCollapsed;
10330  
10331  // Find the list item in the DOM (#galleryTree first — not horizontal li.horizontal-submenu-item)
10332  const listItem = getGalleryTreeListItem(id, event);
10333  if (!listItem) {
10334    return;
10335  }
10336  
10337  
10338  // Toggle the expanded class FIRST - this is important for CSS to work correctly
10339  listItem.classList.toggle('expanded', !gallery.isCollapsed);
10340
10341  // Keep ARIA in sync for folder label + chevron (screen readers / keyboard)
10342  const folderExpanded = !gallery.isCollapsed;
10343  listItem.querySelectorAll('.menu-item.folder, .submenu-toggle').forEach((el) => {
10344    el.setAttribute('aria-expanded', folderExpanded ? 'true' : 'false');
10345  });
10346  
10347  // Force a reflow after toggling the class to ensure CSS recalculates
10348  void listItem.offsetHeight;
10349  
10350    // Find the nested list - SIMPLIFIED: Check if we're in sidebar first, then horizontal
10351    // CRITICAL: Check #galleryTree first - this is the definitive indicator of sidebar layout
10352    // Use contains() instead of closest() because closest() only checks ancestors
10353    const galleryTree = document.getElementById('galleryTree');
10354    const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10355    const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10356    // Only consider it horizontal menu if it's explicitly in .horizontal-menu
10356-container AND not in galleryTree
10357    const isInHorizontalMenu = listItem.closest('.horizontal-menu-container') && !isInGalleryTree && !isInSidebar;
10358    
10359    let nestedList = null;
10360    
10361    // If we're in sidebar (#galleryTree), ONLY look for ol.nested-sortable - ignore horizontal structures
10362    // CRITICAL: Prioritize sidebar detection - if we're in galleryTree, we're definitely in sidebar
10363    if (isInGalleryTree) {
10364      nestedList = listItem.querySelector('ol.nested-sortable');
10365      
10366      // Also check direct children as fallback
10367      if (!nestedList) {
10368        const directChildren = Array.from(listItem.children);
10369        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10370      }
10371    } else if (isInSidebar) {
10372      // Fallback: if we're in .sidebar but not in #galleryTree, still treat as sidebar
10373      nestedList = listItem.querySelector('ol.nested-sortable');
10374      if (!nestedList) {
10375        const directChildren = Array.from(listItem.children);
10376        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10377      }
10378    }
10379    
10380    // For horizontal layout, check horizontal structures (ONLY if NOT in sidebar/galleryTree)
10381    if (!nestedList && isInHorizontalMenu && !isInGalleryTree && !isInSidebar) {
10382      nestedList = listItem.querySelector('.horizontal-submenu');
10383      if (!nestedList) {
10384        nestedList = listItem.querySelector('.horizontal-dropdown');
10385      }
10386      if (!nestedList) {
10387        // For horizontal nested dropdowns, they're appended to document.body, not the list item
10388        // They're linked via data-flyout-id attribute
10389        const flyoutId = listItem.getAttribute('data-flyout-id');
10390        if (flyoutId) {
10391          // Look specifically for div.horizontal-nested-dropdown, not the list item itself
10392          const allElementsWithId = document.querySelectorAll('[data-flyout-id="' + flyoutId + '"]');
10393          
10394          // Find the actual dropdown div, not the list item
10395          const flyout = Array.from(allElementsWithId).find(el => 
10396            el.classList.contains('horizontal-nested-dropdown') && el !== listItem
10397          );
10398          
10399          
10400          if (flyout) {
10401            nestedList = flyout.querySelector('.horizontal-nested-list') || flyout;
10402          }
10403        }
10404      }
10405      if (!nestedList) {
10406        nestedList = listItem.querySelector('.horizontal-nested-dropdown');
10407      }
10408    }
10409    
10410    // Fallback: try all selectors if not found yet
10411    if (!nestedList) {
10412      nestedList = listItem.querySelector('ol.nested-sortable');
10413    }
10414    if (!nestedList) {
10415      // Debug: Log the actual DOM structure to understand what's there
10416      
10417      // Try to find any nested structure at all
10418      const anyNested = listItem.querySelector('ol, ul, div[class*="submenu"], div[class*="nested"], div[class*="dropdown"]');
10419      if (anyNested) {
10420        nestedList = anyNested; // Use it anyway
10421      }
10422      
10423      // Last resort: Check if nested list exists as a direct child but wasn't found by querySelector
10424      // This can happen if the element was removed/re-added or if there's a timing issue
10425      const directChildren = Array.from(listItem.children);
10426      const olChild = directChildren.find(child => child.tagName === 'OL');
10427      if (olChild) {
10428        nestedList = olChild;
10429      }
10430    }
10431    
10432    if (!nestedList) {
10433      
10434      // If the nested list doesn't exist, we need to create it
10435      // This can happen if children aren't rendered until the folder is opened
10436      // First try flat array
10437      let gallery = galleries.find(g => g.id === id);
10438      
10439      // If gallery not found or has no children in flat array, build tree structure to find it
10440      if (!gallery || !gallery.children || gallery.children.length === 0) {
10441        
10442        // Build tree structure from flat array
10443        const buildTree = (items, parentId = null) => {
10444          return items
10445            .filter(item => item.parentId === parentId)
10446            .map(item => ({
10447              ...item,
10448              children: buildTree(items, item.id)
10449            }));
10450        };
10451        
10452        const galleryTree = buildTree(galleries);
10453        
10454        // Recursive function to find gallery in tree
10455        const findInTree = (items) => {
10456          for (const item of items) {
10457            if (item.id === id) {
10458              return item;
10459            }
10460            if (item.children && item.children.length > 0) {
10461              const found = findInTree(item.children);
10462              if (found) return found;
10463            }
10464          }
10465          return null;
10466        };
10467        
10468        const treeGallery = findInTree(galleryTree);
10469        if (treeGallery) {
10470          gallery = treeGallery;
10471        }
10472      }
10473      
10474        if (gallery && gallery.children && gallery.children.length > 0) {
10475          
10476          // Determine nesting level from list item
10477          const currentLevel = parseInt(listItem.getAttribute('data-nesting-level') || '0', 10);
10478          const nextLevel = currentLevel + 1;
10479          
10480          // Create the nested list element
10481          nestedList = document.createElement('ol');
10482          nestedList.id = 'gallery-children-' + id;
10483          nestedList.className = 'nested-sortable';
10484          if (nextLevel > 0) {
10485            nestedList.classList.add('nested-level-' + nextLevel);
10486          }
10487          
10488          // Render the children - ensure we're in the right context (not in a horizontal dropdown wrapper)
10489          gallery.children.forEach(child => {
10490            const childHtml = renderGalleryItem(child, nextLevel, 20);
10491            const tempDiv = document.createElement('div');
10492            tempDiv.innerHTML = childHtml;
10493            const childLi = tempDiv.querySelector('li');
10494            if (childLi) {
10495              nestedList.appendChild(childLi);
10496            }
10497          });
10498          
10499          // CRITICAL: Only check for horizontal dropdowns if we're actually in a horizontal menu context
10500          // If we're in #galleryTree (sidebar), skip all horizontal menu logic
10501          const galleryTree = document.getElementById('galleryTree');
10502          const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10503          const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10504          
10505          if (!isInGalleryTree && !isInSidebar) {
10506            const isInHorizontalMenu = listItem.closest('.horizontal-menu-container');
10507            if (isInHorizontalMenu) {
10508              const horizontalDropdown = listItem.closest('.horizontal-dropdown, .horizontal-nested-dropdown');
10509              if (horizontalDropdown && horizontalDropdown !== listItem) {
10510                horizontalDropdown.style.setProperty('display', 'block', 'important');
10511                horizontalDropdown.style.setProperty('visibility', 'visible', 'important');
10512                horizontalDropdown.style.setProperty('opacity', '1', 'important');
10513              }
10514            }
10515          }
10516          
10517          // CRITICAL: Ensure list item itself is visible and has correct display
10518          listItem.style.setProperty('display', 'list-item', 'important');
10519          listItem.style.setProperty('visibility', 'visible', 'important');
10520          
10521          // CRITICAL: Ensure ALL parent elements up to the sidebar are visible
10522          // BUT: Don't make .horizontal-menu-container visible - that's the entire horizontal menu!
10523          let parent = listItem.parentElement;
10524          let depth = 0;
10525          while (parent && depth < 10) {
10526            // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10527            if (parent.classList.contains('horizontal-menu-container')) {
10528              parent = parent.parentElement;
10529              depth++;
10530              continue;
10531            }
10532            
10533            const parentStyles = window.getComputedStyle(parent);
10534            if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10535              if (parentStyles.display === 'none') {
10536                parent.style.setProperty('display', 'block', 'important');
10537              }
10538              if (parentStyles.visibility === 'hidden') {
10539                parent.style.setProperty('visibility', 'visible', 'important');
10540              }
10541              if (parentStyles.opacity === '0') {
10542                parent.style.setProperty('opacity', '1', 'important');
10543              }
10544            }
10545            // Stop at sidebar or galleryTree
10546            if (parent.classList.contains('sidebar') || parent.id === 'galleryTree' || parent.id === 'sidebar') {
10547              break;
10548            }
10549            parent = parent.parentElement;
10550            depth++;
10551          }
10552          
10553          // Append directly to the list item (not to any wrapper)
10554          // Make sure we're not appending to a hidden parent
10555          listItem.appendChild(nestedList);
10556          
10557          // Immediately set the nested list to be visible since we're opening it
10558          nestedList.style.setProperty('display', 'block', 'important');
10559          nestedList.style.setProperty('visibility', 'visible', 'important');
10560          nestedList.style.setProperty('opacity', '1', 'important');
10561          nestedList.style.setProperty('height', 'auto', 'important');
10562          nestedList.style.setProperty('max-height', 'none', 'important');
10563          
10564          // Force immediate reflow after appending
10565          void nestedList.offsetHeight;
10566          void listItem.offsetHeight;
10567        }
10568    }
10569    
10570    if (nestedList) {
10571    if (!gallery.isCollapsed) {
10572      // Show the nested list - ensure parent has expanded class FIRST
10573      // CRITICAL: Add expanded class BEFORE any style operations
10574      listItem.classList.add('expanded');
10575      
10576      // Verify expanded class was added
10577      if (!listItem.classList.contains('expanded')) {
10578        listItem.className = listItem.className + ' expanded';
10579      }
10580      
10581      // CRITICAL: Ensure parent li is visible (it might be hidden by CSS)
10582      const parentVisibility = window.getComputedStyle(listItem).visibility;
10583      if (parentVisibility === 'hidden') {
10584        listItem.style.setProperty('visibility', 'visible', 'important');
10585      }
10586      
10587      // Force a reflow to ensure CSS recalculates after adding expanded class
10588      void listItem.offsetHeight;
10589      void nestedList.offsetHeight;
10590      
10591      // Show the nested list with !important to override CSS
10592      // Set these BEFORE removing any existing inline styles to avoid flicker
10593      nestedList.style.setProperty('display', 'block', 'important');
10594      nestedList.style.setProperty('visibility', 'visible', 'important');
10595      nestedList.style.setProperty('opacity', '1', 'important');
10596      nestedList.style.setProperty('height', 'auto', 'important');
10597      nestedList.style.setProperty('max-height', 'none', 'important');
10598      nestedList.style.setProperty('overflow', 'visible', 'important');
10599      
10600      // Also ensure the nested list is not clipped by parent overflow
10601      // BUT: Don't modify .horizontal-menu-container overflow - that's the entire horizontal menu!
10602      let checkParent = nestedList.parentElement;
10603      let checkDepth = 0;
10604      while (checkParent && checkDepth < 5) {
10605        // Skip .horizontal-menu-container - we don't want to modify the entire horizontal menu
10606        if (checkParent.classList.contains('horizontal-menu-container')) {
10607          checkParent = checkParent.parentElement;
10608          checkDepth++;
10609          continue;
10610        }
10611        
10612        const parentOverflow = window.getComputedStyle(checkParent).overflow;
10613        if (parentOverflow === 'hidden') {
10614          checkParent.style.setProperty('overflow', 'visible', 'important');
10615        }
10616        if (checkParent.classList.contains('sidebar') || checkParent.id === 'galleryTree') {
10617          break;
10618        }
10619        checkParent = checkParent.parentElement;
10620        checkDepth++;
10621      }
10622      
10623      // Force another reflow after setting styles
10624      void nestedList.offsetHeight;
10625      
10626      const finalRect = nestedList.getBoundingClientRect();
10627      
10628      // If the nested list has zero dimensions, it's not actually visible
10629      if (finalRect.width === 0 || finalRect.height === 0) {
10630        // Force all parent elements to be visible
10631        // BUT: Don't modify .horizontal-menu-container - that's the entire horizontal menu!
10632        let forceParent = nestedList.parentElement;
10633        while (forceParent && forceParent !== document.body) {
10634          // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10635          if (forceParent.classList.contains('horizontal-menu-container')) {
10636            forceParent = forceParent.parentElement;
10637            continue;
10638          }
10639          
10640          forceParent.style.setProperty('display', 'block', 'important');
10641          forceParent.style.setProperty('visibility', 'visible', 'important');
10642          forceParent.style.setProperty('opacity', '1', 'important');
10643          forceParent.style.setProperty('overflow', 'visible', 'important');
10644          if (forceParent.classList.contains('sidebar') || forceParent.id === 'galleryTree') {
10645            break;
10646          }
10647          forceParent = forceParent.parentElement;
10648        }
10649        void nestedList.offsetHeight;
10650      }
10651      
10652      // Check for any parent elements that might be hiding the nested list and fix them
10653      let parent = nestedList.parentElement;
10654      let depth = 0;
10655      while (parent && depth < 5) {
10656        const parentStyles = window.getComputedStyle(parent);
10657        if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10658          
10659          // Fix hidden parent elements
10660          if (parentStyles.display === 'none') {
10661            parent.style.setProperty('display', 'block', 'important');
10662          }
10663          if (parentStyles.visibility === 'hidden') {
10664            parent.style.setProperty('visibility', 'visible', 'important');
10665          }
10666          if (parentStyles.opacity === '0') {
10667            parent.style.setProperty('opacity', '1', 'important');
10668          }
10669        }
10670        parent = parent.parentElement;
10671        depth++;
10672      }
10673      
10674    } else {
10675      // Hide the nested list
10676      nestedList.style.setProperty('display', 'none', 'important');
10677      nestedList.style.setProperty('visibility', 'hidden', 'important');
10678      nestedList.style.setProperty('opacity', '0', 'important');
10679    }
10680    
10681    // Force multiple reflows to ensure the browser applies all changes
10682    void listItem.offsetHeight;
10683    void nestedList.offsetHeight;
10684    void listItem.offsetWidth;
10685    void nestedList.offsetWidth;
10686    
10687    // Force a more aggressive repaint by temporarily toggling a class
10688    // This is especially important for sidebar layout where CSS rules might be interfer
10688ing
10689    listItem.classList.add('force-reflow');
10690    void listItem.offsetHeight;
10691    listItem.classList.remove('force-reflow');
10692    
10693    // Force reflow again after class manipulation
10694    void listItem.offsetHeight;
10695    void nestedList.offsetHeight;
10696    
10697    // Use requestAnimationFrame with multiple frames to ensure visual update
10698    // This ensures the browser has time to recalculate layout and repaint
10699    requestAnimationFrame(() => {
10700      void nestedList.offsetHeight;
10701      // Second frame to ensure all CSS has been recalculated
10702      requestAnimationFrame(() => {
10703        void nestedList.offsetHeight;
10704        // Verify the final state
10705        const finalDisplay = window.getComputedStyle(nestedList).display;
10706        const finalVisibility = window.getComputedStyle(nestedList).visibility;
10707        
10708        // If still not visible, force one more repaint cycle
10709        if (!gallery.isCollapsed && finalDisplay === 'none') {
10710          nestedList.style.setProperty('display', 'block', 'important');
10711          nestedList.style.setProperty('visibility', 'visible', 'important');
10712          void nestedList.offsetHeight;
10713          requestAnimationFrame(() => {
10714            void nestedList.offsetHeight;
10715          });
10716        }
10717      });
10718    });
10719    }
10720  
10721  // Find the submenu toggle icon and rotate it
10722  const toggleIcon = listItem.querySelector('.toggle-icon');
10723  if (toggleIcon) {
10724    toggleIcon.classList.toggle('rotated', !gallery.isCollapsed);
10725  }
10726  
10727  
10728  // Folder open/closed is a local UI preference — do not POST the full site config.
10729  try {
10730    const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}');
10731    folderStates[id] = !gallery.isCollapsed;
10732    localStorage.setItem('folder_states', JSON.stringify(folderStates));
10733  } catch (e) {
10734    console.error('Error saving folder state to localStorage:', e);
10735  }
10736}
10737
10738window.toggleFolder = toggleFolder;
10739
10740/**
10741 * Toggle password protection for a gallery/page
10742 * @param {number} id - The gallery ID
10743 * @param {Event} event - The click event
10744 */
10745function toggleGalleryPasswordProtection(id, event) {
10746  event.stopPropagation();
10747  
10748  const gallery = findGalleryById(galleries, id);
10749  if (!gallery) {
10750    console.error("Could not find gallery for password protection toggle, ID:", id);
10751    return;
10752  }
10753
10754  if (gallery.passwordProtected) {
10755    // Remove password protection
10756    if (confirm('Remove password protection from this page?')) {
10757      gallery.passwordProtected = false;
10758      delete gallery.password;
10759      saveGalleries();
10760      renderGalleries();
10761    }
10762  } else {
10763    // Add password protection
10764    const password = prompt('Enter a password for this page:');
10765    if (password && password.trim()) {
10766      gallery.passwordProtected = true;
10767      gallery.password = password.trim();
10768      saveGalleries();
10769      renderGalleries();
10770    }
10771  }
10772}
10773
10774window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection;
10775
10776function onPageMenuChromeToggle(checkbox) {
10777  if (!checkbox || !checkbox.checked) return;
10778  const root = checkbox.closest ? checkbox.closest('li') : null;
10779  if (!root) return;
10780  const siblingClass = checkbox.classList.contains('edit-hide-menu') ? '.edit-overlay-menu' : '.edit-hide-menu';
10781  const sibling = root.querySelector(siblingClass);
10782  if (sibling && sibling !== checkbox) sibling.checked = false;
10783}
10784window.onPageMenuChromeToggle = onPageMenuChromeToggle;
10785
10786/**
10787 * Saves the edited gallery item details.
10788 * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic)
10789 * @param {number} id - The ID of the gallery item.
10790 * @param {Event} event - The click event.
10791 */
10792function saveEdit(id, event) {
10793  event.stopPropagation();
10794  const li = getGalleryTreeListItem(id, event);
10795  if (!li) {
10796    console.error("Could not find list item for saveEdit, ID:", id);
10797    return;
10798  }
10799
10800  const titleInput = li.querySelector('.edit-title');
10801  const slugInput = li.querySelector('.edit-slug');
10802  const externalUrlInput = li.querySelector('.edit-external-url');
10803  const metaTitleInput = li.querySelector('.edit-meta-title');
10804  const metaDescriptionInput = li.querySelector('.edit-meta-description');
10805  const hideMenuInput = li.querySelector('.edit-hide-menu');
10806  const overlayMenuInput = li.querySelector('.edit-overlay-menu');
10807  const actsAsMenuParentInput = li.querySelector('.edit-acts-as-menu-parent');
10808  const passwordProtectedInput = li.querySelector('.edit-password-protected');
10809  const passwordInput = li.querySelector('.edit-password');
10810  const noIndexInput = li.querySelector('.edit-no-index');
10811
10812  const title = titleInput ? titleInput.value.trim() : '';
10813  const newSlugRaw = slugInput ? slugInput.value.trim() : null;
10814  const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : '';
10815  const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : '';
10816  const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : '';
10817  const hideMenu = hideMenuInput ? hideMenuInput.checked : false;
10818  const overlayMenu = overlayMenuInput ? overlayMenuInput.checked : false;
10819  const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false;
10820  const password = passwordInput ? passwordInput.value.trim() : '';
10821  // noIndex is stored explicitly so it survives visibility changes without reverting
10822  const noIndex = noIndexInput ? noIndexInput.checked : undefined;
10823
10824  const gallery = findGalleryById(galleries, id);
10825  if (!gallery) {
10826    console.error("Could not find gallery object for saveEdit, ID:", id);
10827    renderGalleries();
10828    return;
10829  }
10830
10831  if (!title && !gallery.isSpacer) {
10832    alert('Please enter a title');
10833    return;
10834  }
10835
10836  // Validate password protection
10837  if (passwordProtected && !password) {
10838    alert('Please enter a password when password protection is enabled');
10839    return;
10840  }
10841
10842  // Only mutate the opt-in property when the user explicitly changes this checkbox.
10843  // This preserves the serialized shape and behavior of every existing item.
10844  if (actsAsMenuParentInput) {
10845    const wasNavigableMenuParent = gallery.actsAsMenuParent === true;
10846    const wantsNavigableMenuParent = actsAsMenuParentInput.checked === true;
10847    if (wasNavigableMenuParent && !wantsNavigableMenuParent) {
10848      const hasNestedItems = galleries.some(item => item.parentId === gallery.id);
10849      if (hasNestedItems) {
10850        alert('Move all nested menu items out of this item before turning off “Allow nested menu items”.');
10851        return;
10852      }
10853      delete gallery.actsAsMenuParent;
10854    } else if (!wasNavigableMenuParent && wantsNavigableMenuParent) {
10855      gallery.actsAsMenuParent = true;
10856    }
10857  }
10858
10859  gallery.title = title;
10860  // Sidebar SEO fields are only shown for galleries; do not overwrite stored values when saving a page.
10861  if (!gallery.isPage) {
10862    gallery.metaTitle = metaTitle;
10863    gallery.metaDescription = metaDescription;
10864  }
10865  // Hide-menu wins if both are somehow checked; overlay is opt-in and omitted when off.
10866  if (hideMenu) {
10867    gallery.hideMenuOnPage = true;
10868    delete gallery.overlayMenuOnPage;
10869  } else {
10870    gallery.hideMenuOnPage = hideMenu;
10871    if (overlayMenu) {
10872      gallery.overlayMenuOnPage = true;
10873    } else {
10874      delete gallery.overlayMenuOnPage;
10875    }
10876  }
10877  // Persist the explicit noIndex choice so it survives future visibility toggles
10878  if (noIndex !== undefined) gallery.noIndex = noIndex;
10879
10880  // Save password protection settings
10881  gallery.passwordProtected = passwordProtected;
10882  if (passwordProtected) {
10883    gallery.password = password;
10884  } else {
10885    delete gallery.password; // Remove password if protection is disabled
10886  }
10887
10888  if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) {
10889    const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
10890    let sanitizedSlug = slugify(newSlugRaw);
10891
10892    if (!sanitizedSlug) {
10893      sanitizedSlug = slugify(gallery.title);
10894    }
10895
10896    if (sanitizedSlug !== originalSlug) {
10897      const otherGalleries = galleries.filter(g => g.id !== id);
10898      gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries);
10899      gallery.url = `/${gallery.slug}`;
10900    } else {
10901      if (gallery.url !== `/${gallery.slug}`) {
10902        gallery.url = `/${gallery.slug}`;
10903      }
10904    }
10905  }
10906
10907  // Update external URL if this is an external link
10908  if (gallery.isExternal && externalUrl) {
10909    gallery.url = externalUrl;
10910  }
10911
10912  saveGalleries();
10913  renderGalleries();
10914
10915  // If the currently active gallery was just edited, re-apply menu visibility
10916    if (gallery.id === activeGalleryId) {
10917    const bodyEl = document.body;
10918    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
10919        bodyEl.classList.add('menu-hidden-on-page');
10920    } else {
10921        bodyEl.classList.remove('menu-hidden-on-page');
10922    }
10923    applyMenuOverlayClassForGallery(gallery);
10924  }
10925}
10926// Helper function to check if a gallery is an ancestor of another
10927function hasAncestor(galleryId, ancestorId) {
10928if (galleryId === ancestorId) return true;
10929
10930const gallery = galleries.find(g => g.id === galleryId);
10931if (!gallery || !gallery.parentId) return false;
10932
10933if (gallery.parentId === ancestorId) return true;
10934
10935return hasAncestor(gallery.parentId, ancestorId);
10936}
10937
10938function deleteGallery(id, event) {
10939  event.stopPropagation();
10940  const gallery = galleries.find(g => g.id === id);
10941  const title = gallery && (gallery.title || gallery.slug || 'this item') || 'this item';
10942  if (!confirm('Are you sure you want to delete "' + title + '"?')) {
10943    return;
10944  }
10945  const overlay = showDeletionOverlay();
10946  overlay.updateMessage('Saving...');
10947  galleries = galleries.filter(g => g.id !== id);
10948  if (typeof window !== 'undefined') {
10949    window.galleries = galleries;
10950  }
10951  if (activeGalleryId === id) {
10952    activeGalleryId = null;
10953    const frame = document.getElementById('galleryFrame');
10954    if (frame) frame.src = '';
10955  }
10956  saveGalleries()
10957    .then(function(success) {
10958      renderGalleries();
10959      overlay.hide();
10960      if (success === false) {
10961        alert('Failed to delete. Please try again.');
10962      }
10963    })
10964    .catch(function(err) {
10965      overlay.hide();
10966      console.error('Error deleting item:', err);
10967      alert('Failed to delete. Please try again.');
10968    });
10969}
10970
10971function debugNestedStructure() {
10972console.group('Current Gallery Structure');
10973
10974function printGallery(gallery, level = 0) {
10975const indent = '  '.repeat(level);
10976console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`);
10977
10978if (gallery.children && gallery.children.length > 0) {
10979  gallery.children.forEach(child => printGallery(child, level + 1));
10980}
10981}
10982  // Create a tree structure for debugging
10983const galleryTree = createGalleryTree(galleries);
10984galleryTree.forEach(gallery => printGallery(gallery));
10985
10986console.groupEnd();
10987}
10988
10989function toggleHome(id, event) {
10990  event.stopPropagation();
10991  
10992  console.log('Setting home page to gallery:', id);
10993  
10994  // Find gallery
10995  const gallery = galleries.find(g => g.id === id);
10996  if (!gallery) return;
10997  
10998  // Check if this gallery is already set as home
10999  const alreadyHome = gallery.isHomePage === true;
11000  
11001  // Set all galleries to not be the home page
11002  galleries.forEach(g => {
11003    g.isHomePage = false;
11004  });
11005  
11006  // Toggle this gallery's home status - if it was already home, we're unsetting it
11007  gallery.isHomePage = !alreadyHome;
11008  
11009  // Save changes
11010  saveGalleries();
11011  
11012  // Update UI
11013  renderGalleries();
11014  
11015  // If this gallery is now the home page, display a message
11016  if (gallery.isHomePage) {
11017    const title = gallery.title || 'This page';
11018    showSuccessMessage(`${title} is now set as the Home page`);
11019  }
11020}
11021
11022// Function to toggle gallery visibility
11023function toggleVisibility(id, event) {
11024  event.stopPropagation();
11025  
11026  const gallery = galleries.find(g => g.id === id);
11027  if (gallery) {
11028    // Toggle the visibility
11029    gallery.visible = gallery.visible === false ? true : false;
11030    
11031    // Save the change to server
11032    saveGalleries();
11033    
11034    // Update UI - important to toggle the class on the button itself
11035    const toggle = event.currentTarget;
11036    if (toggle) {
11037      if (gallery.visible === false) {
11038        toggle.classList.add('hidden');
11039        
11040        // Update the SVG icon
11041        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>';
11042      } else {
11043        toggle.classList.remove('hidden');
11044        
11045        // Update the SVG icon
11046        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>';
11047      }
11048    }
11049    
11050    // Update parent row class (#galleryTree row, not horizontal submenu li)
11051    const listItem = getGalleryTreeListItem(id, event);
11052    if (listItem) {
11053      if (gallery.visible === false) {
11054        listItem.classList.add('hidden-gallery');
11055      } else {
11056        listItem.classList.remove('hidden-gallery');
11057      }
11058    }
11059    
11060    // Only clear iframe if not in edit mode
11061    if (id === activeGalleryId && gallery.visible === false && !isEditing) {
11062      document.getElementById('galleryFrame').src = '';
11063    }
11064    
11065    // Re-render menus based on current layout
11066    const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 
11067                         document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar';
11068    
11069    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
11070      window.renderHorizontalMenu();
11071    }
11072    
11073    // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied
11074    // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode
11075    if (typeof window.renderGalleries === 'function') {
11076      window.renderGalleries();
11077    } else if (typeof renderGalleries === 'function') {
11078      renderGalleries();
11079    }
11080  }
11081}
11082
11083
11084
11085/**
11086* Simplified loadGallery function that follows the same content swap pattern
11087* @param {number} id - The gallery ID
11088* @param {Event} event - Optional event object
11089*/
11090// Enhanced loadGallery function with script cleanup
11091async function loadGallery(id, event) {
11092
11093// Stop event propagation if provided; preventDefault keeps <a href> SPA navigation
11094if (event) {
11095  if (typeof event.preventDefault === 'function') event.preventDefault();
11096  event.stopPropagation();
11097}
11098
11099const galleryContainer = document.querySelector('.gallery-container');
11100if (galleryContainer) {
11101  // Apply immediate hiding styles
11102  galleryContainer.style.opacity = '0';
11103}
11104
11105console.log('Loading gallery with ID:', id);
11106
11107if (typeof dismissGalleryPasswordOverlay === 'function') {
11108  dismissGalleryPasswordOverlay();
11109}
11110
11111// Find gallery by ID
11112const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
11113
11114if (!gallery) {
11115  console.warn('No gallery found with ID:', id);
11116  if (galleryContainer) {
11117  galleryContainer.style.opacity = '1'; 
11118  } 
11119  return;
11120}
11121
11122// Check if this is a page, if so use loadPage instead
11123if (isPageMenuItem(gallery)) {
11124  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
11125  return loadPage(id, event);
11126}
11127
11128// Skip submenu items
11129if (gallery.isSubmenu) {
11130  console.log('Gallery is submenu, skipping URL update');
11131  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
11132  if (galleryContainer) {
11133  galleryContainer.style.opacity = '1';
11134  } // Show container
11135  return;
11136}
11137
11138console.log('Found gallery:', gallery.title);
11139
11140window._activeContentType = 'gallery';
11141beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
11142if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
11143  window.PageManager.clearCurrentPage();
11144}
11145
11146// CRITICAL: Set active gallery ID consistently in both global and window scope
11147console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
11148window.activeGalleryId = id;
11149activeGalleryId = id; // Ensure both variables are synchronized
11150
11151// Save reference to which gallery we're loading (for later verification)
11152window.currentLoadingGalleryId = id;
11153
11154// CRITICAL: Thorough cleanup with await to ensure completion
11155if (typeof window.removeGalleryScriptsWithPause === 'function') {
11156  await window.removeGalleryScriptsWithPause();
11157}
11158
11159
11160// Clear all existing content from gallery container
11161if (!galleryContainer) {
11162  console.error('Gallery container not found');
11163  return;
11164}
11165
11166// Force clear all content
11167galleryContainer.innerHTML = '';
11168
11169
11170  // CRITICAL: Double-check active gallery ID before updating UI
11171  if (window.activeGalleryId !== id || activeGalleryId !== id) {
11172    console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
11173    window.activeGalleryId = id;
11174    activeGalleryId = id;
11175  }
11176
11177// Apply menu visibility based on the gallery's setting and edit mode
11178const bodyEl = document.body;
11179if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
11180    bodyEl.classList.add('menu-hidden-on-page');
11181} else {
11182    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
11183}
11184applyMenuOverlayClassForGallery(gallery);
11185
11186// Update UI state using explicit window function references or local fallbacks
11187if (typeof window.updateActiveStates === 'function') {
11188  window.updateActiveStates();
11189} else if (typeof updateActiveStates === 'function') {
11190  updateActiveStates();
11191}
11192
11193if (typeof window.updateMobileTitle === 'function') {
11194  window.updateMobileTitle();
11195} else if (typeof updateMobileTitle === 'function') {
11196  updateMobileTitle();
11197}
11198
11199if (typeof window.closeMobileMenu === 'function') {
11200  window.closeMobileMenu();
11201} else if (typeof closeMobileMenu === 'function') {
11202  closeMobileMenu();
11203}
11204
11205// Update URL with gallery slug
11206if (typeof window.updateURLWithGallerySlug === 'function') {
11207  window.updateURLWithGallerySlug(gallery);
11208} else if (typeof updateURLWithGallerySlug === 'function') {
11209  updateURLWithGallerySlug(gallery);
11210}
11211
11212  // Create gallery container and load content
11213const neonGalleryContainer = document.createElement('div');
11214neonGalleryContainer.id = 'neon-gallery-container';
11215neonGalleryContainer.className = 'gallery-direct-content';
11216neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
11217neonGalleryContainer.style.width = '100%';
11218neonGalleryContainer.style.height = '100%';
11219galleryContainer.appendChild(neonGalleryContainer);
11220
11221// Add a cache-busting parameter to ensure script is freshly loaded
11222const timestamp = Date.now();
11223
11224// Set up gallery parameters
11225window.Parameters = {
11226  SiteAlias: window.location.hostname,
11227  InitialPageUuid: gallery.pageId,
11228  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
11229  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
11230  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11231  isHydra: true,
11232  galleryInstanceId: timestamp,
11233  // Added loadedGalleryId as per your original function
11234  loadedGalleryId: gallery.id
11235};
11236
11237// Debug: Check what galleryOptions contains
11238console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
11239
11240// Set up gallery config
11241window.neonGalleryConfig = {
11242  useData: true,
11243  useCDN: true,
11244  version: 'live',
11245  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
11246  layoutType: gallery.galleryOptions?.layoutType || "grid",
11247  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
11248  // This ensures that only properties from the current gallery are included, not from previous galleries
11249  ...(gallery.galleryOptions || {}),
11250  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
11251  // These fields are required for the gallery to determine if it should use Load Network racing
11252  loadTxId: gallery.galleryOptions?.loadTxId || null,
11253  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
11254  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
11255  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
11256  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
11257  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
11258  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
11259  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
11260  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11261  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
11262  galleryId: gallery.id,
11263  galleryInstanceId: timestamp
11264};
11265
11266console.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);
11267
11268// Create and add the gallery script with cache-busting
11269// Use Worker route (avoids CDN SSL issues)
11270const script = document.createElement('script');
11271const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
11272script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
11273script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
11274script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
11275console.log('Loading gallery-main script from Worker:', script.src);
11276script.onerror = function() {
11277  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
11278  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
11279  script.src = proxyUrl;
11280  script.onerror = function() {
11281    console.error('❌ Proxy failed, trying storage:', proxyUrl);
11282    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
11283  };
11284};
11285
11286// Added script.onload from your original function
11287script.onload = function() {
11288  if (window.currentLoadingGalleryId !== gallery.id) {
11289    console.warn('Gallery ID mismatch! Expected:', gallery.id,
11290                 'Current:', window.currentLoadingGalleryId);
11291  }
11292  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
11293};
11294
11295
11296if (galleryContainer) {
11297    setTimeout(() => {
11298      galleryContainer.style.opacity = '1';
11299    }, 50); // Small delay to ensure content is ready
11300  }
11301
11302console.log(`Loading fresh gallery script for: ${gallery.title}`);
11303document.body.appendChild(script);
11304}
11305
11306
11307
11308function removeGalleryScriptsWithPause() {
11309  return new Promise(resolve => {
11310    console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)');
11311    
11312    // 1. Capture and clear any gallery data in localStorage (remains the same)
11313    try {
11314      const localStorageKeys = Object.keys(localStorage);
11315      const galleryStorageKeys = localStorageKeys.filter(key => 
11316        key.includes('gallery') || 
11317        key.includes('neon') || 
11318        key.includes('lightbox')
11319      );
11320      galleryStorageKeys.forEach(key => {
11321        localStorage.removeItem(key);
11322      });
11323    } catch (e) {
11324      console.warn('Error clearing localStorage:', e);
11325    }
11326    
11327    // @ts-ignore
11328    window._galleryPageContext = null; // (remains the same)
11329    
11330    // Keep HP gallery CSS. Page-element edit/view remounts skip HP reload;
11331    // stripping neon-gallery-hp-hydra.css zeros masonry-item / container margins.
11332    const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]');
11333    galleryStyles.forEach(style => {
11334      const href = (style && style.href) ? String(style.href) : '';
11335      if (style && style.id === 'neon-gallery-css') return;
11336      if (href.indexOf('neon-gallery-hp-hydra') !== -1) return;
11337      if (style && style.parentNode) {
11338        style.parentNode.removeChild(style);
11339      }
11340    });
11341    
11342    // 4. Remove lightbox elements (more specific selectors now)
11343    // These are specific IDs and classes for lightbox components.
11344    const lightboxComponentSelectors = [
11345      '#neon-lightbox',      // Main lightbox container by ID
11346      '.neon-lightbox',     // Alternative class for main container
11347      '.lightbox-container',// General container class if used
11348      // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements
11349    ];
11350    const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', '));
11351    lightboxElements.forEach(lightboxEl => {
11352      if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) {
11353        // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className);
11354        lightboxEl.parentNode.removeChild(lightboxEl);
11355      }
11356    });
11357    
11358    // 5. Remove other gallery-specific DOM elements
11359    // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list
11360    // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body.
11361    const galleryElementSelectors = [
11362      '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay',
11363      '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu',
11364      '.neon-thumbnails', '.neon-image', '.neon-caption',
11365      '.neon-controls', '.neon-pagination',
11366      '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues.
11367      '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry'
11368      // Ensure none of these selectors inadvertently match document.body or critical layout containers
11369    ];
11370    const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', '));
11371    
11372    if (galleryElements.length > 0) {
11373      galleryElements.forEach(el => {
11374        // CRITICAL FIX: Add checks to ensure we don't remove body or html
11375        if (el && el.parentNode && el !== document.body && el !== document.documentElement) {
11376          // console.log('Removing gallery-specific DOM element:', el.id || el.className);
11377          el.parentNode.removeChild(el);
11378        } else if (el === document.body || el === document.documentElement) {
11379          console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`);
11380        }
11381      });
11382    }
11383    
11384    // 6. Find and remove ALL gallery scripts (remains the same)
11385    const scripts = document.querySelectorAll(
11386      'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11387      'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11388    );
11389    scripts.forEach(script => {
11390      if (script && script.parentNode) {
11391        // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id'));
11392        script.parentNode.removeChild(script);
11393      }
11394    });
11395
11396    // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries)
11397    const optionsScripts = document.querySelectorAll(
11398      'script[src*="galleryOptions"], script[data-options-js]'
11399    );
11400    optionsScripts.forEach(script => {
11401      if (script && script.parentNode) {
11402        console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js'));
11403        script.parentNode.removeChild(script);
11404      }
11405    });
11406
11407    // NOTE: We intentionally do NOT remove gallery options CSS here
11408    // The CSS is needed across galleries and doesn't cause conflicts like scripts do
11409
11410    setTimeout(() => {
11411      const remainingScripts = document.querySelectorAll(
11412        'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11413        'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11414      );
11415      if (remainingScripts.length > 0) {
11416        // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`);
11417      }
11418    }, 50);
11419        
11420    // 7. Reset ALL known global state variables used by the gallery (remains the same)
11421    const resetGlobals = [
11422      'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
11423      'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState',
11424      'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings',
11425      'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance',
11426      'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache',
11427    ];
11428    
11429    resetGlobals.forEach(prop => {
11430      // @ts-ignore
11431      if (window[prop] !== undefined) {
11432        // @ts-ignore
11433        window[prop] = null;
11434      }
11435    });
11436    // Keep the bootstrap wipe flag. Clearing it let a re-injected main script
11437    // replace PageManager embed config with the default "mod" collection.
11438    if (typeof window.resetNeonGalleryInitGates === 'function') {
11439      window.resetNeonGalleryInitGates('removeGalleryScriptsWithPause');
11440    }
11441
11442    // 7b. Reset gallery options panel state
11443    if (window._editorButtonVisibilityInterval) {
11444      clearInterval(window._editorButtonVisibilityInterval);
11445      window._editorButtonVisibilityInterval = null;
11446    }
11447    const editorBtn = document.getElementById('editorButton');
11448    if (editorBtn) {
11449      editorBtn.classList.add('gallery-options-force-hidden');
11450      editorBtn.style.setProperty('display', 'none', 'important');
11451      editorBtn.style.pointerEvents = '';
11452    }
11453    if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
11454      window.syncGalleryOptionsButtonVisibility();
11455    }
11456    // Reset optionsVisible flag if it exists on window (for cross-module access)
11457    // @ts-ignore
11458    if (window.optionsVisible !== undefined) {
11459      // @ts-ignore
11460      window.optionsVisible = false;
11461    }
11462    
11463    // Reset options-related globals
11464    const optionsGlobals = [
11465      'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 
11466      'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible'
11467    ];
11468    optionsGlobals.forEach(prop => {
11469      // @ts-ignore
11470      if (window[prop] !== undefined && typeof window[prop] !== 'function') {
11471        // Only reset non-function properties (like flags)
11472        // Functions should remain available
11473        // @ts-ignore
11474        if (prop === 'optionsVisible') {
11475          // @ts-ignore
11476          window[prop] = false;
11477        }
11478      }
11479    });
11480    
11481    // Reset EnhancedGallerySystem so a pending initPromise cannot hang forever
11482    // after scripts it was waiting on were just removed from the DOM.
11483    if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
11484      window.EnhancedGallerySystem.resetInitializationState();
11485    }
11486
11487    // Reset PageManager page-embedded gallery tracking
11488    window.neonGalleryScriptLoaded = false;
11489    window.pageManagerGalleryInitBlocked = false;
11490    if (window.pageManagerGalleryContainers) {
11491      window.pageManagerGalleryContainers.clear();
11492    }
11493
11494    // Remove orphaned lightbox keyboard listeners (survive script tag removal)
11495    if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
11496      window.removeAllLightboxKeyboardListeners();
11497    } else if (window.currentLightboxKeyboardListener) {
11498      document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
11499      window.currentLightboxKeyboardListener = null;
11500      window.lightboxKeyboardListenerActive = false;
11501    }
11502
11503    // Short pause before resolving
11504    setTimeout(resolve, 150);
11505  });
11506}
11507
11508
11509// Fixed clearAddForm function with proper variable names
11510function clearAddForm() {
11511// Clear the title input
11512const titleInput = document.getElementById('galleryTitle');
11513if (titleInput) {
11514titleInput.value = '';
11515}
11516
11517// Clear the URL input
11518const urlInput = document.getElementById('galleryUrl');
11519if (urlInput) {
11520urlInput.value = '';
11521}
11522
11523// Reset the parent selection
11524const parentSelect = document.getElementById('galleryParent');
11525if (parentSelect) {
11526parentSelect.value = '';
11527}
11528
11529// Reset radio buttons to Gallery (integrated) as default
11530const galleryRadio = document.querySelector('input[name="galleryType"][value="integrated"]');
11531if (galleryRadio) {
11532galleryRadio.checked = true;
11533}
11534
11535// Hide URL input container when default is Gallery
11536const urlInputContainer = document.getElementById('urlInputContainer');
11537if (urlInputContainer) {
11538urlInputContainer.style.display = 'none';
11539} else {
11540console.log('URL input container not found');
11541}
11542}
11543
11544/**
11545 * Ensures window.galleries is synchronized with the global galleries variable
11546 * Call this function after any modification to gallery settings
11547 * 
11548 * @returns {boolean} True if synchronization was successful
11549 */
11550/**
11551 * Enhanced synchronizeGalleries function that checks ALL galleries
11552 * not just imported ones, and ensures settings are preserved
11553 * @returns {boolean} Success indicator
11554 */
11555function synchronizeGalleries() {
11556  console.log('Starting enhanced gallery synchronization...');
11557  
11558  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
11559    // First check if window.galleries exists and is different from galleries
11560    const needsSync = !window.galleries || 
11561                     !Array.isArray(window.galleries) || 
11562                     window.galleries.length !== galleries.length;
11563    
11564    // ENHANCED: Check ALL galleries for options preservation, not just imported ones
11565    if (!needsSync && window.galleries.length > 0) {
11566      // Find all galleries with galleryOptions
11567      const galleriesWithOptions = galleries.filter(g => 
11568        g.galleryOptions && Object.keys(g.galleryOptions).length > 4
11569      );
11570      
11571      if (galleriesWithOptions.length > 0) {
11572        // Check ALL galleries with substantial options, not just a sample
11573        let syncNeeded = false;
11574        
11575        // Use for loop so we can break early if we find a mismatch
11576        for (let i = 0; i < galleriesWithOptions.length; i++) {
11577          const gallery = galleriesWithOptions[i];
11578          const windowGallery = window.galleries.find(g => g.id === gallery.id);
11579          
11580          if (windowGallery) {
11581            // Get options depth
11582            const galleryOptionsDepth = gallery.galleryOptions ? 
11583              Object.keys(gallery.galleryOptions).length : 0;
11584              
11585            const windowOptionsDepth = windowGallery.galleryOptions ? 
11586              Object.keys(windowGallery.galleryOptions).length : 0;
11587            
11588            // If options depth differs significantly, we need to sync
11589            if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) {
11590              console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`);
11591              console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`);
11592              
11593              syncNeeded = true;
11594              break; // No need to check more galleries
11595            }
11596            
11597            // Additional check: Look for specific important gallery settings that should be preserved
11598            // Only do this check for galleries with substantial settings
11599            if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) {
11600              // Critical keys that indicate gallery customization
11601              const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 
11602                                  'autoplaySingles', 'autoplayRandomStart', 'lightboxBgColor'];
11603              
11604              // Check if any critical keys are missing in window.galleries
11605              const missingKeys = criticalKeys.filter(key => 
11606                gallery.galleryOptions[key] !== undefined && 
11607                windowGallery.galleryOptions[key] === undefined
11608              );
11609              
11610              if (missingKeys.length > 0) {
11611                console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`);
11612                console.log(`Missing keys: ${missingKeys.join(', ')}`);
11613                
11614                syncNeeded = true;
11615                break; // No need to check more galleries
11616              }
11617            }
11618          }
11619        }
11620        
11621        // If we detected a need to sync, do it now
11622        if (syncNeeded) {
11623          // Ensure we preserve rich gallery settings and per-page custom HTML fields
11624          window.galleries = galleries.map(gallery => {
11625            const windowGallery = window.galleries.find(g => g.id === gallery.id);
11626            if (!windowGallery) return gallery;
11627
11628            let merged = { ...gallery };
11629
11630            if (windowGallery.galleryOptions && gallery.galleryOptions) {
11631              const globalOptionsDepth = Object.keys(gallery.galleryOptions).length;
11632              const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length;
11633              if (windowOptionsDepth > globalOptionsDepth) {
11634                merged = {
11635                  ...merged,
11636                  galleryOptions: {
11637                    ...windowGallery.galleryOptions,
11638                    ...gallery.galleryOptions
11639                  }
11640                };
11641              }
11642            }
11643
11644            if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11645              merged = {
11646                ...merged,
11647                customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11648                customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11649                customPageHtmlFilename: windowGallery.customPageHtmlFilename
11650              };
11651            }
11652
11653            return merged;
11654          });
11655          
11656          console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11657          return true;
11658        }
11659      }
11660    }
11661    
11662    // If we determined sync is needed (or as a precaution), update window.galleries
11663    if (needsSync || !window.galleries) {
11664      if (window.galleries && Array.isArray(window.galleries)) {
11665        galleries = galleries.map(function(gallery) {
11666          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11667          if (!windowGallery) return gallery;
11668          if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11669            return {
11670              ...gallery,
11671              customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11672              customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11673              customPageHtmlFilename: windowGallery.customPageHtmlFilename
11674            };
11675          }
11676          return gallery;
11677        });
11678      }
11679      window.galleries = [...galleries]; // Create a shallow copy to ensure different reference
11680      console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11681    } else {
11682      console.log('Gallery synchronization check: No sync needed');
11683    }
11684    
11685    // Verify that all galleries with rich settings have their full options preserved
11686    const richGalleries = galleries.filter(g => 
11687      g.galleryOptions && Object.keys(g.galleryOptions).length > 5
11688    );
11689    
11690    console.log(`Status: Found ${richGalleries.length} galleries with rich settings`);
11691    
11692    // Final verification - keep both arrays aligned without dropping page-level fields
11693    if (window.galleries !== galleries) {
11694      console.warn('References not identical after sync - merging page fields into global galleries');
11695      if (window.galleries && Array.isArray(window.galleries)) {
11696        galleries = galleries.map(function(gallery) {
11697          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11698          if (!windowGallery) return gallery;
11699          return {
11700            ...gallery,
11701            customPageHtmlEnabled: windowGallery.customPageHtmlEnabled ?? gallery.customPageHtmlEnabled,
11702            customPageHtmlR2Key: windowGallery.customPageHtmlR2Key ?? gallery.customPageHtmlR2Key,
11703            customPageHtmlFilename: windowGallery.customPageHtmlFilename ?? gallery.customPageHtmlFilename,
11704            hideMenuOnPage: windowGallery.hideMenuOnPage ?? gallery.hideMenuOnPage,
11705            overlayMenuOnPage: windowGallery.overlayMenuOnPage ?? gallery.overlayMenuOnPage
11706          };
11707        });
11708      }
11709      window.galleries = galleries;
11710      return true;
11711    }
11712    
11713    return true;
11714  } else {
11715    console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array');
11716    
11717    // If we have window.galleries but no global galleries, try to reverse-sync
11718    if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) {
11719      console.log('Attempting reverse sync: updating global galleries from window.galleries');
11720      try {
11721        // This is risky and should only happen in unusual circumstances
11722        galleries = window.galleries;
11723        return true;
11724      } catch (e) {
11725        console.error('Could not reverse-sync galleries:', e);
11726      }
11727    }
11728    
11729    return false;
11730  }
11731}
11732
11733
11734
11735/**
11736 * Loads NDJSON content from the original gallery
11737 * @param {string} siteId - The site ID
11738 * @param {string} pageId - The page ID of the original gallery
11739 * @returns {Promise<string|null>} - The NDJSON content or null if not found/error
11740 */
11741async function loadGalleryNDJSON(siteId, pageId) {
11742  console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId);
11743  
11744  if (!siteId || !pageId) {
11745    console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId });
11746    return null;
11747  }
11748
11749  try {
11750    const ndjsonFilename = `${siteId}_${pageId}.ndjson`;
11751    const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`;
11752    
11753    console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url);
11754    console.error('[NDJSON Duplication] Filename:', ndjsonFilename);
11755    
11756    const response = await fetch(url);
11757    
11758    console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText);
11759    
11760    if (!response.ok) {
11761      if (response.status === 404) {
11762        console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet');
11763        console.error('[NDJSON Duplication] Attempted URL:', url);
11764        return null;
11765      }
11766      const errorText = await response.text().catch(() => 'Could not read error text');
11767      console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText);
11768      throw new Error(`Failed to load NDJSON: ${response.statusText}`);
11769    }
11770    
11771    const ndjsonContent = await response.text();
11772    console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters');
11773    console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200));
11774    return ndjsonContent;
11775  } catch (error) {
11776    console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error);
11777    console.error('[NDJSON Duplication] Error message:', error.message);
11778    console.error('[NDJSON Duplication] Error stack:', error.stack);
11779    return null;
11780  }
11781}
11782
11783/**
11784 * Duplicates and publishes NDJSON for a gallery
11785 * @param {Object} originalGallery - The original gallery object
11786 * @param {Object} newGallery - The new gallery object with new pageId
11787 * @returns {Promise<boolean>} - Success indicator
11788 */
11789async function duplicateGalleryNDJSON(originalGallery, newGallery) {
11790  console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON =====');
11791  console.error('[NDJSON Duplication] Original gallery:', {
11792    id: originalGallery.id,
11793    title: originalGallery.title,
11794    pageId: originalGallery.pageId,
11795    classicGuid: originalGallery.classicGuid,
11796    hasGalleryOptions: !!originalGallery.galleryOptions,
11797    manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11798    isClassicCollection: originalGallery.galleryOptions?.isClassicCollection
11799  });
11800  console.error('[NDJSON Duplication] New gallery:', {
11801    id: newGallery.id,
11802    title: newGallery.title,
11803    pageId: newGallery.pageId
11804  });
11805  
11806  try {
11807    // Check if this is an NDJSON-based gallery (not classic collection)
11808    // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files
11809    // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON)
11810    // Having a pageId means it uses NDJSON files, so we should duplicate them
11811    const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true;
11812    
11813    console.error('[NDJSON Duplication] Classic collection check:', {
11814      hasClassicGuid: !!originalGallery.classicGuid,
11815      manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11816      startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('G
11816UID='),
11817      isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection,
11818      hasPageId: !!originalGallery.pageId,
11819      isClassicCollection: isClassicCollection,
11820      willSkip: isClassicCollection && !originalGallery.pageId
11821    });
11822    
11823    // Only skip if explicitly marked as classic collection AND has no pageId
11824    // If it has a pageId, it uses NDJSON files and we should duplicate them
11825    if (isClassicCollection && !originalGallery.pageId) {
11826      console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication');
11827      return true; // Not an error, just not applicable
11828    }
11829
11830    // Check if original gallery has a pageId
11831    if (!originalGallery.pageId) {
11832      console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication');
11833      console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery));
11834      return true; // Not an error, just not applicable
11835    }
11836
11837    // Get siteId
11838    const siteId = window.siteId || originalGallery.siteId;
11839    console.error('[NDJSON Duplication] SiteId resolution:', {
11840      windowSiteId: window.siteId,
11841      gallerySiteId: originalGallery.siteId,
11842      resolvedSiteId: siteId
11843    });
11844    
11845    if (!siteId) {
11846      console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId');
11847      console.error('[NDJSON Duplication] window.siteId:', window.siteId);
11848      console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId);
11849      return false;
11850    }
11851
11852    // Load original NDJSON
11853    console.error('[NDJSON Duplication] Loading original NDJSON for duplication...');
11854    console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId);
11855    const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId);
11856    
11857    if (!originalNdjson) {
11858      console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet');
11859      console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found');
11860      return false;
11861    }
11862
11863    // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher
11864    console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId);
11865    const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`;
11866    console.error('[NDJSON Duplication] New filename:', dataFileName);
11867    console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length);
11868    
11869    try {
11870      // Hydra sites require Bearer auth (same token resolution as Edition Publisher)
11871      const publishHeaders = { 'Content-Type': 'application/json' };
11872      let authToken = null;
11873      try {
11874        if (window.TokenManager) {
11875          if (typeof window.TokenManager.getHydraAuthToken === 'function') {
11876            try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
11877          }
11878          if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
11879            try {
11880              const tokenWithBearer = window.TokenManager.getBestToken();
11881              authToken = tokenWithBearer && tokenWithBearer.startsWith('Bearer ')
11882                ? tokenWithBearer.substring(7)
11883                : tokenWithBearer;
11884            } catch (e) {}
11885          }
11886        }
11887        if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
11888        if (!authToken) authToken = window.authToken || window.didToken;
11889        if (authToken) {
11890          publishHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
11891            ? 'Bearer ' + authToken
11892            : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
11893          console.error('[NDJSON Duplication] Auth token attached for publish');
11894        } else {
11895          console.error('[NDJSON Duplication] WARNING: No auth token — publish will likely fail with 401');
11896        }
11897      } catch (authErr) {
11898        console.error('[NDJSON Duplication] Auth token lookup failed:', authErr.message);
11899      }
11900
11901      // Call the publish endpoint (same as edition publisher)
11902      console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish');
11903      const response = await fetch('https://hydra-press-v2.fly.dev/publish', {
11904        method: 'POST',
11905        headers: publishHeaders,
11906        body: JSON.stringify({
11907          ndjsonContent: originalNdjson,
11908          dataFileName: dataFileName
11909        }),
11910      });
11911      
11912      console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText);
11913      console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries()));
11914      
11915      if (!response.ok) {
11916        const errorText = await response.text().catch(() => 'Unknown error');
11917        console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText);
11918        console.error('[NDJSON Duplication] Response status:', response.status);
11919        console.error('[NDJSON Duplication] Response statusText:', response.statusText);
11920        return false;
11921      }
11922      
11923      const result = await response.json();
11924      console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON');
11925      console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2));
11926      console.error('[NDJSON Duplication] Publish result URLs:', result.urls);
11927      
11928      // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions
11929      // with txId/permaURL here. If Irys upload is needed, it would need to be done separately.
11930      
11931      console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY =====');
11932      return true;
11933    } catch (error) {
11934      console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error);
11935      console.error('[NDJSON Duplication] Error message:', error.message);
11936      console.error('[NDJSON Duplication] Error stack:', error.stack);
11937      return false;
11938    }
11939  } catch (error) {
11940    console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error);
11941    console.error('[NDJSON Duplication] Error message:', error.message);
11942    console.error('[NDJSON Duplication] Error stack:', error.stack);
11943    return false;
11944  }
11945}
11946
11947/**
11948 * Creates and shows a loading overlay for gallery deletion (same pattern as duplication).
11949 * @returns {Object} Object with hide() and updateMessage() to control the overlay
11950 */
11951function showDeletionOverlay() {
11952    const existingOverlay = document.getElementById('deletion-overlay');
11953    if (existingOverlay) existingOverlay.remove();
11954    if (!document.querySelector('#deletion-overlay-styles')) {
11955        const style = document.createElement('style');
11956        style.id = 'deletion-overlay-styles';
11957        style.textContent = `
11958            #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; }
11959            #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; }
11960            #deletion-overlay .deletion-spinner { width: 80px; height: 80px; color: #444; }
11961        `;
11962        document.head.appendChild(style);
11963    }
11964    const overlayEl = document.createElement('div');
11965    overlayEl.id = 'deletion-overlay';
11966    const messageEl = document.createElement('div');
11967    messageEl.className = 'deletion-message';
11968    const spinnerEl = document.createElement('div');
11969    spinnerEl.className = 'deletion-spinner';
11970    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>`;
11971    const textEl = document.createElement('div');
11972    textEl.textContent = 'Deleting...';
11973    textEl.style.textAlign = 'center';
11974    messageEl.appendChild(spinnerEl);
11975    messageEl.appendChild(textEl);
11976    overlayEl.appendChild(messageEl);
11977    document.body.appendChild(overlayEl);
11978    return {
11979        hide: function() { if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl); },
11980        updateMessage: function(msg) { textEl.textContent = msg; }
11981    };
11982}
11983
11984/**
11985 * Creates and shows a loading overlay for gallery duplication
11986 * @returns {Object} Object with hide function to remove the overlay
11987 */
11988function showDuplicationOverlay() {
11989    // Remove any existing overlay first
11990    const existingOverlay = document.getElementById('duplication-overlay');
11991    if (existingOverlay) {
11992        existingOverlay.remove();
11993    }
11994    
11995    // Add styles if not already present
11996    if (!document.querySelector('#duplication-overlay-styles')) {
11997        const style = document.createElement('style');
11998        style.id = 'duplication-overlay-styles';
11999        style.textContent = `
12000            #duplication-overlay {
12001                position: fixed;
12002                top: 0;
12003                left: 0;
12004                right: 0;
12005                bottom: 0;
12006                background: rgba(0, 0, 0, 0.5);
12007                z-index: 99999999;
12008                display: flex;
12009                align-items: center;
12010                justify-content: center;
12011                cursor: not-allowed;
12012                pointer-events: all;
12013            }
12014            #duplication-overlay .duplication-message {
12015                background: rgba(255, 255, 255, 0.95);
12016                padding: 30px 50px;
12017                font-family: Arial, sans-serif;
12018                font-size: 16px;
12019                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
12020                display: flex;
12021                flex-direction: column;
12022                align-items: center;
12023                gap: 20px;
12024            }
12025            #duplication-overlay .duplication-spinner {
12026                width: 80px;
12027                height: 80px;
12028                color: #444444;
12029            }
12030        `;
12031        document.head.appendChild(style);
12032    }
12033    
12034    const overlay = document.createElement('div');
12035    overlay.id = 'duplication-overlay';
12036    
12037    const message = document.createElement('div');
12038    message.className = 'duplication-message';
12039    
12040    const spinner = document.createElement('div');
12041    spinner.className = 'duplication-spinner';
12042    spinner.innerHTML = `
12043        <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
12044          <defs>
12045            <filter id="svgSpinnersGooeyBalls20">
12046              <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
12047              <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"/>
12048              <feBlend in="SourceGraphic" in2="z"/>
12049            </filter>
12050          </defs>
12051          <g filter="url(#svgSpinnersGooeyBalls20)">
12052            <circle cx="5" cy="12" r="4" fill="currentColor">
12053              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
12054            </circle>
12055            <circle cx="19" cy="12" r="4" fill="currentColor">
12056              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
12057            </circle>
12058            <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
12059          </g>
12060        </svg>
12061    `;
12062    
12063    const text = document.createElement('div');
12064    text.textContent = 'Duplicating gallery...';
12065    text.style.textAlign = 'center';
12066    
12067    message.appendChild(spinner);
12068    message.appendChild(text);
12069    overlay.appendChild(message);
12070    document.body.appendChild(overlay);
12071    
12072    return {
12073        hide: function() {
12074            if (overlay && overlay.parentNode) {
12075                overlay.parentNode.removeChild(overlay);
12076            }
12077        },
12078        updateMessage: function(newMessage) {
12079            text.textContent = newMessage;
12080        }
12081    };
12082}
12083
12084/**
12085 * Duplicates a gallery or page item.
12086 * @param {number} id - The ID of the item to duplicate.
12087 * @param {Event} event - The click event.
12088 */
12089async function duplicateGalleryItem(id, event) {
12090    event.stopPropagation();
12091    console.log(`Duplicating item with ID: ${id}`);
12092
12093    // Show loading overlay
12094    const overlay = showDuplicationOverlay();
12095
12096    // Find the original item in the global galleries array
12097    const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find
12098
12099    if (!originalItem) {
12100        console.error(`Cannot duplicate: Item with ID ${id} not found.`);
12101        overlay.hide();
12102        alert('Error: Could not find the item to duplicate.');
12103        return;
12104    }
12105
12106    if (isPageMenuItem(originalItem)) {
12107        overlay.updateMessage('Duplicating page...');
12108    }
12109
12110    try {
12111        // --- Create a Deep Copy ---
12112        // Using JSON.parse/stringify is a common way for simple object structures
12113        // Be cautious if your objects have Dates, functions, Maps, Sets, etc.
12114        const newItem = JSON.parse(JSON.stringify(originalItem));
12115
12116        // --- Modify Copied Item ---
12117        newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID
12118        newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title
12119
12120        // Generate new pageId if it's a page or integrated gallery
12121        if (newItem.isPage || newItem.isIntegrated) {
12122            newItem.pageId = generatePageId(); // Assign a new unique pageId
12123        }
12124
12125        // Generate new slug and URL
12126        const newBaseSlug = slugify(newItem.title); // Use the new title
12127        newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness
12128        newItem.url = `/${newItem.slug}`; // Update URL based on new slug
12129
12130        // Reset children (don't duplicate children for simplicity)
12131        newItem.children = [];
12132
12133        // Deep clone galleryOptions and pageElements, update pageId if needed
12134        if (newItem.galleryOptions) {
12135            newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions));
12136            // Update pageId within options if it exists and matches the old one
12137            if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) {
12138                newItem.galleryOptions.pageId = newItem.pageId;
12139            }
12140            // Clear permanent-storage pointers so duplicate loads its own Tigris NDJSON, not the original
12141            ['txId', 'permaURL', 'loadTxId', 'loadPermaURL', 'loadNetworkUrl', 'r2StorageUrl',
12142             'cdnNdjsonId', 'cdnNdjsonUrl', 'cdnNdjsonStableUrl', 'embedPointerUrl', 'cdnSaveTimestamp',
12143             'flyIoSaveTimestamp', 'loadNetworkSaveTimestamp', 'irysSaveTimestamp', 'timestampsMatch', 'isPerma']
12144              .forEach(function(field) { delete newItem.galleryOptions[field]; });
12145        }
12146        if (newItem.pageElements) {
12147            newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements));
12148             // Give new IDs to duplicated page elements
12149             newItem.pageElements.forEach(el => {
12150                 el.id = Date.now() + Math.floor(Math.random() * 10000);
12151             });
12152        }
12153
12154        // Reset home page status
12155        newItem.isHomePage = false;
12156
12157        // --- Duplicate NDJSON if applicable (galleries only; pages copy content via pageElements) ---
12158        const isDuplicatingPage = isPageMenuItem(originalItem);
12159        console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...', {
12160            isDuplicatingPage,
12161            newItemPageId: newItem.pageId,
12162            originalItemPageId: originalItem.pageId
12163        });
12164
12165        if (!isDuplicatingPage && newItem.pageId && originalItem.pageId) {
12166            overlay.updateMessage('Duplicating gallery files...');
12167            console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...');
12168            const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem);
12169            if (!ndjsonDuplicated) {
12170                console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication');
12171                console.error('[Gallery Duplication] User can republish the gallery later if needed');
12172                alert('Gallery menu item was created, but gallery images could not be copied. Open Edition Publisher on this gallery and publish to load images.');
12173                // Don't block the duplication if NDJSON fails - user can republish later
12174            } else {
12175                console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully');
12176            }
12177        } else {
12178            console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', {
12179                isDuplicatingPage,
12180                hasNewPageId: !!newItem.pageId,
12181                hasOriginalPageId: !!originalItem.pageId,
12182                reason: isDuplicatingPage ? 'Page items use pageElements, not NDJSON' :
12183                    !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId'
12184            });
12185        }
12186
12187        overlay.updateMessage(isDuplicatingPage ? 'Saving duplicated page...' : 'Saving duplicated gallery...');
12188
12189        // --- Insert into Galleries Array ---
12190        // Find the index of the original item
12191        const originalIndex = galleries.findIndex(item => item.id === id);
12192
12193        if (originalIndex > -1) {
12194            // Insert the new item right after the original
12195            galleries.splice(originalIndex + 1, 0, newItem);
12196            console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`);
12197
12198             // Update positions for all items after the insertion point
12199             for (let i = originalIndex + 1; i < galleries.length; i++) {
12200                 galleries[i].position = i; // Re-assign position based on new index
12201             }
12202
12203        } else {
12204            // Fallback: Add to the end if original couldn't be found (shouldn't happen)
12205            console.warn(`Original item ${id} not found in array, adding duplicate to the end.`);
12206            newItem.position = galleries.length;
12207            galleries.push(newItem);
12208        }
12209
12210        console.log('Rendering galleries immediately after duplication...');
12211        renderGalleries();
12212        // Update active states if necessary (though unlikely needed here)
12213        updateActiveStates();
12214
12215        // --- Save and Refresh ---
12216        console.log('Saving duplicated item...');
12217        saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh
12218            .then(success => {
12219                overlay.hide(); // Hide overlay before page refresh
12220                if (success) {
12221                    console.log('Duplicate saved successfully. Page will refresh.');
12222                    // No need to call renderGalleries() here because saveGalleries will trigger a reload.
12223                } else {
12224                    console.error('Failed to save the duplicated item.');
12225                    alert('Error: Could not save the duplicated item.');
12226                    // Optionally try to revert the galleries array change here if save fails
12227                }
12228            })
12229            .catch(error => {
12230                 overlay.hide(); // Hide overlay on error
12231                 console.error('Error saving duplicated item:', error);
12232                 alert('Error saving duplicated item.');
12233            });
12234
12235    } catch (error) {
12236        overlay.hide(); // Hide overlay on error
12237        console.error('Error duplicating item:', error);
12238        alert('An error occurred while duplicating the item.');
12239    }
12240}
12241
12242/**
12243 * saveGalleries function that always saves FULL data
12244 * Ensures gallery settings are preserved in both localStorage and server
12245 * 
12246 * @param {Object} customData - Optional custom data to save instead of galleries
12247 * @returns {Promise<boolean>} - Success indicator
12248 */
12249
12250function attachLiveSidebarElements(data) {
12251  if (!data || typeof data !== 'object') return data;
12252  var hasGetter = !!(window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function');
12253  if (hasGetter) {
12254    var live = window.SidebarManager.getElementsForSave();
12255    if (Array.isArray(live) && live.length > 0) {
12256      data.sidebarElements = live;
12257      return data;
12258    }
12259    delete data.sidebarElements;
12260    return data;
12261  }
12262  if (Array.isArray(data.sidebarElements) && data.sidebarElements.length === 0) {
12263    delete data.sidebarElements;
12264  }
12265  return data;
12266}
12267
12268async function saveGalleries(customData) {
12269  if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') {
12270    alert('You must be logged in as an admin to save changes');
12271    return false;
12272  }
12273
12274  if (window.HelpSystem && typeof window.HelpSystem.isSandboxActive === 'function' && window.HelpSystem.isSandboxActive()) {
12275    console.warn('Save blocked: help sandbox is active');
12276    return false;
12277  }
12278
12279  function _hasHelpDemoInTree(items) {
12280    if (!Array.isArray(items)) return false;
12281    for (var i = 0; i < items.length; i++) {
12282      var g = items[i];
12283      if (!g) continue;
12284      if (g.isHelpDemo || Number(g.id) < 0) return true;
12285      if (g.children && _hasHelpDemoInTree(g.children)) return true;
12286    }
12287    return false;
12288  }
12289
12290  if (window.activeGalleryId != null && Number(window.activeGalleryId) < 0) {
12291    console.warn('Save blocked: active help demo gallery');
12292    return false;
12293  }
12294
12295  var savePayload = customData && customData.galleries ? customData.galleries : galleries;
12296  if (_hasHelpDemoInTree(savePayload)) {
12297    console.warn('Save blocked: payload contains help demo records');
12298    return false;
12299  }
12300
12301  // Prevent multiple simultaneous saves — queue the latest payload instead of dropping it
12302  if (window._saveInProgress) {
12303    console.log('Save already in progress, queueing latest payload');
12304    if (customData) {
12305      window._pendingSaveGalleriesData = attachLiveSidebarElements(customData);
12306    }
12307    return new Promise(function(resolve) {
12308      window._pendingSaveResolvers = window._pendingSaveResolvers || [];
12309      window._pendingSaveResolvers.push(resolve);
12310    });
12311  }
12312
12313  window._saveInProgress = true;
12314
12315  try {
12316    // CRITICAL: Synchronize galleries before saving to ensure we use the latest data
12317    if (typeof synchronizeGalleries === 'function') {
12318      synchronizeGalleries();
12319      console.log('Galleries synchronized before saving');
12320    }
12321
12322    // Try to get a valid token from multiple sources
12323    let token = null;
12324    let tokenSource = 'none';
12325    
12326    // First try the global didToken (traditional approach)
12327    if (didToken) {
12328      console.log('Using global didToken for save operation');
12329      token = didToken;
12330      tokenSource = 'didToken';
12331    } 
12332    // Next try TokenManager if available
12333    else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
12334      token = window.TokenManager.getToken();
12335      
12336      // Remove Bearer prefix if present (we'll add it later)
12337      if (token && token.startsWith('Bearer ')) {
12338        token = token.substring(7);
12339      }
12340      
12341      console.log('Using TokenManager token for save operation, length:', token ? token.length : 0);
12342      tokenSource = 'TokenManager';
12343    }
12344    // Fallback to localStorage
12345    else if (localStorage.getItem('hydra_auth_token')) {
12346      token = localStorage.getItem('hydra_auth_token');
12347      console.log('Using localStorage token for save operation, length:', token.length);
12348      tokenSource = 'localStorage';
12349    }
12350    // Last resort: try to get a fresh token
12351    else if (magic && magic.user) {
12352      try {
12353        console.log('Attempting to get fresh token from Magic');
12354        token = await magic.user.getIdToken();
12355        console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed');
12356        tokenSource = token ? 'magic' : 'none';
12357      } catch (e) {
12358        console.error('Error getting token from Magic:', e);
12359        tokenSource = 'magic_error';
12360      }
12361    }
12362    
12363    // [AUTH_SAVE] Log for S+J download - token state and logged-in email before request
12364    const loggedInEmail = (window.userMetadata && window.userMetadata.email) ||
12365      localStorage.getItem('hydra_auth_email') ||
12366      '';
12367    console.log('[AUTH_SAVE] Token resolved:', {
12368      loggedInEmail: loggedInEmail || '(none)',
12369      tokenSource,
12370      tokenLength: token ? token.length : 0,
12371      tokenPrefix: token ? token.substring(0, 15) + '...' : '(none)',
12372      hasToken: !!token,
12373      isAuthenticated,
12374      hydraIsAdmin: localStorage.getItem('hydra_is_admin') === 'true',
12375      hasLocalStorageToken: !!localStorage.getItem('hydra_auth_token'),
12376      hasTokenManager: !!(window.TokenManager && typeof window.TokenManager.getToken === 'function')
12377    });
12378    
12379    if (!token) {
12380      console.error('[AUTH_SAVE] No token available - save will fail. Sources checked: didToken, TokenManager, localStorage, magic.');
12381      throw new Error('Could not find a valid authentication token. Please log in again.');
12382    }
12383
12384    // IMPORTANT: Try to save to localStorage with isolated error handling
12385    // This ensures localStorage failures don't prevent server saves
12386    if (!customData) { // Only try localStorage for regular gallery saves
12387      try {
12388        // FIXED: Always save the FULL galleries data to localStorage
12389        localStorage.setItem('galleries', JSON.stringify(galleries));
12390        console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`);
12391      } catch (storageError) {
12392        // Now storage errors are isolated and non-fatal
12393        console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError);
12394      }
12395    } else if (customData && customData.isImport) {
12396      // Skip localStorage for import operations - data too large
12397      console.log('Skipping localStorage for import operation - data too large for local storage');
12398    }
12399
12400    // IMPORTANT: Ensure all pages have proper URLs and slugs before saving
12401    galleries.forEach(gallery => {
12402      if (gallery.isPage === true) {
12403        const pageSlug = slugify(gallery.title);
12404        
12405        // Make sure slug and URL are set correctly
12406        if (!gallery.slug || gallery.slug === '') {
12407          gallery.slug = pageSlug;
12408        }
12409        
12410        // Make sure URL points to slug
12411        if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) {
12412          gallery.url = `/${pageSlug}`;
12413        }
12414      }
12415    });
12416
12417    // Set up the data to save - ensure we're using the complete data
12418    let siteData;
12419    console.log('saveGalleries: customData received:', JSON.stringify(customData));
12420    console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata);
12421    console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title);
12422    console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics);
12423    if (customData) {
12424      // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar
12425      // If customData contains galleries (which it should for sidebar-initiated saves),
12426      // merge them with any existing settings to ensure nothing is lost
12427      if (customData.galleries && Array.isArray(customData.galleries)) {
12428        // First, ensure we preserve all current gallery settings from the existing galleries
12429        const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || [];
12430        
12431        // Create merged galleries array with preserved settings
12432        const mergedGalleries = customData.galleries.map(gallery => {
12433          const existingGallery = existingGalleries.find(g => g.id === gallery.id);
12434          
12435          // If this gallery exists in the current dataset, ensure all galleryOptions are preserved
12436          if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) {
12437            // Calculate settings depth
12438            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12439            const serverOptionsDepth = Object.keys(gallery.galleryOptions).length;
12440            
12441            // If server has fewer settings, merge them with existing settings
12442            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12443              console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12444              
12445              // Create a merged gallery with preserved settings
12446              return {
12447                ...gallery,
12448                galleryOptions: {
12449                  ...existingGallery.galleryOptions, // Start with all existing settings
12450                  ...gallery.galleryOptions    // Override with any new server settings
12451                }
12452              };
12453            }
12454          }
12455          
12456          // If no preservation needed, use server gallery as is
12457          return gallery;
12458        });
12459        
12460        // Use the merged galleries in customData
12461        siteData = {
12462          ...customData,
12463          galleries: mergedGalleries
12464        };
12465        
12466        console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`);
12467        console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData));
12468        console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata);
12469        console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title);
12470        console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics);
12471      } else {
12472        siteData = customData;
12473      }
12474    } else {
12475      const galleriesForSave = (typeof window !== 'undefined' && window.galleries && Array.isArray(window.galleries))
12476        ? window.galleries
12477        : galleries;
12478      siteData = {
12479        galleries: galleriesForSave
12480      };
12481      
12482      if (window.siteMetadata) {
12483        siteData.siteMetadata = window.siteMetadata;
12484      }
12485
12486      if (window.SidebarManager) {
12487        const liveSidebar = typeof window.SidebarManager.getElementsForSave === 'function'
12488          ? window.SidebarManager.getElementsForSave()
12489          : null;
12490        if (Array.isArray(liveSidebar) && liveSidebar.length > 0) {
12491          siteData.sidebarElements = liveSidebar;
12492        }
12493      }
12494    }
12495
12496    attachLiveSidebarElements(siteData);
12497
12498    // Ensure galleries are never saved as pages: any item with galleryOptions is a gallery and must persist with isPage false.
12499    // (loadPage can set isPage=true in memory; sidebar/full saves would otherwise persist that and convert TEAM etc. to pages.)
12500    if (siteData.galleries && Array.isArray(siteData.galleries)) {
12501      siteData.galleries = siteData.galleries.map(g => {
12502        if (g && g.galleryOptions && typeof g.galleryOptions === 'object') {
12503          return { ...g, isPage: false };
12504        }
12505        return g;
12506      });
12507    }
12508
12509    // Get the save URL
12510    const saveUrl = typeof getApiUrl === 'function' ? 
12511      getApiUrl('/api/save-config') : 
12512      '/api/save-config';
12513    
12514    // Get user email from various sources
12515    const userEmail = (window.userMetadata && window.userMetadata.email) || 
12516                     localStorage.getItem('hydra_auth_email') || 
12517                     '';
12518    
12519    // Ensure token has proper format
12520    const formattedToken = token.startsWith('hydra:') ? token :
12521                          (token.includes('.') ? token : `hydra:${token}`);
12522    
12523    console.log('Making server save request...');
12524    console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData));
12525    console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata);
12526    console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title);
12527    console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics);
12528    
12529    // [AUTH_SAVE] Log for S+J download - request details (no full token)
12530    console.log('[AUTH_SAVE] Sending save request:', {
12531      saveUrl,
12532      authHeaderPrefix: `Bearer ${formattedToken.substring(0, 20)}...`,
12533      userEmail: userEmail || '(empty)',
12534      tokenSource,
12535      formattedTokenLength: formattedToken.length
12536    });
12537    
12538    // Make the request
12539    let response = await fetch(saveUrl, {
12540      method: 'POST',
12541      headers: {
12542        'Content-Type': 'application/json',
12543        'Authorization': `Bearer ${formattedToken}`,
12544        'X-User-Email': userEmail,
12545        'X-API-Request': 'true' // Always include this header for API requests
12546      },
12547      body: JSON.stringify(siteData)
12548    });
12549
12550    console.log('Save response status:', response.status);
12551    
12552    // Process the response
12553    let responseText = await response.text();
12554    console.log('Server response text (first 500 chars):', responseText.substring(0, 500));
12555    let responseData;
12556    
12557    try {
12558      responseData = JSON.parse(responseText);
12559        console.log('Server response parsed successfully:', responseData);
12560        if (responseData.debug) {
12561          console.log('Server debug info:', responseData.debug);
12562          console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys);
12563          console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata);
12564          console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata);
12565          console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata);
12566          console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics);
12567        }
12568    } catch (jsonError) {
12569      if (response.status === 200 && responseText.trim().startsWith('<!DOCTYPE html>')) {
12570        responseData = { success: true };
12571      } else {
12572        throw new Error('Invalid response format: ' + responseText.substring(0, 100));
12573      }
12574    }
12575
12576    if (!response.ok && !responseData.success) {
12577      // [AUTH_SAVE] Log for S+J download - capture full 401/auth failure details
12578      const authSaveFailure = {
12579        status: response.status,
12580        statusText: response.statusText,
12581        error: responseData && responseData.error,
12582        details: responseData && responseData.details,
12583        authMethod: responseData && responseData.authMethod,
12584        fullResponse: responseData,
12585        responseTextPreview: responseText.substring(0, 300)
12586      };
12587      console.error('[AUTH_SAVE] Save request failed:', authSaveFailure);
12588      
12589      // Retry once on 401: token may have expired - try refresh or fresh Magic token
12590      if (response.status === 401) {
12591        console.log('[AUTH_SAVE] 401 received - attempting token refresh before retry...');
12592        let newToken = null;
12593        if (window.TokenManager && typeof window.TokenManager.refreshToken === 'function') {
12594          newToken = await window.TokenManager.refreshToken();
12595        }
12596        if (!newToken && magic && magic.user) {
12597          try {
12598            newToken = await magic.user.getIdToken();
12599          } catch (e) {
12600            console.warn('[AUTH_SAVE] Magic getIdToken failed:', e);
12601          }
12602        }
12603        if (newToken) {
12604          if (newToken.startsWith('Bearer ')) newToken = newToken.substring(7);
12605          token = newToken;
12606          didToken = newToken;
12607          localStorage.setItem('hydra_auth_token', newToken);
12608          if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12609            window.TokenManager.storeToken(newToken.startsWith('hydra:') ? newToken : `hydra:${newToken}`, userEmail);
12610          }
12611          console.log('[AUTH_SAVE] Got fresh token, retrying save...');
12612          const retryResponse = await fetch(saveUrl, {
12613            method: 'POST',
12614            headers: {
12615              'Content-Type': 'application/json',
12616              'Authorization': `Bearer ${token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`)}`,
12617              'X-User-Email': userEmail,
12618              'X-API-Request': 'true'
12619            },
12620            body: JSON.stringify(siteData)
12621          });
12622          const retryText = await retryResponse.text();
12623          let retryData;
12624          try { retryData = JSON.parse(retryText); } catch (e) {
12625            if (retryResponse.status === 200 && retryText.startsWith('<!DOCTYPE')) retryData = { success: true };
12626            else throw new Error('Invalid retry response');
12627          }
12628          if (retryResponse.ok || retryData.success) {
12629            response = retryResponse;
12630            responseText = retryText;
12631            responseData = retryData;
12632            console.log('[AUTH_SAVE] Retry succeeded');
12633          } else {
12634            console.error('[AUTH_SAVE] Retry also failed:', retryResponse.status, retryData);
12635            throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12636          }
12637        } else {
12638          throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12639        }
12640      } else {
12641        throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12642      }
12643    }
12644
12645    console.log('Configuration saved successfully');
12646
12647    // Update local lastKnownConfigTimestamp after successful save
12648    if (responseData.lastModified) {
12649      window.lastKnownConfigTimestamp = responseData.lastModified;
12650      console.error('[CONFLICT DETECT] ✅ Updated local config timestamp from save response:', window.lastKnownConfigTimestamp);
12651    } else {
12652      console.error('[CONFLICT DETECT] ⚠️ No lastModified in save response, fetching from server...');
12653      // If server doesn't return timestamp, fetch it
12654      try {
12655        const timestampUrl = typeof getApiUrl === 'function' ? 
12656          getApiUrl('/api/config-timestamp') : 
12657          '/api/config-timestamp';
12658        const timestampResponse = await fetch(timestampUrl);
12659        if (timestampResponse.ok) {
12660          const timestampData = await timestampResponse.json();
12661          if (timestampData.lastModified) {
12662            window.lastKnownConfigTimestamp = timestampData.lastModified;
12663            console.error('[CONFLICT DETECT] ✅ Fetched and updated local config timestamp:', window.lastKnownConfigTimestamp);
12664          } else {
12665            console.error('[CONFLICT DETECT] ⚠️ No lastModified in fetched timestamp data:', timestampData);
12666          }
12667        } else {
12668          console.error('[CONFLICT DETECT] ⚠️ Failed to fetch timestamp after save, status:', timestampResponse.status);
12669        }
12670      } catch (err) {
12671        console.error('[CONFLICT DETECT] ⚠️ Could not fetch timestamp after save:', err);
12672      }
12673    }
12674
12675    // Store new token if provided
12676    if (responseData.hydraToken) {
12677      console.log('Received new hydra token from server');
12678      
12679      // Update the global didToken
12680      didToken = responseData.hydraToken.startsWith('hydra:') ? 
12681        responseData.hydraToken.substring(6) : responseData.hydraToken;
12682      
12683      // Update localStorage
12684      localStorage.setItem('hydra_auth_token', didToken);
12685      
12686      // Update TokenManager if available
12687      if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12688        window.TokenManager.storeToken(responseData.hydraToken, userEmail);
12689      }
12690    }
12691
12692    // Process server response data if needed
12693    if (responseData.galleries) {
12694      // If server returns updated galleries data, update the global galleries variable
12695      if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) {
12696        // CRITICAL FIX: Before replacing galleries with server response,
12697        // preserve any gallery settings that might be lost in the response
12698        const updatedGalleries = responseData.galleries.map(serverGallery => {
12699          // Find the matching gallery in our current data
12700          const existingGallery = galleries.find(g => g.id === serverGallery.id);
12701          
12702          // Check if we need to preserve gallery settings
12703          if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) {
12704            // Calculate settings depth
12705            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12706            const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length;
12707            
12708            // If server has fewer settings, merge them with existing settings
12709            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12710              console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12711              
12712              // Create a merged gallery with preserved settings
12713              return {
12714                ...serverGallery,
12715                galleryOptions: {
12716                  ...existingGallery.galleryOptions, // Start with all existing settings
12717                  ...serverGallery.galleryOptions    // Override with any new server settings
12718                }
12719              };
12720            }
12721          }
12722          
12723          // If no preservation needed, use server gallery as is
12724          return serverGallery;
12725        });
12726        
12727        // Update the galleries variable with our preserved data
12728        galleries = updatedGalleries;
12729        console.log(`Updated galleries from server response: ${galleries.length} galleries`);
12730        
12731        // IMPORTANT: Synchronize again after receiving server response
12732        if (typeof synchronizeGalleries === 'function') {
12733          synchronizeGalleries();
12734          console.log('Galleries re-synchronized after server response');
12735        }
12736        
12737        // Keep sidebar UI in sync with current galleries (e.g. after delete)
12738        if (typeof renderGalleries === 'function') {
12739          renderGalleries();
12740        }
12741        
12742        // Update localStorage with full data
12743        try {
12744          localStorage.setItem('galleries', JSON.stringify(galleries));
12745          console.log('Updated localStorage with server gallery data (FULL format)');
12746        } catch (e) {
12747          console.warn('Could not update localStorage with server gallery data', e);
12748        }
12749      }
12750    }
12751
12752    // CRITICAL FIX: Clear all sessionStorage caches after successful save
12753    // This ensures that when users open the site in a new tab, they get fresh data
12754    try {
12755      console.log('Clearing sessionStorage caches after successful save...');
12756      const keysToRemove = [];
12757      for (let i = 0; i < sessionStorage.length; i++) {
12758        const key = sessionStorage.key(i);
12759        if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) {
12760          keysToRemove.push(key);
12761        }
12762      }
12763      keysToRemove.forEach(key => {
12764        sessionStorage.removeItem(key);
12765        console.log('Cleared sessionStorage key: ' + key);
12766      });
12767      console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries');
12768    } catch (cacheError) {
12769      console.warn('Error clearing sessionStorage cache:', cacheError);
12770      // Don't fail the save if cache clearing fails
12771    }
12772
12773    window._saveInProgress = false;
12774
12775    if (!customData || !Array.isArray(customData.sidebarElements)) {
12776      document.dispatchEvent(new CustomEvent('sidebar-save-requested'));
12777    }
12778
12779    // Flush any save queued while this request was in flight
12780    if (window._pendingSaveGalleriesData) {
12781      const pending = window._pendingSaveGalleriesData;
12782      window._pendingSaveGalleriesData = null;
12783      const resolvers = window._pendingSaveResolvers || [];
12784      window._pendingSaveResolvers = [];
12785      return saveGalleries(pending).then(function(result) {
12786        resolvers.forEach(function(r) { r(result); });
12787        return result;
12788      });
12789    }
12790
12791    const idleResolvers = window._pendingSaveResolvers || [];
12792    window._pendingSaveResolvers = [];
12793    idleResolvers.forEach(function(r) { r(true); });
12794
12795    return true;
12796  } catch (error) {
12797    // [AUTH_SAVE] Log for S+J download - capture exception (network, parse, or thrown after 401)
12798    console.error('[AUTH_SAVE] Save threw exception:', {
12799      message: error.message,
12800      name: error.name,
12801      stack: error.stack
12802    });
12803    console.error('Error saving configuration:', error);
12804    alert('Failed to save changes: ' + error.message);
12805    window._saveInProgress = false;
12806    const failResolvers = window._pendingSaveResolvers || [];
12807    window._pendingSaveResolvers = [];
12808    failResolvers.forEach(function(r) { r(false); });
12809    return false;
12810  }
12811}
12812
12813// Helper function to get API URL for preview sites
12814function getApiUrl(endpoint) {
12815  // Check if we're on a preview URL
12816  const isPreviewUrl = window.location.hostname === 'preview.neonsky.app';
12817  
12818  if (isPreviewUrl) {
12819    // Extract the site GUID from the path (first segment after domain)
12820    const pathParts = window.location.pathname.split('/').filter(Boolean);
12821    if (pathParts.length > 0) {
12822      const siteGuid = pathParts[0];
12823      // Include the site GUID in the API URL
12824      return `/${siteGuid}${endpoint}`;
12825    }
12826  }
12827  
12828  // Regular case - return the endpoint as is
12829  return endpoint;
12830}
12831
12832function updateActiveStates() {
12833  // Get the most current active gallery ID, ensuring consistency between both variables
12834  const currentActiveId = window.activeGalleryId || activeGalleryId;
12835  
12836  // Ensure both variables are synchronized
12837  window.activeGalleryId = currentActiveId;
12838  activeGalleryId = currentActiveId;
12839  
12840  console.log("updateActiveStates: Using active gallery ID:", currentActiveId);
12841  
12842  // Special case for invisible home page - don't try to highlight it in the menu
12843  const activeGallery = galleries.find(g => g.id === currentActiveId);
12844  if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) {
12845    console.log("Active gallery is invisible home page - not highlighting in menu");
12846    // We're not returning here, so it will still update the visibility classes
12847    // but won't try to find an element to mark as active
12848  }
12849  
12850  // Fix selector issue - look for both .tree and .sidebar
12851  document.querySelectorAll('.tree li, .sidebar li').forEach(li => {
12852    if (!li.dataset.id) return; // Skip items without data-id attribute
12853    
12854    const id = parseInt(li.dataset.id);
12855    if (isNaN(id)) return; // Skip if ID is not a number
12856    
12857    const gallery = galleries.find(g => g.id === id);
12858    
12859    // Update active state using the most current ID
12860    const isActive = id === currentActiveId;
12861    
12862    // Add or remove the active class
12863    if (isActive) {
12864      li.classList.add('active');
12865      console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`);
12866    } else {
12867      li.classList.remove('active');
12868    }
12869    
12870    // Update visibility class if gallery exists
12871    if (gallery) {
12872      li.classList.toggle('hidden-gallery', gallery.visible === false);
12873      
12874      // Update visibility toggle icon if it exists
12875      const toggleButton = li.querySelector('.visibility-toggle');
12876      if (toggleButton) {
12877        toggleButton.classList.toggle('hidden', gallery.visible === false);
12878      }
12879    }
12880  });
12881  
12882  // Verify that the active state was actually set for the current activeGalleryId
12883  const activeElements = document.querySelectorAll('.active');
12884  if (activeElements.length === 0 && currentActiveId) {
12885    // Only log a warning if we're not dealing with an invisible home page
12886    const activeGallery = galleries.find(g => g.id === currentActiveId);
12887    if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) {
12888      console.warn(`No active elements found after update for ID: ${currentActiveId}`);
12889    }
12890  }
12891  
12892  // Log active items for debugging
12893  console.log("Active gallery ID after update:", currentActiveId);
12894  activeElements.forEach(el => 
12895    console.log("Active element:", el.textContent.trim())
12896  );
12897}
12898
12899
12900function debugGalleryStructure() {
12901console.group('Gallery Structure Debug');
12902
12903// Log the raw data structure
12904console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries)));
12905
12906// Log the tree structure
12907const treeStructure = createGalleryTree(galleries);
12908
12909// Helper to print the tree
12910function printTree(items, level = 0) {
12911const indent = '  '.repeat(level);
12912items.forEach(item => {
12913  console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`);
12914  if (item.children && item.children.length > 0) {
12915    printTree(item.children, level + 1);
12916  }
12917});
12918}
12919
12920console.log('Tree structure:');
12921printTree(treeStructure);
12922
12923// Log the DOM structure
12924console.log('DOM structure:');
12925const treeEl = document.getElementById('galleryTree');
12926console.log(treeEl);
12927
12928console.groupEnd();
12929}
12930
12931function createGalleryTree(galleries) {
12932console.log("Creating gallery tree with preservation safeguards...");
12933
12934// First, create a map of galleries by id for quick lookup
12935const galleryMap = {};
12936const processedIds = new Set();
12937
12938// First pass: Create clean copies without children
12939galleries.forEach(gallery => {
12940if (!gallery || !gallery.id) return; // Skip invalid galleries
12941
12942// Create a copy of the gallery object without the children field
12943const cleanGallery = { ...gallery };
12944delete cleanGallery.children;
12945
12946// Initialize an empty children array
12947cleanGallery.children = [];
12948
12949// Add to map - even if we've seen this ID before, keep the latest version
12950if (processedIds.has(gallery.id)) {
12951  console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`);
12952}
12953
12954processedIds.add(gallery.id);
12955galleryMap[gallery.id] = cleanGallery;
12956});
12957
12958// Second pass: Assign children to their parents
12959const rootGalleries = [];
12960
12961// To detect already-processed children
12962const childrenAssigned = new Set();
12963
12964galleries.forEach(gallery => {
12965if (!gallery || !gallery.id) return; // Skip invalid galleries
12966
12967const galleryId = gallery.id;
12968
12969// Skip if this gallery isn't in our map (could be invalid)
12970if (!galleryMap[galleryId]) return;
12971
12972// Skip if we've already assigned this gallery as a child
12973if (childrenAssigned.has(galleryId)) return;
12974
12975if (gallery.parentId && galleryMap[gallery.parentId]) {
12976  // This is a child gallery, but first check it doesn't create a circular reference
12977  
12978  // Find all parents in the chain up to the root
12979  let currentParent = galleryMap[gallery.parentId];
12980  let safeToAdd = true;
12981  let chainIds = [galleryId, gallery.parentId];
12982  
12983  while (currentParent.parentId) {
12984    // If we've seen this ID in the chain, it would create a circular reference
12985    if (chainIds.includes(currentParent.parentId)) {
12986      console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`);
12987      safeToAdd = false;
12988      break;
12989    }
12990    
12991    chainIds.push(currentParent.parentId);
12992    currentParent = galleryMap[currentParent.parentId];
12993    
12994    // If parent doesn't exist in the map, break the chain
12995    if (!currentParent) break;
12996  }
12997  
12998  if (safeToAdd) {
12999    // Safe to add as child
13000    galleryMap[gallery.parentId].children.push(galleryMap[galleryId]);
13001    childrenAssigned.add(galleryId);
13002  } else {
13003    // Not safe - make it a root gallery
13004    rootGalleries.push(galleryMap[galleryId]);
13005  }
13006} else {
13007  // This is a root gallery (no parent or parent doesn't exist)
13008  if (!childrenAssigned.has(galleryId)) {
13009    rootGalleries.push(galleryMap[galleryId]);
13010  }
13011}
13012});
13013
13014// Final verification
13015const allGalleryIds = new Set(galleries.map(g => g.id));
13016const treeGalleryIds = new Set();
13017
13018// Function to collect all IDs in the tree
13019const collectIds = (items) => {
13020items.forEach(item => {
13021  treeGalleryIds.add(item.id);
13022  if (item.children && item.children.length > 0) {
13023    collectIds(item.children);
13024  }
13025});
13026};
13027
13028collectIds(rootGalleries);
13029
13030// Check if any galleries are missing from the tree
13031const missingIds = [];
13032allGalleryIds.forEach(id => {
13033if (!treeGalleryIds.has(id)) {
13034  missingIds.push(id);
13035}
13036});
13037
13038if (missingIds.length > 0) {
13039console.warn(`${missingIds.length} galleries are missing from the tree structure`);
13040
13041// Add the missing galleries as root items
13042missingIds.forEach(id => {
13043  const gallery = galleryMap[id];
13044  if (gallery && !childrenAssigned.has(id)) {
13045    console.log(`Adding missing gallery to root: ${gallery.title}`);
13046    rootGalleries.push(gallery);
13047  }
13048});
13049}
13050
13051console.log(`Created tree with ${rootGalleries.length} root galleries`);
13052return rootGalleries;
13053}
13054
13055function alignFolderStates() {
13056  console.log("Aligning folder data states with collapsed visual appearance");
13057  let changed = 0;
13058  
13059  galleries.forEach(gallery => {
13060    if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) {
13061      gallery.isCollapsed = true;
13062      changed++;
13063    }
13064  });
13065  
13066}
13067
13068
13069
13070function renderGalleryItem(gallery, level = 0, maxLevel = 10) {
13071  // Prevent infinite recursion by limiting depth
13072  if (level >= maxLevel) {
13073    console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`);
13074    return '';
13075  }
13076
13077  // Add debug info to console
13078  console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`);
13079
13080  // Skip hidden items in view mode (but not spacers)
13081  if (!isEditing && gallery.visible === false && !gallery.isSpacer) {
13082    return '';
13083  }
13084  
13085const isPage = isPageMenuItem(gallery);
13086const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu
13087const isNavigableParent = isNavigableMenuParent(gallery);
13088const isSpacer = gallery.isSpacer === true;
13089const isExternal = gallery.isExternal === true;
13090  
13091// CRITICAL FIX: Ensure children are always populated for folders
13092// If gallery.children is missing or empty, but this is a folder, build children from flat galleries array
13093// This ensures the nested <ol> is always rendered with children in the DOM, even in view mode
13094if (isFolder) {
13095  // Always ensure children array exists
13096  if (!gallery.children) {
13097    gallery.children = [];
13098  }
13099  // If children array is empty, build it from flat galleries array using parentId
13100  if (gallery.children.length === 0) {
13101    const childrenFromFlat = galleries.filter(g => g.parentId === gallery.id);
13102    if (childrenFromFlat.length > 0) {
13103      gallery.children = childrenFromFlat;
13104      console.log(`[renderGalleryItem] Built children for folder ${gallery.title}: ${gallery.children.length} children`);
13105    }
13106  }
13107}
13108
13109// Parent-capable items need their chevron and empty nested drop zone immediately,
13110// before the first child has been added.
13111const hasChildren = isFolder || isNavigableParent || gallery.children?.length > 0;
13112const hasChildrenClass = hasChildren ? 'has-children' : '';
13113
13114// Standard collapsed state check
13115const isCollapsed = gallery.isCollapsed === true;
13116
13117// Simple expanded class logic - if the folder is not collapsed, add 'expanded' class
13118const isExpandedClass = (isFolder || isNavigableParent) && !isCollapsed ? 'expanded' : '';
13119
13120// Add additional classes for special types
13121const spacerClass = isSpacer ? 'spacer-item' : '';
13122const externalClass = isExternal ? 'external-link' : '';
13123const folderClass = isFolder ? 'folder-item' : '';
13124const navigableParentClass = isNavigableParent ? 'navigable-parent-item' : '';
13125  
13126        const duplicateIconSvg = `
13127    <svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" width="24" height="24" viewBox="0 0 24 24">
13128      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
13129        <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"/>
13130        <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"/>
13131      </g>
13132    </svg>
13133  `;
13134
13135
13136  
13137  // NEW: Add nesting level as a data attribute for styling
13138  const nestingLevelAttr = `data-nesting-level="${level}"`;
13139
13140  // Accessible href for SPA items (loadPage/loadGallery still preventDefault)
13141  const menuHref = gallery.isHomePage
13142    ? '/'
13143    : (gallery.url && String(gallery.url).startsWith('/')
13144        ? gallery.url
13145        : (gallery.slug ? ('/' + gallery.slug) : '#'));
13146  const childrenListId = 'gallery-children-' + gallery.id;
13147  const folderExpandedAttr = !isCollapsed ? 'true' : 'false';
13148  const ariaCurrentAttr = (gallery.id === activeGalleryId && !isFolder && !isSpacer)
13149    ? ' aria-current="page"'
13150    : '';
13151
13152  // Create the gallery item HTML with enhanced nesting indicators
13153  const html = `
13154<li data-id="${gallery.id}" 
13155    ${nestingLevelAttr}
13156    class="${gallery.id === activeGalleryId ? 'active' : ''} 
13157          ${gallery.visible === false ? 'hidden-gallery' : ''} 
13158          ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass} ${navigableParentClass}">
13159  <div class="gallery-item-content nested-sortable-handle">
13160    ${hasChildren ? `
13161      <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;')}">
13162        <svg class="icon toggle-icon ${!isCollapsed ? 'rotated' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
13163          <polyline points="9 6 15 12 9 18"></polyline>
13164        </svg>
13165      </button>
13166    ` : ''}
13167    
13168    <!-- Menu item with different rendering based on type -->
13169    ${isSpacer ? `
13170      <!-- Spacer with non-breaking space to ensure height -->
13171      <span class="menu-item spacer" aria-hidden="true">&nbsp;</span>
13172    ` : isExternal ? `
13173      <!-- External URL uses an anchor tag with target="_blank" -->
13174      <a href="${gallery.url}" class="menu-item external-link" target="_blank" rel="noopener noreferrer"${ariaCurrentAttr}>
13175        ${gallery.title}
13176      </a>
13177    ` : isFolder ? `
13178      <!-- Folder item with toggle functionality (button for keyboard / SR) -->
13179      <button type="button" class="menu-item folder" data-folder="true"
13180            onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}">
13181        ${gallery.title}
13182      </button>
13183    ` : `
13184      <!-- Real link for SR/keyboard; SPA handlers keep click behavior -->
13185      <a href="${menuHref}" class="menu-item" onclick="${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}"${ariaCurrentAttr}>
13186        ${gallery.title}
13187      </a>
13188    `}
13189    
13190<div class="controls">
13191      ${!isSpacer ? `
13192      <button class="btn visibility-toggle ${gallery.visible === false ? 'hidden' : ''}" onclick="toggleVisibility(${gallery.id}, event)">
13193        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13194          ${gallery.visible !== false ? 
13195            '<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>' :
13196            '<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>'
13197          }
13198        </svg>
13199      </button>
13200      
13201      <button class="btn password-toggle ${gallery.passwordProtected ? 'protected' : ''}" onclick="toggleGalleryPasswordProtection(${gallery.id}, event)" title="${gallery.passwordProtected ? 'Remove Password Protection' : 'Add Password Protection'}">
13202        <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24">
13203          ${gallery.passwordProtected ? 
13204            '<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>' :
13205            '<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"/>'
13206          }
13207        </svg>
13208      </button>
13209      
13210      <button class="btn edit-btn" onclick="editGallery(${gallery.id}, event)">
13211        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13212          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
13213          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
13214        </svg>
13215      </button>
13216      ` : ''}
13217
13218        <button class="btn duplicate-btn" onclick="duplicateGalleryItem(${gallery.id}, event)" title="Duplicate">
13219         ${duplicateIconSvg}
13220      </button>
13221
13222      
13223      <button class="btn delete-btn" onclick="deleteGallery(${gallery.id}, event)">
13224        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13225          <polyline points="3 6 5 6 21 6"></polyline>
13226          <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>
13227        </svg>
13228      </button>
13229${!isSpacer ? `
13230<!-- Add home toggle button -->
13231<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'}">
13232  <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13233    ${gallery.isHomePage ? 
13234      '<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"/>' :
13235      '<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>'
13236    }
13237  </svg>
13238</button>
13239` : ''}
13240    </div>
13241  </div>
13242  <ol id="${childrenListId}" class="nested-sortable ${level > 0 ? 'nested-level-' + level : ''}">
13243    ${gallery.children && gallery.children.length > 0 ? 
13244      gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''}
13245  </ol>
13246</li>
13247`;
13248
13249  return html;
13250}
13251
13252// Additional helper function to add visual indicators during drag operations
13253function addDragVisualFeedback() {
13254  // Add event listener for drag operations
13255  document.addEventListener('dragover', function(e) {
13256    // Find the closest nested sortable list
13257    const nearestList = e.target.closest('.nested-sortable');
13258    if (!nearestList) return;
13259    
13260    // Remove active-drop-target class from all lists
13261    document.querySelectorAll('.active-drop-target').forEach(el => {
13262      el.classList.remove('active-drop-target');
13263    });
13264    
13265    // Add it to the current list
13266    nearestList.classList.add('active-drop-target');
13267    
13268    // Calculate if this would be a nested position
13269    // This is a simplified calculation - SortableJS has its own logic,
13270    // but this helps provide additional visual feedback
13271    const rect = nearestList.getBoundingClientRect();
13272    const distanceFromLeft = e.clientX - rect.left;
13273    
13274    // If we're close to the left edge of a nested list, add a class
13275    if (distanceFromLeft < 30) {
13276      nearestList.classList.add('potential-parent');
13277    } else {
13278      nearestList.classList.remove('potential-parent');
13279    }
13280  });
13281  
13282  // Remove classes when drag ends
13283  document.addEventListener('dragend', function() {
13284    document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => {
13285      el.classList.remove('active-drop-target', 'potential-parent');
13286    });
13287  });
13288}
13289
13290// Make sure the addDragVisualFeedback function is called when the document is ready
13291document.addEventListener('DOMContentLoaded', function() {
13292  // Initialize the visual feedback helper
13293  addDragVisualFeedback();
13294  
13295  // If we're in edit mode, make sure the nested sortables are initialized
13296  if (document.body.classList.contains('edit-mode-active')) {
13297    setTimeout(function() {
13298      initializeNestedSortables();
13299    }, 300);
13300  }
13301});
13302
13303// Update the toggleEditMode function to ensure proper initialization of sortables
13304const originalToggleEditMode = window.toggleEditMode;
13305window.toggleEditMode = function() {
13306    originalToggleEditMode.apply(this, arguments); // Call original function
13307
13308    stopAutoAdvanceTimer();
13309    
13310    const sidebar = document.querySelector('.sidebar');
13311    const editControls = document.querySelector('.edit-controls');
13312
13313    if (currentMenuLayout === 'horizontal') {
13314        if (document.body.classList.contains('edit-mode-active')) {
13315            // In horizontal layout and edit mode is ON: show sidebar and edit controls
13316            if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container
13317            if(editControls) editControls.style.display = 'flex';
13318             // Re-render the sidebar menu for editing
13319            renderGalleries();
13320        } else {
13321            // In horizontal layout and edit mode is OFF: hide sidebar
13322            if(sidebar) sidebar.style.display = 'none';
13323             // Re-render the horizontal menu for viewing
13324            renderHorizontalMenu();
13325        }
13326    } else {
13327        // For other layouts (sidebar, top), ensure sidebar is visible
13328        if(sidebar) sidebar.style.display = 'block'; // Or 'flex'
13329    }
13330};
13331
13332function verifyGalleryStructure() {
13333console.log("Verifying gallery structure integrity...");
13334
13335// Count number of galleries
13336const galleryCount = galleries.length;
13337console.log(`Total galleries in flat array: ${galleryCount}`);
13338
13339// Create tree and count galleries in tree
13340const tree = createGalleryTree([...galleries]);
13341let treeCount = 0;
13342
13343function countGalleriesInTree(items) {
13344items.forEach(item => {
13345  treeCount++;
13346  if (item.children && item.children.length > 0) {
13347    countGalleriesInTree(item.children);
13348  }
13349});
13350}
13351
13352countGalleriesInTree(tree);
13353console.log(`Total galleries in tree structure: ${treeCount}`);
13354
13355if (treeCount !== galleryCount) {
13356console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`);
13357return false;
13358} else {
13359console.log("Gallery structure is consistent");
13360return true;
13361}
13362}
13363
13364function renderGalleries() {
13365console.log("Rendering galleries...");
13366
13367  // Align with the explicit edit toggle only (not admin login / stale sidebar.editing)
13368  if (document.body.classList.contains('edit-mode-active')) {
13369    isEditing = true;
13370  } else if (typeof window.isEditing === 'boolean') {
13371    isEditing = window.isEditing;
13372  }
13373
13374  var galleriesSource = galleries;
13375  if (window.HelpSystem && typeof window.HelpSystem.getRenderGalleries === 'function') {
13376    var helpList = window.HelpSystem.getRenderGalleries();
13377    if (helpList) galleriesSource = helpList;
13378  }
13379
13380try {
13381// Only fix circular references if absolutely necessary
13382const foundCircular = fixCircularReferences();
13383
13384// Only create a new tree if we had to fix circular references
13385let galleryTree;
13386if (foundCircular) {
13387  console.log("Creating fresh gallery tree after fixing circular references");
13388  galleryTree = createGalleryTree(galleriesSource);
13389} else {
13390  console.log("Using existing structure to create gallery tree");
13391  galleryTree = createGalleryTree(galleriesSource);
13392}
13393
13394// Render the tree - IMPORTANT: Add null check
13395const tree = document.getElementById('galleryTree');
13396if (!tree) {
13397  console.warn('galleryTree element not found, cannot render galleries');
13398  return;  // Exit the function if the element doesn't exist
13399}
13400
13401// Render the tree with maximum depth protection
13402tree.innerHTML = `
13403  <ol class="nested-sortable">
13404    ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')}
13405  </ol>
13406`;
13407
13408// Initialize sortable for all nested sortable elements
13409if (isEditing) {
13410  // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables
13411  // This prevents drag issues when items are added and immediately dragged
13412  requestAnimationFrame(() => {
13413    initializeNestedSortables();
13414  });
13415}
13416
13417console.log("Galleries rendered successfully");
13418
13419// Set up folder toggle handler after rendering (works in both view and edit modes)
13420setupFolderToggleHandler();
13421
13422// Verify structure integrity after rendering
13423verifyGalleryStructure();
13424
13425if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
13426} catch (error) {
13427console.error("Error rendering galleries:", error);
13428}
13429}
13430
13431/** Prefer visible menu links for AT; keep SEO nav in DOM for crawlers. */
13432function syncSeoNavA11y() {
13433  const seoNav = document.querySelector('.hydra-seo-nav');
13434  if (!seoNav) return;
13435  const hasVisibleLinks =
13436    document.querySelectorAll('#galleryTree a.menu-item').length > 0 ||
13437    document.querySelectorAll('.horizontal-menu-container a.horizontal-menu-label').length > 0;
13438  const hideFromAt = hasVisibleLinks;
13439  seoNav.setAttribute('aria-hidden', hideFromAt ? 'true' : 'false');
13440  // aria-hidden alone is not enough — focusable descendants must leave the tab order
13441  seoNav.querySelectorAll('a, button, [tabindex]').forEach((el) => {
13442    if (hideFromAt) {
13443      if (!el.hasAttribute('data-seo-prev-tabindex')) {
13444        el.setAttribute('data-seo-prev-tabindex', el.hasAttribute('tabindex') ? el.getAttribute('tabindex') : '');
13445      }
13446      el.setAttribute('tabindex', '-1');
13447    } else {
13448      const prev = el.getAttribute('data-seo-prev-tabindex');
13449      if (prev === null) return;
13450      if (prev === '') el.removeAttribute('tabindex');
13451      else el.setAttribute('tabindex', prev);
13452      el.removeAttribute('data-seo-prev-tabindex');
13453    }
13454  });
13455}
13456
13457function getMenuItemHref(galleryItem) {
13458  if (!galleryItem) return '#';
13459  if (galleryItem.isExternal && galleryItem.url) return galleryItem.url;
13460  if (galleryItem.isHomePage) return '/';
13461  if (galleryItem.url && String(galleryItem.url).startsWith('/')) return galleryItem.url;
13462  if (galleryItem.slug) return '/' + galleryItem.slug;
13463  return '#';
13464}
13465
13466
13467function fixCircularReferences() {
13468console.log("Selectively checking for circular references...");
13469
13470// Only process galleries that need checking:
13471// 1. Imported galleries (they're new and might cause issues)
13472// 2. Galleries with extremely deep nesting
13473// 3. Actual circular references
13474
13475const processed = new Set();
13476
13477// First pass: Identify actual circular references without modifying structure
13478const circularIds = new Set();
13479const suspiciousIds = new Set();
13480
13481galleries.forEach(gallery => {
13482if (!gallery || !gallery.id) return;
13483
13484// Skip galleries we've already processed
13485if (processed.has(gallery.id)) return;
13486
13487// Check for self-reference - this is always wrong
13488if (gallery.parentId === gallery.id) {
13489  console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`);
13490  circularIds.add(gallery.id);
13491  return;
13492}
13493
13494// Skip galleries without parents
13495if (!gallery.parentId) {
13496  processed.add(gallery.id);
13497  return;
13498}
13499
13500// Follow the parent chain to detect circular references
13501const parentChain = [gallery.id];
13502let currentId = gallery.parentId;
13503let loopCount = 0;
13504let foundCircular = false;
13505
13506while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops
13507  loopCount++;
13508  
13509  // If we've seen this ID before, it's a circular reference
13510  if (parentChain.includes(currentId)) {
13511    console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`);
13512    circularIds.add(gallery.id);
13513    foundCircular = true;
13514    break;
13515  }
13516  
13517  parentChain.push(currentId);
13518  
13519  // Move up to the next parent
13520  const parent = galleries.find(g => g.id === currentId);
13521  currentId = parent?.parentId;
13522  
13523  // If we can't find the parent, we can stop
13524  if (!parent) break;
13525}
13526
13527// If we hit our loop limit but didn't find a circular reference,
13528// it's suspicious but not definitely circular
13529if (loopCount >= 20 && !foundCircular) {
13530  console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`);
13531  suspiciousIds.add(gallery.id);
13532}
13533
13534// Mark this gallery and all its ancestors as processed
13535parentChain.forEach(id => processed.add(id));
13536});
13537
13538// Second pass: Only fix the identified circular references
13539if (circularIds.size > 0) {
13540console.log(`Fixing ${circularIds.size} confirmed circular references`);
13541
13542galleries.forEach(gallery => {
13543  if (circularIds.has(gallery.id)) {
13544    console.log(`Breaking circular reference for gallery: ${gallery.title}`);
13545    gallery.parentId = null;
13546  }
13547});
13548} else {
13549console.log("No circular references found");
13550}
13551
13552// For suspicious galleries, don't automatically fix them
13553if (suspiciousIds.size > 0) {
13554console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`);
13555}
13556
13557return circularIds.size > 0; // Return true if we fixed anything
13558}
13559
13560// Deep linking implementation that uses the existing slug property
13561
13562// Function to get the slug for a gallery
13563function getGallerySlug(gallery) {
13564// First try to use the existing slug property
13565if (gallery.slug) {
13566console.log(`Using existing gallery slug: ${gallery.slug}`);
13567return gallery.slug;
13568}
13569
13570// If no slug exists, generate one from the title as fallback
13571if (gallery.title) {
13572const generatedSlug = gallery.title
13573  .toLowerCase()
13574  .replace(/[^ws-]/g, '')
13575  .replace(/s+/g, '-')
13576  .replace(/--+/g, '-')
13577  .trim();
13578  
13579console.log(`Generated slug from title: ${generatedSlug}`);
13580return generatedSlug || 'gallery';
13581}
13582
13583// Default fallback
13584console.warn('Gallery has no slug or title, using default');
13585return 'gallery';
13586}
13587
13588// Function to update the URL when a gallery is loaded
13589// Update the URL when a gallery is loaded
13590function updateURLWithGallerySlug(gallery) {
13591  if (!gallery) return;
13592
13593  // Keep bookmarkable /_manage URL — do not rewrite to / or /slug when loading galleries/pages
13594  const __hydraPath = window.location.pathname;
13595  if (__hydraPath === '/_manage' || __hydraPath === '/_manage/') {
13596    console.log('[updateURLWithGallerySlug] On /_manage editor path; skipping slug URL update.');
13597    return;
13598  }
13599
13600  // Get the slug for this gallery/page
13601  let slug = gallery.slug;
13602
13603  if (!slug && gallery.title) {
13604    slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim();
13605  }
13606
13607  // Check if we're in preview mode (add this new code)
13608  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13609  
13610  // Check if current history state already has this galleryId
13611  const currentState = window.history.state;
13612  const currentPath = window.location.pathname;
13613  const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && 
13614                               ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug));
13615  
13616  if (isPreviewMode) {
13617    // Extract GUID from current URL path - first segment after domain
13618    const pathParts = window.location.pathname.split('/').filter(Boolean);
13619    const siteGuid = pathParts[0];
13620    
13621    // Create history entry if we have a valid GUID, and it's not already the current state.
13622    // Home pages use /{guid}/ (canonical preview home), matching live's '/' special-case.
13623    if (siteGuid && (gallery.isHomePage || slug)) {
13624      const targetPath = gallery.isHomePage ? ('/' + siteGuid + '/') : ('/' + siteGuid + '/' + slug);
13625      const alreadyAtPreviewHome = gallery.isHomePage && (
13626        currentPath === '/' + siteGuid || currentPath === '/' + siteGuid + '/'
13627      );
13628      // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13629      if ((!alreadyAtPreviewHome && currentPath !== targetPath) || !isAlreadyCurrentPage) {
13630        console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id);
13631        
13632        // Update browser URL without reloading, preserving the GUID
13633        window.history.pushState(
13634          { galleryId: gallery.id }, 
13635          gallery.title || 'Gallery', 
13636          targetPath
13637        );
13638        
13639        // Update page title
13640        document.title = formatGalleryDocumentTitle(gallery.title);
13641      } else {
13642        console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)');
13643      }
13644    }
13645    
13646    return; // Exit early, we've handled the preview case
13647  }
13648  
13649  // Regular (non-preview) URL handling
13650  if (slug) {
13651    // If this gallery is the designated home page, keep the URL as '/' (canonical home URL)
13652    const targetPath = gallery.isHomePage ? '/' : '/' + slug;
13653    // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13654    if (currentPath !== targetPath || !isAlreadyCurrentPage) {
13655      console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id);
13656      window.history.pushState(
13657        { galleryId: gallery.id }, 
13658        gallery.title || 'Gallery', 
13659        targetPath
13660      );
13661      document.title = formatGalleryDocumentTitle(gallery.title);
13662    } else {
13663      console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update');
13664    }
13665  }
13666}
13667
13668
13669function loadHomePage() {
13670  // Find a gallery marked as home page (regardless of visibility)
13671  const homePage = galleries.find(gallery => gallery.isHomePage === true);
13672  
13673  if (homePage) {
13674    console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`);
13675    
13676    // Update active gallery ID
13677    activeGalleryId = homePage.id;
13678    
13679    // Add a special flag to indicate we're loading the home page
13680    // This allows us to bypass visibility checks
13681    window._loadingHomePage = true;
13682    
13683    // Determine if this is a page or gallery and load appropriately
13684    if (homePage.isPage) {
13685      loadPage(homePage.id);
13686    } else {
13687      loadGallery(homePage.id);
13688    }
13689    
13690    // Clear the flag after loading
13691    setTimeout(() => {
13692      window._loadingHomePage = false;
13693    }, 100);
13694    
13695    // Update UI state
13696    updateActiveStates();
13697    updateMobileTitle();
13698    if (typeof closeMobileMenu === 'function') closeMobileMenu();
13699    
13700    return true;
13701  }
13702  
13703  console.log('No home page defined');
13704  return false;
13705}
13706
13707// 4. Update the handleURLNavigation function to check for home page
13708// Find the existing handleURLNavigation function and modify as follows:
13709// In index.ts
13710function handleURLNavigation() {
13711  const path = window.location.pathname.substring(1); // Remove leading slash
13712  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13713  let slug = path;
13714  let siteGuid = null;
13715
13716  // /_manage is the Hydra editor entry — treat like site root for slug resolution (not a gallery slug)
13717  if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
13718    console.log('[handleURLNavigation] /_manage detected — using root navigation behavior');
13719    slug = '';
13720  }
13721
13722  if (isPreviewMode && path) {
13723    const pathParts = path.split('/');
13724    if (pathParts.length >= 1) {
13725        siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug
13726    }
13727    if (pathParts.length >= 2) {
13728      slug = pathParts[1]; // Slug is the part after GUID
13729      console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`);
13730    } else {
13731      // Only GUID is present, or path is empty after GUID
13732      slug = ''; // Treat as root/home for the given GUID
13733      console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`);
13734    }
13735  }
13736
13737  if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/')
13738    console.log("No specific slug in path (root or GUID-only preview), checking for home page.");
13739    if (!loadHomePage() && galleries.length > 0) {
13740      const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13741      if (firstVisibleGallery) {
13742        console.log("No home page, loading first visible item:", firstVisibleGallery.title);
13743        if (firstVisibleGallery.isPage) {
13744            loadPage(firstVisibleGallery.id);
13745        } else {
13746            loadGallery(firstVisibleGallery.id);
13747        }
13748      } else {
13749        console.log("No home page and no visible items to load for this path.");
13750      }
13751    }
13752  } else {
13753    console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : ""));
13754    // findGalleryByPath should be able to find the item using the slug,
13755    // handling preview mode internally if necessary or by being passed the correct slug.
13756    const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid);
13757
13758    if (matchingGallery) {
13759      console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')');
13760      console.log('🔍 [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements)));
13761      
13762      if (matchingGallery.isSubmenu) {
13763        console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.');
13764        // Potentially, you might want to find and expand its parent here if the UI supports it.
13765      } else {
13766        // Set activeGalleryId *before* calling loadPage/loadGallery
13767        activeGalleryId = matchingGallery.id;
13768        window.activeGalleryId = matchingGallery.id;
13769
13770        // Determine if this is a page by checking multiple indicators
13771        // Check isPage flag, pageId, or pageElements array
13772        const isPage = matchingGallery.isPage === true || 
13773                       (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) ||
13774                       (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0);
13775        
13776        if (isPage) {
13777          console.log('🔍 [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"');
13778          const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
13779          pageLoader(matchingGallery.id);
13780        } else {
13781          console.log('🔍 [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"');
13782          const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
13783          galleryLoader(matchingGallery.id, null, { forceReload: true });
13784        }
13785      }
13786    } else {
13787      console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`);
13788      if (!loadHomePage()) { // Try loading home page
13789        const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13790        if (firstVisibleGallery) {
13791          console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`);
13792           if (firstVisibleGallery.isPage) {
13793               loadPage(firstVisibleGallery.id);
13794           } else {
13795               loadGallery(firstVisibleGallery.id);
13796           }
13797        } else {
13798            console.log("No matching gallery, no home page, and no visible items to load.");
13799            // Optionally, display a 404 message in the gallery-container
13800            const galleryContainer = document.querySelector('.gallery-container');
13801            if (galleryContainer) {
13802                galleryContainer.innerHTML = '<div>Page not found.</div>';
13803                galleryContainer.style.opacity = '1';
13804            }
13805        }
13806      }
13807    }
13808  }
13809
13810  // Update UI states after initiating load.
13811  // These functions should ideally use the now-set activeGalleryId.
13812  if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree
13813  if (typeof updateMobileTitle === 'function') updateMobileTitle();   // For mobile header
13814
13815  // **** ADDED FOR DEEP LINKING HORIZONTAL MENU ****
13816  // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load)
13817  let currentLayout = 'sidebar'; // Default assumption
13818  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
13819    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
13820  } else {
13821    // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings
13822    if (document.body.classList.contains('menu-layout-horizontal')) {
13823        currentLayout = 'horizontal';
13824    }
13825  }
13826
13827  console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`);
13828
13829  if (currentLayout === 'horizontal') {
13830    if (typeof updateActiveStatesHorizontal === 'function') {
13831      console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation.");
13832      // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call.
13833      // A small delay might be needed, or ensure renderHorizontalMenu has run.
13834      // For now, call it directly. If items aren't there, it won't do anything harmful.
13835      setTimeout(() => { // Add a slight delay to allow menu rendering
13836          updateActiveStatesHorizontal();
13837      }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered.
13838    } else {
13839        console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout.");
13840    }
13841  }
13842}
13843
13844/**
13845* Enhanced loadPage function that properly cleans up gallery content
13846* @param {number} id - The page ID
13847* @param {Event} event - Optional event object
13848*/
13849window.loadPage = function(id, event) {
13850  if (event && event.target && event.target.isConnected === false) {
13851    console.log('🔍 [loadPage] Ignoring navigation from detached element');
13852    return;
13853  }
13854  if (event) {
13855    event.preventDefault();
13856    event.stopPropagation();
13857    const now = Date.now();
13858    const lastCallTime = window._lastPageLoadTime || 0;
13859    window._lastPageLoadTime = now;
13860    if (now - lastCallTime < 100) {
13861      console.log('Ignoring duplicate loadPage call');
13862      return;
13863    }
13864  }
13865  console.error('🔍 [loadPage] Loading page with gallery ID:', id);
13866
13867  stopAutoAdvanceTimer();
13868  if (typeof dismissGalleryPasswordOverlay === 'function') {
13869    dismissGalleryPasswordOverlay();
13870  }
13871
13872  let galleriesData = window.galleries || galleries;
13873  const gallery = findGalleryById(galleriesData, id);
13874
13875  if (!gallery) {
13876    console.warn('No gallery found with ID:', id, 'for page load.');
13877    return;
13878  }
13879
13880  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
13881
13882  window.activeGalleryId = id;
13883  activeGalleryId = id;
13884  window._activeContentType = 'page';
13885  if (window.EnhancedGallerySystem) {
13886    window.EnhancedGallerySystem.currentGalleryId = null;
13887  }
13888
13889  if (typeof window.removeGalleryScriptsWithPause === 'function') {
13890    window.removeGalleryScriptsWithPause();
13891  }
13892
13893  const galleryContainer = document.querySelector('.gallery-container');
13894  if (galleryContainer) {
13895    // Clear the container but ensure it's visible
13896    galleryContainer.innerHTML = '';
13897    galleryContainer.style.display = 'block';
13898    galleryContainer.style.visibility = 'visible';
13899    galleryContainer.style.opacity = '1';
13900    console.log('🔍 [loadPage] Cleared and ensured gallery-container is visible');
13901  } else {
13902    console.error('🔍 [loadPage] gallery-container not found!');
13903  }
13904
13905  if (!gallery.pageId) gallery.pageId = `page_${id}`;
13906  if (!gallery.isPage) gallery.isPage = true;
13907
13908  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13909    window.syncGalleryOptionsButtonVisibility();
13910  }
13911
13912  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
13913  window._pageIdToGalleryId[gallery.pageId] = id;
13914
13915  const seedPageElementsOntoManager = function() {
13916    if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
13917      if (window.PageManager && window.PageManager.elements) {
13918        window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
13919      }
13920    } else if (window.PageManager && window.PageManager.elements &&
13921               window.PageManager.elements[gallery.pageId] &&
13922               window.PageManager.elements[gallery.pageId].length > 0) {
13923      gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
13924    }
13925  };
13926  seedPageElementsOntoManager();
13927
13928  const invokePageManagerLoad = function() {
13929    seedPageElementsOntoManager();
13930    try {
13931      console.error('🔍 [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId);
13932      console.error('🔍 [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0);
13933      console.error('🔍 [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0);
13934      console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId]));
13935      
13936      // Small delay to ensure DOM is ready, especially when navigating back via history
13937      setTimeout(() => {
13938        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13939          console.log('🔍 [loadPage] Skipping stale delayed PageManager load for:', gallery.pageId);
13940          return;
13941        }
13942
13943        // Verify container still exists
13944        const containerCheck = document.querySelector('.gallery-container');
13945        console.error('🔍 [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer);
13946        
13947        // PageManager.loadPage is async - we need to properly handle it
13948        try {
13949          const loadPromise = window.PageManager.loadPage(gallery.pageId);
13950          console.error('🔍 [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function');
13951          
13952          if (loadPromise && typeof loadPromise.then === 'function') {
13953            loadPromise.then(() => {
13954              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13955                console.log('🔍 [loadPage] Ignoring stale PageManager completion for:', gallery.pageId);
13956                return;
13957              }
13958
13959              console.error('🔍 [loadPage] PageManager.loadPage promise resolved');
13960              if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13961                window.syncGalleryOptionsButtonVisibility();
13962              }
13963              // Ensure container visibility after PageManager has initialized
13964              if (galleryContainer) {
13965                const pageContainer = galleryContainer.querySelector('.page-container');
13966                if (pageContainer) {
13967                  pageContainer.style.display = 'block';
13968                  pageContainer.style.visibility = 'visible';
13969                  pageContainer.style.opacity = '1';
13970                  const elementCount = pageContainer.querySelectorAll('.page-element').length;
13971                  console.error('🔍 [loadPage] Page container found and made visible, has', elementCount, 'elements');
13972                  if (elementCount === 0) {
13973                    console.error('🔍 [loadPage] WARNING: Page container exists but has no elements!');
13974                    console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]);
13975                  }
13976                } else {
13977                  console.error('❌ [loadPage] Page container not found after PageManager.loadPage');
13978                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
13979                  console.error('❌ [loadPage] Gallery container children:', galleryContainer.children.length);
13980                }
13981              } else {
13982                console.error('❌ [loadPage] Gallery container is null after PageManager.loadPage');
13983              }
13984            }).catch((error) => {
13985              console.error('❌ [loadPage] Error in PageManager.loadPage promise:', error);
13986              console.error('❌ [loadPage] Error stack:', error.stack);
13987            });
13988          } else {
13989            // If loadPage doesn't return a promise, check after a delay
13990            console.error('🔍 [loadPage] loadPage did not return a promise, checking after delay');
13991            setTimeout(() => {
13992              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
13993                console.log('🔍 [loadPage] Ignoring stale non-promise PageManager completion for:', gallery.pageId);
13994                return;
13995              }
13996              if (galleryContainer) {
13997                const pageContainer = galleryContainer.querySelector('.page-container');
13998                if (pageContainer) {
13999                  pageContainer.style.display = 'block';
14000                  pageContainer.style.visibility = 'visible';
14001                  pageContainer.style.opacity = '1';
14002                  console.error('🔍 [loadPage] Ensured page-container is visible (non-promise path)');
14003                } else {
14004                  console.error('❌ [loadPage] Page container not found (non-promise path)');
14005                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
14006                }
14007              }
14008            }, 200);
14009          }
14010        } catch (loadError) {
14011          console.error('❌ [loadPage] Exception calling PageManager.loadPage:', loadError);
14012          console.error('❌ [loadPage] Error stack:', loadError.stack);
14013        }
14014        
14015        const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
14016        if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
14017          window.PageManager.setEditMode(isCurrentlyEditing);
14018        }
14019      }, 50); // Small delay to ensure DOM is ready
14020    } catch (error) {
14021      console.error('❌ [loadPage] Error in loadPage function:', error);
14022      console.error('❌ [loadPage] Error stack:', error.stack);
14023    }
14024  };
14025
14026  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
14027    invokePageManagerLoad();
14028  } else {
14029    console.error('❌ [loadPage] PageManager not found yet — waiting for CDN proxy script');
14030    console.error('❌ [loadPage] window.PageManager:', !!window.PageManager);
14031    console.error('❌ [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A');
14032    whenPageManagerReady(function() {
14033      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
14034        console.log('🔍 [loadPage] Skipping stale PageManager ready retry for:', gallery.pageId);
14035        return;
14036      }
14037      console.log('✅ [loadPage] PageManager became available — retrying page render');
14038      invokePageManagerLoad();
14039    });
14040  }
14041
14042  // Apply menu visibility
14043  const bodyEl = document.body;
14044  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
14045      bodyEl.classList.add('menu-hidden-on-page');
14046  } else {
14047      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
14048  }
14049  applyMenuOverlayClassForGallery(gallery);
14050
14051  if (typeof updateActiveStates === 'function') updateActiveStates();
14052  if (typeof updateMobileTitle === 'function') updateMobileTitle();
14053  if (typeof closeMobileMenu === 'function') closeMobileMenu();
14054  // Close the gallery options panel when navigating to a different page
14055  if (typeof window.closeOptionsPanel === 'function') {
14056    window.closeOptionsPanel();
14057  }
14058  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
14059  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
14060    window.syncGalleryOptionsButtonVisibility();
14061  }
14062  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
14063    window.dispatchEvent(new CustomEvent('page-loaded', {
14064      detail: { pageId: gallery.pageId, galleryId: id }
14065    }));
14066  }
14067}
14068
14069
14070
14071/**
14072* Ensures all required gallery styles are loaded
14073*/
14074function ensureGalleryStylesLoaded() {
14075window.ensureGalleryStylesLoaded = ensureGalleryStylesLoaded;
14076// Use Worker route for gallery CSS (avoids CDN SSL issues)
14077const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14078const requiredStyles = [
14079{ id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' },
14080{ id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' }
14081];
14082console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css');
14083
14084requiredStyles.forEach(style => {
14085if (!document.getElementById(style.id)) {
14086  const link = document.createElement('link');
14087  link.id = style.id;
14088  link.rel = 'stylesheet';
14089  link.href = style.href;
14090  if (style.href.includes('cdn.neonsky.app')) {
14091    link.onerror = function() {
14092      console.error('❌ Gallery CSS failed, retrying via proxy:', link.href);
14093      // Set flag if not already set
14094      if (!window.useCdnProxy) {
14095        window.useCdnProxy = true;
14096        console.error('🚩 CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests');
14097      }
14098      const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14099      link.href = proxyUrl;
14100      link.onerror = function() {
14101        console.error('❌ Proxy failed, trying storage:', proxyUrl);
14102        link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
14103      };
14104    };
14105  }
14106  document.head.appendChild(link);
14107  console.log(`Added gallery style: ${style.id}`);
14108}
14109});
14110}
14111
14112
14113/**
14114* Ensures all required gallery scripts are loaded
14115* @returns {Promise} A promise that resolves when all scripts are loaded
14116*/
14117function ensureGalleryScriptsLoaded() {
14118const requiredScripts = [
14119{ id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js' },
14120{ id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' },
14121          { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260923-002.js' }
14122];
14123
14124const promises = requiredScripts.map(script => {
14125return new Promise((resolve, reject) => {
14126  // If script is already loaded, resolve immediately
14127  if (document.getElementById(script.id)) {
14128    resolve();
14129    return;
14130  }
14131  
14132  // Create and load the script
14133  const scriptElement = document.createElement('script');
14134  scriptElement.id = script.id;
14135  scriptElement.src = script.src;
14136  scriptElement.onload = resolve;
14137  scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`));
14138  document.head.appendChild(scriptElement);
14139});
14140});
14141
14142return Promise.all(promises);
14143}
14144
14145/**
14146 * Creates a clean gallery context without using an iframe
14147 * @param {Object} gallery - The gallery object
14148 * @param {HTMLElement} container - The container to load the gallery into
14149 */
14150function createGalleryContext(gallery, container) {
14151  console.log('Creating gallery context for:', gallery.title);
14152
14153  // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script
14154  // This ensures styles are available even if NDJSON fails and falls back to classic JSON
14155  ensureGalleryStylesLoaded();
14156
14157  // Generate the page alias safely
14158  const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-');
14159
14160  // Get our gallery options
14161  const galleryOptions = gallery.galleryOptions || {};
14162
14163  // Get token data
14164  let authToken = null;
14165  // First try TokenManager if available
14166  if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
14167    authToken = window.TokenManager.getToken();
14168  } 
14169  // Then try global token
14170  else if (window.didToken) {
14171    authToken = window.didToken;
14172  }
14173
14174  // Set up Parameters for the gallery
14175  const galleryParameters = {
14176    SiteAlias: window.location.hostname,
14177    InitialPageUuid: gallery.pageId,
14178    InitialPageAlias: pageAlias,
14179    isInEditor: window.isEditing || false,
14180    siteId: window.siteId || '',
14181    isHydra: true
14182  };
14183
14184  // If we have a token, add it
14185  if (authToken) {
14186    galleryParameters.hydraAuthToken = authToken;
14187  }
14188
14189  // Preserve original window.Parameters
14190  const originalParameters = window.Parameters;
14191
14192  // REMOVED: Loading indicator - no longer showing the loading animation
14193
14194  // Load the main gallery script - Use Worker route (avoids CDN SSL issues)
14195  const galleryScript = document.createElement('script');
14196  const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14197  galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js';
14198  console.log('Loading gallery-main script from Worker:', galleryScript.src);
14199  galleryScript.onerror = function() {
14200    console.error('❌ Gallery script failed, retrying via proxy:', galleryScript.src);
14201    const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14202    galleryScript.src = proxyUrl;
14203    galleryScript.onerror = function() {
14204      console.error('❌ Proxy failed, trying storage:', proxyUrl);
14205      galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14206    };
14207  };
14208
14209  // Setup the gallery configuration
14210  const neonGalleryConfig = {
14211    useData: true,
14212    useCDN: true,
14213    version: 'live',
14214    manualCollectionName: "mod",
14215    layoutType: "grid",
14216    ...galleryOptions,
14217    siteId: window.siteId || ''
14218  };
14219
14220  // Execute in a safe way that minimizes global namespace pollution
14221  try {
14222    // Set up the global variables needed by the gallery script
14223    window.Parameters = galleryParameters;
14224    window.neonGalleryConfig = neonGalleryConfig;
14225
14226    // Monitor for script load/error
14227    galleryScript.onload = () => {
14228      console.log('Gallery script loaded successfully for:', gallery.title);
14229      
14230      // REMOVED: Don't need to remove the loading indicator since we don't add it
14231    };
14232
14233    galleryScript.onerror = () => {
14234      console.error('Failed to load gallery script for:', gallery.title);
14235      //container.innerHTML = '<div class="gallery-error">Error loading gallery. Please try again later.</div>';
14236      // Restore original parameters
14237      window.Parameters = originalParameters;
14238    };
14239
14240    // Add the script to load the gallery
14241    document.body.appendChild(galleryScript);
14242
14243    // Set up a cleanup function that will be called when another gallery is loaded
14244    container.cleanup = () => {
14245      // Restore original parameters when cleaning up
14246      window.Parameters = originalParameters;
14247      
14248      // Remove the gallery script to prevent conflicts
14249      galleryScript.remove();
14250      
14251      // Clear main gallery container
14252      if (container.parentNode) {
14253        container.innerHTML = '';
14254      }
14255    };
14256  } catch (error) {
14257    console.error('Error creating gallery context:', error);
14258    //container.innerHTML = '<div class="gallery-error">Error initializing gallery. Please try again later.</div>';
14259    // Restore original parameters
14260    window.Parameters = originalParameters;
14261  }
14262}
14263
14264/**
14265* Only used for the PageManager's initializeDOM - ensures clean content swap
14266*/
14267function cleanGalleryContainer() {
14268const galleryContainer = document.querySelector('.gallery-container');
14269if (galleryContainer) {
14270galleryContainer.innerHTML = '';
14271}
14272}
14273
14274
14275
14276/**
14277* Initializes the page using the PageManager
14278* To be used by PageManager.initializeDOM
14279*/
14280function initPageContent(pageId) {
14281// First clear the container completely
14282cleanGalleryContainer();
14283
14284// Then load the page content
14285if (window.PageManager && window.PageManager.loadPage) {
14286window.PageManager.loadPage(pageId);
14287}
14288}
14289
14290
14291/**
14292* Cleanup gallery content when changing between galleries or pages
14293*/
14294function cleanupGalleryContent() {
14295// Get the gallery container
14296const galleryContainer = document.querySelector('.gallery-container');
14297if (!galleryContainer) return;
14298
14299// Find the direct gallery content
14300const directContent = galleryContainer.querySelector('.gallery-direct-content');
14301if (directContent && typeof directContent.cleanup === 'function') {
14302// Call the cleanup function
14303directContent.cleanup();
14304}
14305
14306// Clear the container
14307galleryContainer.innerHTML = '';
14308}
14309
14310
14311
14312
14313/**
14314* Simplified loadGallery function that follows the same content swap pattern
14315* @param {number} id - The gallery ID
14316* @param {Event} event - Optional event object
14317*/
14318
14319// Override loadGallery to update URL
14320window.loadGallery = async function(id, event) {
14321
14322
14323// Stop event propagation if provided
14324if (event) {
14325  event.stopPropagation();
14326}
14327
14328const galleryContainer = document.querySelector('.gallery-container');
14329if (galleryContainer) {
14330  // Apply immediate hiding styles
14331  galleryContainer.style.opacity = '0';
14332}
14333
14334console.log('Loading gallery with ID:', id);
14335
14336stopAutoAdvanceTimer();
14337if (typeof dismissGalleryPasswordOverlay === 'function') {
14338  dismissGalleryPasswordOverlay();
14339}
14340
14341// Find gallery by ID
14342const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
14343
14344if (!gallery) {
14345  console.warn('No gallery found with ID:', id);
14346  if (galleryContainer) {
14347  galleryContainer.style.opacity = '1';
14348  } // Show container if gallery not found
14349  return;
14350}
14351
14352// Check if this is a page, if so use loadPage instead
14353if (isPageMenuItem(gallery)) {
14354  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
14355  return loadPage(id, event);
14356}
14357
14358// Skip submenu items
14359if (gallery.isSubmenu) {
14360  console.log('Gallery is submenu, skipping URL update');
14361  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
14362  if (galleryContainer) {
14363  galleryContainer.style.opacity = '1';
14364  } // Show container
14365  return;
14366}
14367
14368console.log('Found gallery:', gallery.title);
14369
14370window._activeContentType = 'gallery';
14371beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
14372if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14373  window.PageManager.clearCurrentPage();
14374}
14375
14376// CRITICAL: Set active gallery ID consistently in both global and window scope
14377console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
14378window.activeGalleryId = id;
14379activeGalleryId = id; // Ensure both variables are synchronized
14380
14381// Save reference to which gallery we're loading (for later verification)
14382window.currentLoadingGalleryId = id;
14383
14384// CRITICAL: Thorough cleanup with await to ensure completion
14385if (typeof window.removeGalleryScriptsWithPause === 'function') {
14386  await window.removeGalleryScriptsWithPause();
14387}
14388
14389
14390// Clear all existing content from gallery container
14391if (!galleryContainer) {
14392  console.error('Gallery container not found');
14393  return;
14394}
14395
14396// Force clear all content
14397galleryContainer.innerHTML = '';
14398
14399
14400// CRITICAL: Double-check active gallery ID before updating UI
14401if (window.activeGalleryId !== id || activeGalleryId !== id) {
14402  console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
14403  window.activeGalleryId = id;
14404  activeGalleryId = id;
14405}
14406
14407// Apply menu visibility based on the gallery's setting and edit mode
14408const bodyEl = document.body;
14409if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
14410    bodyEl.classList.add('menu-hidden-on-page');
14411} else {
14412    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
14413}
14414applyMenuOverlayClassForGallery(gallery);
14415
14416// Update UI state using explicit window function references or local fallbacks
14417if (typeof window.updateActiveStates === 'function') {
14418  window.updateActiveStates();
14419} else if (typeof updateActiveStates === 'function') {
14420  updateActiveStates();
14421}
14422
14423if (typeof window.updateMobileTitle === 'function') {
14424  window.updateMobileTitle();
14425} else if (typeof updateMobileTitle === 'function') {
14426  updateMobileTitle();
14427}
14428
14429if (typeof window.closeMobileMenu === 'function') {
14430  window.closeMobileMenu();
14431} else if (typeof closeMobileMenu === 'function') {
14432  closeMobileMenu();
14433}
14434
14435// Update URL with gallery slug
14436if (typeof window.updateURLWithGallerySlug === 'function') {
14437  window.updateURLWithGallerySlug(gallery);
14438} else if (typeof updateURLWithGallerySlug === 'function') {
14439  updateURLWithGallerySlug(gallery);
14440}
14441
14442  // Create gallery container and load content
14443const neonGalleryContainer = document.createElement('div');
14444neonGalleryContainer.id = 'neon-gallery-container';
14445neonGalleryContainer.className = 'gallery-direct-content';
14446neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
14447neonGalleryContainer.style.width = '100%';
14448neonGalleryContainer.style.height = '100%';
14449galleryContainer.appendChild(neonGalleryContainer);
14450
14451// Add a cache-busting parameter to ensure script is freshly loaded
14452const timestamp = Date.now();
14453
14454// Set up gallery parameters
14455window.Parameters = {
14456  SiteAlias: window.location.hostname,
14457  InitialPageUuid: gallery.pageId,
14458  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
14459  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
14460  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14461  isHydra: true,
14462  galleryInstanceId: timestamp,
14463  // Added loadedGalleryId as per your original function
14464  loadedGalleryId: gallery.id
14465};
14466
14467// Debug: Check what galleryOptions contains
14468console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
14469
14470// Set up gallery config
14471window.neonGalleryConfig = {
14472  useData: true,
14473  useCDN: true,
14474  version: 'live',
14475  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
14476  layoutType: gallery.galleryOptions?.layoutType || "grid",
14477  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
14478  // This ensures that only properties from the current gallery are included, not from previous galleries
14479  ...(gallery.galleryOptions || {}),
14480  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
14481  // These fields are required for the gallery to determine if it should use Load Network racing
14482  loadTxId: gallery.galleryOptions?.loadTxId || null,
14483  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
14484  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
14485  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
14486  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
14487  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
14488  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
14489  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
14490  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14491  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
14492  galleryId: gallery.id,
14493  galleryInstanceId: timestamp
14494};
14495
14496console.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);
14497
14498// Create and add the gallery script with cache-busting
14499// Use Worker route (avoids CDN SSL issues)
14500const script = document.createElement('script');
14501const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14502script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
14503script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
14504script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
14505console.log('Loading gallery-main script from Worker:', script.src);
14506script.onerror = function() {
14507  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
14508  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14509  script.src = proxyUrl;
14510  script.onerror = function() {
14511    console.error('❌ Proxy failed, trying storage:', proxyUrl);
14512    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14513  };
14514};
14515
14516// Added script.onload from your original function
14517script.onload = function() {
14518  if (window.currentLoadingGalleryId !== gallery.id) {
14519    console.warn('Gallery ID mismatch! Expected:', gallery.id,
14520                 'Current:', window.currentLoadingGalleryId);
14521  }
14522  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
14523};
14524
14525
14526if (galleryContainer) {
14527    setTimeout(() => {
14528      galleryContainer.style.opacity = '1';
14529    }, 50); // Small delay to ensure content is ready
14530  }
14531
14532console.log(`Loading fresh gallery script for: ${gallery.title}`);
14533document.body.appendChild(script);
14534}
14535
14536// Function to ensure gallery container is visible
14537function ensureGalleryVisibility() {
14538  const galleryContainer = document.querySelector('.gallery-container');
14539  if (galleryContainer && galleryContainer.style.opacity === '0') {
14540    console.log('ensureGalleryVisibility: Setting gallery container opacity to 1');
14541    galleryContainer.style.opacity = '1';
14542  }
14543}
14544
14545// Handle browser back/forward navigation
14546window.addEventListener('popstate', async function(event) { // Make it async
14547  if (window.__hydraPopstateLock) {
14548    return;
14549  }
14550  window.__hydraPopstateLock = true;
14551  try {
14552  console.error('🔍 [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname);
14553  let galleryIdToLoad = null;
14554  let galleryToLoad = null;
14555
14556  // First, try to get galleryId from event.state
14557  if (event.state && event.state.galleryId) {
14558    galleryIdToLoad = event.state.galleryId;
14559    console.error('🔍 [Popstate] Found galleryId in event.state:', galleryIdToLoad);
14560    // Ensure galleries array is available and use findGalleryById helper
14561    const currentGalleries = window.galleries || galleries || [];
14562    galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad);
14563  }
14564
14565  if (window.__closingNestedLightbox) {
14566    window.__closingNestedLightbox = false;
14567    if (typeof updateActiveStates === 'function') updateActiveStates();
14568    if (typeof updateMobileTitle === 'function') updateMobileTitle();
14569    return;
14570  }
14571
14572  if (window.lightboxNavigationStack?.length > 0 &&
14573      typeof window.closeNestedGalleryLightbox === 'function') {
14574    const stackTop = window.lightboxNavigationStack[window.lightboxNavigationStack.length - 1];
14575    const parentId = stackTop?.parentGalleryId;
14576    if (parentId && (event.state?.galleryId === parentId || galleryToLoad?.id === parentId)) {
14577      window.closeNestedGalleryLightbox({ fromPopstate: true });
14578      if (typeof updateActiveStates === 'function') updateActiveStates();
14579      if (typeof updateMobileTitle === 'function') updateMobileTitle();
14580      return;
14581    }
14582  }
14583
14584  if (typeof dismissGalleryPasswordOverlay === 'function') {
14585    dismissGalleryPasswordOverlay();
14586  }
14587
14588  // If no gallery found from state, resolve from URL slug (handles history entries with null state)
14589  if (!galleryToLoad) {
14590    console.error('🔍 [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname);
14591    const path = window.location.pathname.substring(1);
14592    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
14593    let slug = path;
14594    let siteGuid = null;
14595    if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
14596      slug = '';
14597    }
14598    if (isPreviewMode && path) {
14599      const pathParts = path.split('/');
14600      if (pathParts.length >= 1) siteGuid = pathParts[0];
14601      if (pathParts.length >= 2) slug = pathParts[1];
14602      else slug = '';
14603    }
14604    if (slug && typeof findGalleryByPath === 'function') {
14605      galleryToLoad = findGalleryByPath(slug, isPreviewMode, siteGuid);
14606      if (galleryToLoad) {
14607        console.error('🔍 [Popstate] Resolved from URL slug: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14608      }
14609    }
14610  }
14611
14612  // If we have a gallery from state or URL, load it
14613  let contentLoaded = false;
14614  if (galleryToLoad) {
14615    console.error('🔍 [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14616    console.error('🔍 [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements)));
14617    
14618    activeGalleryId = galleryToLoad.id;
14619    window.activeGalleryId = galleryToLoad.id;
14620
14621    // Browser history navigation is explicit user intent — load content regardless of menu visibility.
14622    // Determine if this is a page by checking multiple indicators
14623    const isPage = galleryToLoad.isPage === true || 
14624                   (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) ||
14625                   (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0);
14626    
14627    if (isPage) {
14628      console.error('🔍 [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"');
14629      const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
14630      if (typeof pageLoader === 'function') {
14631          console.error('🔍 [Popstate] Calling loadPage for ID:', galleryToLoad.id);
14632          pageLoader(galleryToLoad.id);
14633          contentLoaded = true;
14634      } else {
14635          console.error('❌ [Popstate] loadPage function not available');
14636      }
14637    } else {
14638      console.error('🔍 [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"');
14639      // Clear page state left over from prior page navigation
14640      if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14641        window.PageManager.clearCurrentPage();
14642      }
14643      window._activeContentType = 'gallery';
14644      const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
14645      if (typeof galleryLoader === 'function') {
14646          await galleryLoader(galleryToLoad.id, null, { forceReload: true });
14647          contentLoaded = true;
14648      } else {
14649          console.error('❌ [Popstate] loadGallery function not available');
14650      }
14651    }
14652  } else {
14653    console.error('❌ [Popstate] No gallery found and handleURLNavigation not available');
14654  }
14655
14656  // Update UI states after loading (or attempting to load) content
14657  if (typeof updateActiveStates === 'function') updateActiveStates();
14658  if (typeof updateMobileTitle === 'function') updateMobileTitle();
14659  
14660  // Ensure gallery visibility after popstate navigation
14661  setTimeout(ensureGalleryVisibility, 1000);
14662
14663  let currentLayout = 'sidebar';
14664  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
14665    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
14666  } else if (document.body.classList.contains('menu-layout-horizontal')) {
14667    currentLayout = 'horizontal';
14668  }
14669
14670  if (currentLayout === 'horizontal') {
14671    if (typeof updateActiveStatesHorizontal === 'function') {
14672      console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay.");
14673      setTimeout(() => {
14674          console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal.");
14675          updateActiveStatesHorizontal();
14676      }, 150); // Slightly longer delay for popstate, as full content might be re-initializing
14677    }
14678  }
14679  } finally {
14680    setTimeout(function() { window.__hydraPopstateLock = false; }, 300);
14681  }
14682});
14683
14684// Check for direct URL access on page load (fallback when bootstrap did not route)
14685document.addEventListener('DOMContentLoaded', function() {
14686setTimeout(function() {
14687console.log('Checking for deep links after DOM content loaded');
14688var __p = window.location.pathname;
14689if (__p !== '/' && __p !== '/_manage' && __p !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14690  handleURLNavigation();
14691}
14692// Ensure gallery visibility after URL navigation
14693setTimeout(ensureGalleryVisibility, 1000);
14694}, 500);
14695});
14696
14697document.addEventListener('page-save-requested', function(e) {
14698const data = e.detail;
14699if (data && data.pageId && data.elements) {
14700// Find the gallery/page that corresponds to this pageId
14701const page = galleries.find(g => g.pageId === data.pageId);
14702if (page) {
14703  // Store the page elements
14704  page.pageElements = data.elements;
14705  
14706  // Save to server
14707  saveGalleries();
14708}
14709}
14710});
14711
14712document.addEventListener('DOMContentLoaded', function() {
14713function preloadRichTextEditor() {
14714  if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') {
14715    console.log('Preloading Rich Text Editor dependencies');
14716    return window.RichTextEditor._ensureEditorDependencies()
14717      .then(() => console.log('Rich Text Editor dependencies preloaded successfully'))
14718      .catch(error => console.warn('Failed to preload Rich Text Editor:', error));
14719  }
14720
14721  if (window.PageManager && typeof window.PageManager._ensureRichTextEditor === 'function') {
14722    return window.PageManager._ensureRichTextEditor()
14723      .catch(error => console.warn('Failed to preload Rich Text Editor via PageManager:', error));
14724  }
14725
14726  return null;
14727}
14728
14729preloadRichTextEditor();
14730setTimeout(preloadRichTextEditor, 1000);
14731setTimeout(preloadRichTextEditor, 3000);
14732});
14733
14734document.addEventListener('DOMContentLoaded', function() {
14735const submenuCheck = document.getElementById('createSubmenu');
14736const submenuTitleField = document.getElementById('submenuTitle');
14737const submenuTitleGroup = submenuTitleField?.parentElement;
14738
14739if (submenuCheck && submenuTitleGroup) {
14740submenuCheck.addEventListener('change', function() {
14741  submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
14742});
14743}
14744});
14745
14746// Also check immediately if document is already loaded (fallback only — avoid duplicate loadPage vs bootstrap)
14747if (document.readyState !== 'loading') {
14748setTimeout(function() {
14749console.log('Document already loaded, checking for deep links');
14750var __p2 = window.location.pathname;
14751if (__p2 !== '/' && __p2 !== '/_manage' && __p2 !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14752  handleURLNavigation();
14753}
14754}, 300);
14755}
14756
14757// ============================================
14758// CONCURRENT EDIT CONFLICT DETECTION
14759// ============================================
14760(function() {
14761  // Initialize local timestamp tracking
14762  window.lastKnownConfigTimestamp = null;
14763  let conflictPollInterval = null;
14764  
14765  // Initialize timestamp on page load
14766  async function initializeConfigTimestamp() {
14767    try {
14768      const timestampUrl = typeof getApiUrl === 'function' ? 
14769        getApiUrl('/api/config-timestamp') : 
14770        '/api/config-timestamp';
14771      console.error('[CONFLICT DETECT] Initializing config timestamp from:', timestampUrl);
14772      const response = await fetch(timestampUrl);
14773      if (response.ok) {
14774        const data = await response.json();
14775        if (data.lastModified) {
14776          window.lastKnownConfigTimestamp = data.lastModified;
14777          console.error('[CONFLICT DETECT] Initialized config timestamp:', window.lastKnownConfigTimestamp);
14778        } else {
14779          console.error('[CONFLICT DETECT] No lastModified in response:', data);
14780        }
14781      } else {
14782        console.error('[CONFLICT DETECT] Failed to initialize timestamp, status:', response.status);
14783      }
14784    } catch (error) {
14785      console.error('[CONFLICT DETECT] Could not initialize config timestamp:', error);
14786    }
14787  }
14788  
14789  // Check for conflicts by polling server
14790  async function checkForConflicts() {
14791    // Only poll when in edit mode
14792    if (!document.body.classList.contains('edit-mode-active')) {
14793      console.error('[CONFLICT DETECT] Skipping poll - not in edit mode');
14794      return;
14795    }
14796    
14797    try {
14798      const timestampUrl = typeof getApiUrl === 'function' ? 
14799        getApiUrl('/api/config-timestamp') : 
14800        '/api/config-timestamp';
14801      console.error('[CONFLICT DETECT] Polling for conflicts from:', timestampUrl);
14802      console.error('[CONFLICT DETECT] Current local timestamp:', window.lastKnownConfigTimestamp);
14803      
14804      const response = await fetch(timestampUrl);
14805      
14806      if (response.ok) {
14807        const data = await response.json();
14808        const serverTimestamp = data.lastModified;
14809        console.error('[CONFLICT DETECT] Server timestamp:', serverTimestamp);
14810        
14811        // If we have a local timestamp and it differs from server, show conflict
14812        if (window.lastKnownConfigTimestamp !== null && 
14813            serverTimestamp !== null && 
14814            serverTimestamp !== window.lastKnownConfigTimestamp) {
14815          console.error('[CONFLICT DETECT] ⚠️ CONFLICT DETECTED! Local:', window.lastKnownConfigTimestamp, 'Server:', serverTimestamp);
14816          showConflictIndicator();
14817        } else {
14818          console.error('[CONFLICT DETECT] No conflict - timestamps match or null');
14819          hideConflictIndicator();
14820        }
14821      } else {
14822        console.error('[CONFLICT DETECT] Poll failed with status:', response.status);
14823      }
14824    } catch (error) {
14825      console.error('[CONFLICT DETECT] Error checking for conflicts:', error);
14826    }
14827  }
14828  
14829  // Show conflict indicator icon
14830  function showConflictIndicator() {
14831    const indicator = document.getElementById('conflictIndicator');
14832    console.error('[CONFLICT DETECT] Showing conflict indicator, element found:', !!indicator);
14833    if (indicator) {
14834      indicator.style.display = 'flex';
14835      indicator.style.visibility = 'visible';
14836      indicator.style.opacity = '1';
14837      indicator.style.background = '#d32f2f';
14838      indicator.style.borderColor = '#d32f2f';
14839      console.error('[CONFLICT DETECT] Conflict indicator displayed');
14840    } else {
14841      console.error('[CONFLICT DETECT] ⚠️ Conflict indicator element not found in DOM!');
14842    }
14843  }
14844  
14845  // Hide conflict indicator icon
14846  function hideConflictIndicator() {
14847    const indicator = document.getElementById('conflictIndicator');
14848    if (indicator) {
14849      indicator.style.display = 'none';
14850      indicator.style.visibility = 'hidden';
14851      indicator.style.opacity = '0';
14852    }
14853  }
14854  
14855  // Start/stop polling based on edit mode
14856  function updatePolling() {
14857    const isEditMode = document.body.classList.contains('edit-mode-active');
14858    
14859    // Always hide indicator when not in edit mode
14860    if (!isEditMode) {
14861      hideConflictIndicator();
14862    }
14863    
14864    if (isEditMode && !conflictPollInterval) {
14865      // Start polling every 10 seconds
14866      conflictPollInterval = setInterval(checkForConflicts, 10000);
14867      console.error('[CONFLICT DETECT] ✅ Started conflict detection polling (every 10s)');
14868      
14869      // Also check immediately
14870      checkForConflicts();
14871    } else if (!isEditMode && conflictPollInterval) {
14872      // Stop polling when not in edit mode
14873      clearInterval(conflictPollInterval);
14874      conflictPollInterval = null;
14875      hideConflictIndicator();
14876      console.error('[CONFLICT DETECT] Stopped conflict detection polling');
14877    }
14878  }
14879  
14880  // Initialize on page load
14881  if (document.readyState === 'loading') {
14882    document.addEventListener('DOMContentLoaded', function() {
14883      initializeConfigTimestamp();
14884      // Watch for edit mode changes
14885      const editModeObserver = new MutationObserver(updatePolling);
14886      editModeObserver.observe(document.body, { 
14887        attributes: true, 
14888        attributeFilter: ['class'] 
14889      });
14890      updatePolling();
14891    });
14892  } else {
14893    initializeConfigTimestamp();
14894    // Watch for edit mode changes
14895    const editModeObserver = new MutationObserver(updatePolling);
14896    editModeObserver.observe(document.body, { 
14897      attributes: true, 
14898      attributeFilter: ['class'] 
14899    });
14900    updatePolling();
14901  }
14902})();
14903
14904</script>
14904
14905</head>
14906<body class="menu-layout-horizontal">
14907  <a class="hydra-skip-link" href="#hydra-main-content">Skip to content</a>
14908<nav class="hydra-seo-nav" aria-label="Site navigation">
14909  <ul>
14910    <li><a href="/">PEOPLE</a></li>
14911    <li><a href="/athletes">ATHLETES</a></li>
14912    <li><a href="/higher-ed">HIGHER ED</a></li>
14913    <li><a href="/branding">BRANDING</a></li>
14914    <li><a href="/places--things">PLACES + THINGS</a></li>
14915    <li><a href="/about">ABOUT</a></li>
14916    <li><a href="/contact">CONTACT</a></li>
14917  </ul>
14918</nav>
14919<noscript class="hydra-gallery-noscript">
14920  <article>
14921    <h1>ATHLETES | People - Adena Stevens Photography</h1>
14922    
14923    <ul class="gallery-images">
14924    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg" alt="f21s3uk4 an5m6dbk jdi7x8 Athletes 001 basketballA" loading="lazy"></li>
14925    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_8bymb5_Athletes-002_stevens_018.jpg" alt="f21s3uk4 an5m6dbk 8bymb5 Athletes 002 stevens 018" loading="lazy"></li>
14926    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_kbdx46_Athletes-003_hsgsoc099d-1920w.jpg" alt="f21s3uk4 an5m6dbk kbdx46 Athletes 003 hsgsoc099d 1920w" loading="lazy"></li>
14927    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ggcqhp_Athletes-004_boys_soccer-1920w.jpg" alt="f21s3uk4 an5m6dbk ggcqhp Athletes 004 boys soccer 1920w" loading="lazy"></li>
14928    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_a9ajqg_Athletes-005_sports04.jpg" alt="f21s3uk4 an5m6dbk a9ajqg Athletes 005 sports04" loading="lazy"></li>
14929    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_40o8tu_Athletes-006_softball2-1920w.jpg" alt="f21s3uk4 an5m6dbk 40o8tu Athletes 006 softball2 1920w" loading="lazy"></li>
14930    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_pfew5a_Athletes-007_basketball3.jpg" alt="f21s3uk4 an5m6dbk pfew5a Athletes 007 basketball3" loading="lazy"></li>
14931    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_qy7wn9_Athletes-008_college-basketball-01.jpg" alt="f21s3uk4 an5m6dbk qy7wn9 Athletes 008 college basketball 01" loading="lazy"></li>
14932    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ds305y_Athletes-010_sports07.jpg" alt="f21s3uk4 an5m6dbk ds305y Athletes 010 sports07" loading="lazy"></li>
14933    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_vbj1zs_Athletes-011_volleyballA-1920w.jpg" alt="f21s3uk4 an5m6dbk vbj1zs Athletes 011 volleyballA 1920w" loading="lazy"></li>
14934    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_3yl151_Athletes-012_baseball3.jpg" alt="f21s3uk4 an5m6dbk 3yl151 Athletes 012 baseball3" loading="lazy"></li>
14935    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_60t74z_Athletes-013_volleyball-1920w.jpg" alt="f21s3uk4 an5m6dbk 60t74z Athletes 013 volleyball 1920w" loading="lazy"></li>
14936    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_f2yfb3_Athletes-014_golfAS0531a-1920w.jpg" alt="f21s3uk4 an5m6dbk f2yfb3 Athletes 014 golfAS0531a 1920w" loading="lazy"></li>
14937    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_o2kuzl_Athletes-015_christine_caron.jpg" alt="f21s3uk4 an5m6dbk o2kuzl Athletes 015 christine caron" loading="lazy"></li>
14938    <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_r6n52d_Athletes-016_softball3.jpg" alt="f21s3uk4 an5m6dbk r6n52d Athletes 016 softball3" loading="lazy"></li>
14939  </ul>
14940    <p><a href="https://adenastevens.com/athletes">View full gallery</a></p>
14941  </article>
14942</noscript>
14943
14944  <div class="global-header-controls">
14945    
14946    <button id="editButton" class="btn-icon btn-primary" onclick="toggleEditMode()" style="display: none;" data-help-id="edit-toggle">
14947       <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14948        <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
14949          <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"/>
14950          <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"/>
14951        </g>
14952      </svg>
14953    </button>
14954    <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">
14955      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="color: white;">
14956        <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"/>
14957      </svg>
14958    </button>
14959  </div>
14960
14961  <div class="container">
14962    <div class="mobile-header">
14963      <div class="mobile-header-content">
14964        <div class="horizontal-header-logo"></div>
14965        <div class="horizontal-menu-container"></div>
14966      </div>
14967      <button id="hamburgerBtn" class="hamburger-btn" type="button" aria-label="Menu" aria-expanded="false" aria-controls="siteSidebar">
14968        <span class="hamburger-icon" aria-hidden="true">
14969          <span class="hamburger-line"></span>
14970          <span class="hamburger-line"></span>
14971          <span class="hamburger-line"></span>
14972        </span>
14973      </button>
14974    </div>
14975    <div id="mobileBackdrop" class="mobile-backdrop"></div>
14976
14977    <div class="sidebar" id="siteSidebar" role="navigation" aria-label="Site">
14978      <div class="sidebar-header">
14979</div>
14980
14981  
14982  <div class="edit-controls" data-help-id="edit-toolbar">
14983  <button type="button" class="btn-icon btn-primary" onclick="openHydraHelp()" data-custom-tooltip="Help" data-help-id="help-toolbar">
14984    <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
14985      <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" />
14986    </svg>
14987  </button>
14988  <button class="btn-icon btn-primary" onclick="toggleStyleEditor(undefined, event)" data-custom-tooltip="Edit Style" data-help-id="style-toolbar">
14989    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14990      <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"/>
14991    </svg>
14992  </button>
14993  <button class="btn-icon btn-primary" onclick="toggleAddForm()" data-custom-tooltip="Add">
14994    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
14995      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
14996        <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"/>
14997        <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"/>
14998      </g>
14999    </svg>
15000  </button>
15001  <button class="btn-icon btn-primary" onclick="toggleSidebarElementForm()" data-custom-tooltip="Add Element">
15002    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15003      <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"/>
15004    </svg>
15005  </button>
15006<button class="btn-icon btn-primary" onclick="toggleMetadataEditor()" data-custom-tooltip="Site Metadata">
15007<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.
15007113 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>
15008</button>
15009<button id="logoutButton" class="btn-icon btn-primary" onclick="logout()" data-custom-tooltip="Log Out" style="display: none;">
15010      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15011        <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"/>
15012      </svg>
15013    </button>
15014    
15014<script>
15015
15016// Initialize mobile menu functionality
15017function initMobileMenu() {
15018  const hamburgerBtn = document.getElementById('hamburgerBtn');
15019  const sidebar = document.querySelector('.sidebar');
15020  const mobileBackdrop = document.getElementById('mobileBackdrop');
15021
15022  // --- ADDED CHECK: Only update header if layout is NOT horizontal ---
15023  let currentLayout = 'sidebar'; // Default
15024  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
15025      currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
15026  }
15027
15028  // --- END ADDED CHECK ---
15029
15030  // Ensure backdrop is properly hidden for horizontal layout on page load
15031  if (mobileBackdrop && document.body.classList.contains('menu-layout-horizontal')) {
15032    mobileBackdrop.classList.remove('mobile-open');
15033    mobileBackdrop.classList.remove('active');
15034    // Clear any inline overrides - CSS controls backdrop visibility via .mobile-open
15035    mobileBackdrop.style.display = '';
15036    mobileBackdrop.style.pointerEvents = '';
15037    mobileBackdrop.style.opacity = '';
15038    mobileBackdrop.style.visibility = '';
15039  }
15040
15041  // Toggle mobile menu when hamburger is clicked
15042  hamburgerBtn.addEventListener('click', function(e) {
15043    e.stopPropagation();
15044    toggleMobileMenu(); // Assumes this function exists
15045  });
15046
15047  // Close mobile menu when backdrop is clicked
15048  if (mobileBackdrop) {
15049    mobileBackdrop.addEventListener('click', function() {
15050      closeMobileMenu(); // Assumes this function exists
15051    });
15052  }
15053
15054  // Escape closes mobile menu (keyboard / SR)
15055  document.addEventListener('keydown', function(e) {
15056    if (e.key !== 'Escape') return;
15057    const sb = document.querySelector('.sidebar');
15058    if (sb && sb.classList.contains('mobile-open') && typeof closeMobileMenu === 'function') {
15059      closeMobileMenu();
15060    }
15061  });
15062
15063  // Handle window resize events to ensure proper state
15064  window.addEventListener('resize', function() {
15065    // If we're not on mobile anymore, remove mobile-open class
15066    if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
15067      closeMobileMenu(); // Assumes this function exists
15068    }
15069  });
15070}
15071
15072document.addEventListener('keydown', function(e) {
15073  // Check for caret (Shift+6)
15074  if (e.key === '^' || (e.key === '6' && e.shiftKey)) {
15075    // Only work in edit mode
15076    if (window.isInEditMode && window.isInEditMode()) {
15077      console.log('Import shortcut detected');
15078      toggleImportClassicForm();
15079      e.preventDefault();
15080    }
15081  }
15082});
15083
15084// Enhanced tooltip management with automatic timeouts
15085
15086// Store active tooltip timeouts by ID
15087window.tooltipTimeouts = {};
15088
15089function showTooltip(trigger) {
15090    const tooltipText = trigger.getAttribute('data-custom-tooltip');
15091    if (!tooltipText) return;
15092
15093    // Get or create the main tooltip container
15094    let container = document.getElementById('tooltip-container');
15095    if (!container) {
15096        console.log("Tooltip container not found, creating one.");
15097        container = document.createElement('div'); // Assign to 'container'
15098        container.id = 'tooltip-container';
15099        // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc.
15100        document.body.appendChild(container);
15101    }
15102
15103    const tooltipId = `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`;
15104
15105    // Remove any existing tooltip for this trigger
15106    const existingTooltip = document.getElementById(tooltipId);
15107    if (existingTooltip) {
15108        existingTooltip.remove();
15109    }
15110
15111    // Clear any existing timeout for this tooltip
15112    if (!window.tooltipTimeouts) {
15113        window.tooltipTimeouts = {}; // Initialize if it doesn't exist
15114    }
15115    
15116    if (window.tooltipTimeouts[tooltipId]) {
15117        clearTimeout(window.tooltipTimeouts[tooltipId]);
15118        delete window.tooltipTimeouts[tooltipId];
15119    }
15120
15121    // Create tooltip element
15122    const tooltip = document.createElement('div');
15123    tooltip.className = 'tooltip'; // Ensure your CSS styles .tooltip
15124    tooltip.id = tooltipId;
15125    tooltip.textContent = tooltipText;
15126
15127    // Add to container (now 'container' is guaranteed to be an element)
15128    container.appendChild(tooltip);
15129
15130    // Position tooltip
15131    if (typeof positionTooltip === 'function') {
15132        positionTooltip(tooltip, trigger);
15133    } else {
15134        console.error("positionTooltip function is not defined.");
15135        tooltip.remove(); // Clean up if we can't position
15136        return;
15137    }
15138
15139    // Show with animation (slight delay for CSS transition)
15140    setTimeout(() => {
15141        tooltip.style.opacity = '1';
15142        // The transform for animation should be handled by CSS transitions
15143        // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); }
15144        // Or ensure positionTooltip sets final transform if it needs to override CSS.
15145        // For now, assuming CSS handles the animated transform from its initial state.
15146    }, 10);
15147
15148    trigger.setAttribute('data-active-tooltip', tooltipId);
15149
15150    // Set automatic timeout to hide after 3 seconds
15151    window.tooltipTimeouts[tooltipId] = setTimeout(() => {
15152        hideTooltip(trigger); // Ensure hideTooltip function exists
15153    }, 3000);
15154    
15155    // Also set up a global timer to ensure all tooltips are dismissed
15156    if (window.globalTooltipTimer) {
15157        clearTimeout(window.globalTooltipTimer);
15158    }
15159    window.globalTooltipTimer = setTimeout(() => {
15160        // Force hide all visible tooltips after 3 seconds
15161        const allTooltips = document.querySelectorAll('.tooltip');
15162        allTooltips.forEach(tooltip => {
15163            if (tooltip.style.opacity !== '0') {
15164                tooltip.style.opacity = '0';
15165                setTimeout(() => {
15166                    if (tooltip.parentNode) {
15167                        tooltip.parentNode.removeChild(tooltip);
15168                    }
15169                }, 200);
15170            }
15171        });
15172        // Clear all timeout references
15173        if (window.tooltipTimeouts) {
15174            for (const id in window.tooltipTimeouts) {
15175                clearTimeout(window.tooltipTimeouts[id]);
15176            }
15177            window.tooltipTimeouts = {};
15178        }
15179    }, 3000);
15180}
15181
15182// Function to hide a tooltip
15183function hideTooltip(trigger) {
15184  const tooltipId = trigger.getAttribute('data-active-tooltip');
15185  if (!tooltipId) return;
15186  
15187  const tooltip = document.getElementById(tooltipId);
15188  if (tooltip) {
15189    // Fade out
15190    tooltip.style.opacity = '0';
15191    tooltip.style.transform = 'translateX(-50%) translateY(0)';
15192    
15193    // Remove after animation
15194    setTimeout(() => {
15195      if (tooltip.parentNode) {
15196        tooltip.parentNode.removeChild(tooltip);
15197      }
15198    }, 200);
15199  }
15200  
15201  // Clear any active timeout for this tooltip
15202  if (window.tooltipTimeouts && window.tooltipTimeouts[tooltipId]) {
15203    clearTimeout(window.tooltipTimeouts[tooltipId]);
15204    delete window.tooltipTimeouts[tooltipId];
15205  }
15206  
15207  // Clear reference
15208  trigger.removeAttribute('data-active-tooltip');
15209  
15210  // If this was the last tooltip, clear the global timer
15211  const remainingTooltips = document.querySelectorAll('.tooltip');
15212  if (remainingTooltips.length === 0 && window.globalTooltipTimer) {
15213    clearTimeout(window.globalTooltipTimer);
15214    window.globalTooltipTimer = null;
15215  }
15216}
15217
15218// Function to initialize tooltips across the document
15219function initTooltipSystem() {
15220  // Check if container already exists
15221  let tooltipContainer = document.getElementById('tooltip-container');
15222  
15223  // Create it if it doesn't exist
15224  if (!tooltipContainer) {
15225    tooltipContainer = document.createElement('div');
15226    tooltipContainer.id = 'tooltip-container';
15227    document.body.appendChild(tooltipContainer);
15228  }
15229  
15230  // Find all elements with data-custom-tooltip attribute
15231  const tooltipTriggers = document.querySelectorAll('[data-custom-tooltip]');
15232  
15233  // Add event listeners to each trigger
15234  tooltipTriggers.forEach(trigger => {
15235    // Skip if already initialized
15236    if (trigger.hasAttribute('data-custom-tooltip-initialized')) return;
15237    
15238    // Mark as initialized
15239    trigger.setAttribute('data-custom-tooltip-initialized', 'true');
15240    
15241    // MouseEnter: Show tooltip
15242    trigger.addEventListener('mouseenter', () => {
15243      showTooltip(trigger);
15244    });
15245    
15246    // MouseLeave: Hide tooltip
15247    trigger.addEventListener('mouseleave', () => {
15248      hideTooltip(trigger);
15249    });
15250    
15251    // Click: Hide tooltip immediately
15252    trigger.addEventListener('click', () => {
15253      hideTooltip(trigger);
15254    });
15255    
15256    // Also handle focus/blur for accessibility
15257    trigger.addEventListener('focus', () => {
15258      showTooltip(trigger);
15259    });
15260    
15261    trigger.addEventListener('blur', () => {
15262      hideTooltip(trigger);
15263    });
15264  });
15265  
15266  // Set up a failsafe function to clear any orphaned tooltips
15267  window.clearAllTooltips = function() {
15268    const container = document.getElementById('tooltip-container');
15269    if (container) {
15270      container.innerHTML = '';
15271    }
15272    
15273    // Clear all timeouts
15274    if (window.tooltipTimeouts) {
15275      for (const id in window.tooltipTimeouts) {
15276        clearTimeout(window.tooltipTimeouts[id]);
15277      }
15278      window.tooltipTimeouts = {};
15279    }
15280    
15281    // Clear global timer
15282    if (window.globalTooltipTimer) {
15283      clearTimeout(window.globalTooltipTimer);
15284      window.globalTooltipTimer = null;
15285    }
15286    
15287    // Clear all data-active-tooltip attributes
15288    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15289      el.removeAttribute('data-active-tooltip');
15290    });
15291  };
15292  
15293  // Initialize new tooltips when DOM changes
15294  setupMutationObserver();
15295  
15296  // Add a global function to manually dismiss all tooltips (for testing)
15297  window.dismissAllTooltips = function() {
15298    const allTooltips = document.querySelectorAll('.tooltip');
15299    allTooltips.forEach(tooltip => {
15300      if (tooltip.style.opacity !== '0') {
15301        tooltip.style.opacity = '0';
15302        setTimeout(() => {
15303          if (tooltip.parentNode) {
15304            tooltip.parentNode.removeChild(tooltip);
15305          }
15306        }, 200);
15307      }
15308    });
15309    
15310    // Clear all timeout references
15311    if (window.tooltipTimeouts) {
15312      for (const id in window.tooltipTimeouts) {
15313        clearTimeout(window.tooltipTimeouts[id]);
15314      }
15315      window.tooltipTimeouts = {};
15316    }
15317    
15318    // Clear global timer
15319    if (window.globalTooltipTimer) {
15320      clearTimeout(window.globalTooltipTimer);
15321      window.globalTooltipTimer = null;
15322    }
15323    
15324    // Clear all data-active-tooltip attributes
15325    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15326      el.removeAttribute('data-active-tooltip');
15327    });
15328  };
15329}
15330
15331function positionTooltip(tooltip, trigger) {
15332    const triggerRect = trigger.getBoundingClientRect(); // Viewport-relative coordinates
15333
15334    // To accurately get tooltip dimensions, temporarily make it visible but off-screen.
15335    // The #tooltip-container is position:fixed, so the tooltip's absolute positioning
15336    // will be relative to the viewport.
15337    tooltip.style.visibility = 'hidden'; 
15338    tooltip.style.display = 'block'; // Or 'inline-block' if that's its intended final display
15339    // No need to change position to 'absolute' here if its CSS is already 'position: absolute;'
15340    // within the 'fixed' #tooltip-container.
15341
15342    const tooltipHeight = tooltip.offsetHeight;
15343    const tooltipWidth = tooltip.offsetWidth;
15344
15345    // Restore visibility; opacity transition will handle the fade-in.
15346    tooltip.style.visibility = ''; 
15347    // tooltip.style.display = ''; // Let CSS manage display if opacity is used for showing/hiding
15348
15349    // Check if tooltip should be positioned to the right (for conflict indicator)
15350    const tooltipPosition = trigger.getAttribute('data-tooltip-position');
15351    if (tooltipPosition === 'right') {
15352        // Position to the right of the trigger
15353        const viewportWidth = window.innerWidth;
15354        const viewportHeight = window.innerHeight;
15355        let left = triggerRect.right + 10; // 10px gap from the right edge of trigger
15356        let top = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2); // Vertically centered
15357        
15358        // Recalculate tooltip dimensions after allowing text wrap
15359        tooltip.style.maxWidth = '200px';
15360        tooltip.style.whiteSpace = 'normal';
15361        tooltip.style.wordWrap = 'break-word';
15362        const tooltipWidthAfterWrap = tooltip.offsetWidth;
15363        const tooltipHeightAfterWrap = tooltip.offsetHeight;
15364        
15365        // If tooltip goes off-screen to the right, try positioning to the left of the button
15366        if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15367            left = triggerRect.left - tooltipWidthAfterWrap - 10; // Position to the left of button
15368            // If it still goes off the left edge, position it at the left edge with padding
15369            if (left < 5) {
15370                left = 5; // Keep it at least 5px from the left edge
15371                // The tooltip will wrap to fit within the available space
15372                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15373            }
15374        } else {
15375            // Tooltip fits on the right, but make sure it doesn't go off-screen
15376            if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15377                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15378            }
15379        }
15380        
15381        // Recalculate after max-width adjustment
15382        const finalTooltipWidth = tooltip.offsetWidth;
15383        const finalTooltipHeight = tooltip.offsetHeight;
15384        
15385        // Ensure tooltip doesn't go off-screen on the right side
15386        if (left + finalTooltipWidth > viewportWidth - 5) {
15387            left = viewportWidth - finalTooltipWidth - 5;
15388        }
15389        
15390        // Adjust if tooltip goes off-screen vertically
15391        if (top < 5) {
15392            top = 5;
15393        } else if (top + finalTooltipHeight > viewportHeight - 5) {
15394            top = viewportHeight - finalTooltipHeight - 5;
15395        }
15396        
15397        tooltip.style.left = `${left}px`;
15398        tooltip.style.top = `${top}px`;
15399        tooltip.classList.remove('tooltip-bottom'); // No arrow needed for side positioning
15400        return;
15401    }
15402
15403    // Calculate desired horizontal center point of the trigger
15404    const triggerCenter = triggerRect.left + triggerRect.width / 2;
15405    
15406    // Calculate tooltip's left position to center it under/over trigger's center
15407    let left = triggerCenter - tooltipWidth / 2;
15408
15409    // Default position: above the trigger
15410    // Arrow height (8px) + desired gap (e.g., 2px) = 10px total offset
15411    let top = triggerRect.top - tooltipHeight - 10; 
15412    tooltip.classList.remove('tooltip-bottom'); // Manages arrow direction via CSS
15413
15414    // Check if it goes off-screen at the top (add a small buffer, e.g., 5px)
15415    if (top < 5) { 
15416        // Position below the trigger
15417        top = triggerRect.bottom + 10; // 10px gap (arrow height + small space)
15418        tooltip.classList.add('tooltip-bottom');
15419    }
15420
15421    // Boundary checks for left position to prevent going off-screen horizontally
15422    const viewportWidth = window.innerWidth;
15423    if (left < 5) { // 5px buffer from left edge
15424        left = 5; 
15425    } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge
15426        left = viewportWidth - tooltipWidth - 5;
15427    }
15428
15429    tooltip.style.left = `${left}px`;
15430    tooltip.style.top = `${top}px`;
15431
15432    // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering.
15433    // Since JS now calculates the exact 'left' for centering,
15434    // we can clear any such transform that might interfere.
15435    // However, if your CSS animation relies on a transform, adjust accordingly.
15436    // If your CSS only uses opacity for transition, this is fine:
15437    tooltip.style.transform = ''; 
15438}
15439
15440// Watch for new tooltip triggers added to the DOM
15441function setupMutationObserver() {
15442  // Check if MutationObserver is available
15443  if (!window.MutationObserver) return;
15444  
15445  // Create observer instance
15446  const observer = new MutationObserver((mutations) => {
15447    let shouldInit = false;
15448    
15449    // Check if relevant nodes were added
15450    mutations.forEach(mutation => {
15451      if (mutation.type === 'childList') {
15452        mutation.addedNodes.forEach(node => {
15453          // Check if it's an Element and has data-custom-tooltip or contains elements with data-custom-tooltip
15454          if (node.nodeType === 1) { // ELEMENT_NODE
15455            if (node.hasAttribute && node.hasAttribute('data-custom-tooltip')) {
15456              shouldInit = true;
15457            } else if (node.querySelectorAll) {
15458              const tooltips = node.querySelectorAll('[data-custom-tooltip]');
15459              if (tooltips.length) shouldInit = true;
15460            }
15461          }
15462        });
15463      }
15464    });
15465    
15466    // If relevant nodes found, reinitialize tooltips
15467    if (shouldInit) {
15468      initTooltipSystem();
15469    }
15470  });
15471  
15472  // Start observing the entire document with the configured parameters
15473  observer.observe(document.body, { 
15474    childList: true, 
15475    subtree: true 
15476  });
15477}
15478
15479// Initialize when DOM is ready
15480document.addEventListener('DOMContentLoaded', initTooltipSystem);
15481
15482// Also initialize immediately if document already loaded
15483if (document.readyState === 'complete' || document.readyState === 'interactive') {
15484  initTooltipSystem();
15485}
15486
15487// Add a global safety function that runs every minute to clear any tooltips that might have gotten stuck
15488setInterval(() => {
15489  // Get all visible tooltips
15490  const visibleTooltips = document.querySelectorAll('.tooltip[style*="opacity: 1"]');
15491  
15492  // If any are found, check how long they've been visible
15493  if (visibleTooltips.length > 0) {
15494    console.log('Found visible tooltips during cleanup check, clearing them');
15495    window.clearAllTooltips();
15496  }
15497}, 5000); // 
15498
15499// Mobile drawer a11y: Tab cycle + restore focus. No visual/mouse behavior changes.
15500var _mobileMenuFocusTrapHandler = null;
15501
15502function getMobileMenuFocusables(sidebar) {
15503  if (!sidebar) return [];
15504  return Array.prototype.filter.call(
15505    sidebar.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
15505([tabindex="-1"])'),
15506    function (el) {
15507      if (el.getAttribute('aria-hidden') === 'true') return false;
15508      var style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
15509      if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
15510      return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement;
15511    }
15512  );
15513}
15514
15515function releaseMobileMenuFocusTrap() {
15516  if (_mobileMenuFocusTrapHandler) {
15517    document.removeEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15518    _mobileMenuFocusTrapHandler = null;
15519  }
15520}
15521
15522function installMobileMenuFocusTrap(sidebar) {
15523  releaseMobileMenuFocusTrap();
15524  if (!sidebar) return;
15525  _mobileMenuFocusTrapHandler = function (event) {
15526    if (event.key !== 'Tab') return;
15527    var sb = document.querySelector('.sidebar');
15528    if (!sb || !sb.classList.contains('mobile-open')) return;
15529    var focusables = getMobileMenuFocusables(sb);
15530    var hamburger = document.getElementById('hamburgerBtn');
15531    // Include hamburger so Shift+Tab from first item returns to the control that opened the drawer
15532    var cycle = hamburger ? [hamburger].concat(focusables.filter(function (el) { return el !== hamburger; })) : focusables;
15533    if (!cycle.length) return;
15534    var first = cycle[0];
15535    var last = cycle[cycle.length - 1];
15536    if (event.shiftKey) {
15537      if (document.activeElement === first || (sb.contains(document.activeElement) === false && document.activeElement !== hamburger)) {
15538        event.preventDefault();
15539        last.focus();
15540      }
15541    } else if (document.activeElement === last) {
15542      event.preventDefault();
15543      first.focus();
15544    }
15545  };
15546  document.addEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15547}
15548
15549// Toggle mobile menu state
15550function toggleMobileMenu() {
15551const sidebar = document.querySelector('.sidebar');
15552const hamburgerBtn = document.getElementById('hamburgerBtn');
15553const mobileBackdrop = document.getElementById('mobileBackdrop');
15554
15555// Check if we're in horizontal layout mode
15556const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15557
15558sidebar.classList.toggle('mobile-open');
15559hamburgerBtn.classList.toggle('active');
15560const isOpen = sidebar.classList.contains('mobile-open');
15561hamburgerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
15562
15563if (mobileBackdrop) {
15564  if (isHorizontalLayout) {
15565    // For horizontal layout on mobile, use normal backdrop (tap outside to close)
15566    // Clear any inline overrides so CSS can control visibility
15567    mobileBackdrop.style.display = '';
15568    mobileBackdrop.style.pointerEvents = '';
15569    mobileBackdrop.style.opacity = '';
15570    mobileBackdrop.style.visibility = '';
15571    mobileBackdrop.classList.toggle('mobile-open', isOpen);
15572    setTimeout(() => {
15573      mobileBackdrop.classList.toggle('active', isOpen);
15574    }, 10);
15575  } else {
15576    // For other layouts, use normal backdrop behavior
15577    mobileBackdrop.classList.toggle('mobile-open');
15578    // Slight delay to show opacity transition
15579    setTimeout(() => {
15580      mobileBackdrop.classList.toggle('active', isOpen);
15581    }, 10);
15582  }
15583}
15584
15585// Prevent body scrolling when menu is open
15586document.body.style.overflow = isOpen ? 'hidden' : '';
15587
15588if (isOpen) {
15589  // Trap Tab only — do not steal focus on mouse/touch open (avoids focus rings / scroll jumps)
15590  installMobileMenuFocusTrap(sidebar);
15591} else {
15592  releaseMobileMenuFocusTrap();
15593  // Restore focus only if keyboard focus was inside the drawer
15594  if (hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15595    try { hamburgerBtn.focus(); } catch (e) {}
15596  }
15597}
15598}
15599
15600// Close the mobile menu
15601function closeMobileMenu() {
15602const sidebar = document.querySelector('.sidebar');
15603const hamburgerBtn = document.getElementById('hamburgerBtn');
15604const mobileBackdrop = document.getElementById('mobileBackdrop');
15605const wasOpen = sidebar && sidebar.classList.contains('mobile-open');
15606
15607// Check if we're in horizontal layout mode
15608const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15609
15610sidebar.classList.remove('mobile-open');
15611hamburgerBtn.classList.remove('active');
15612if (hamburgerBtn) hamburgerBtn.setAttribute('aria-expanded', 'false');
15613
15614if (mobileBackdrop) {
15615  if (isHorizontalLayout) {
15616    // For horizontal layout, clear inline overrides and remove mobile-open
15617    mobileBackdrop.style.display = '';
15618    mobileBackdrop.style.pointerEvents = '';
15619    mobileBackdrop.style.opacity = '';
15620    mobileBackdrop.style.visibility = '';
15621    mobileBackdrop.classList.remove('mobile-open');
15622    mobileBackdrop.classList.remove('active');
15623  } else {
15624    // For other layouts, use normal backdrop behavior
15625    mobileBackdrop.classList.remove('active');
15626    setTimeout(() => {
15627      mobileBackdrop.classList.remove('mobile-open');
15628    }, 300); // Match transition duration
15629  }
15630}
15631
15632// Re-enable body scrolling
15633document.body.style.overflow = '';
15634releaseMobileMenuFocusTrap();
15635// Return focus to hamburger only if focus was inside the drawer (keyboard/SR); mouse users unchanged
15636if (wasOpen && hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15637  try { hamburgerBtn.focus(); } catch (e) {}
15638}
15639}
15640
15641function updateMobileHeaderContent() {
15642    const currentLayout = typeof getEffectiveMenuLayout === 'function'
15643        ? getEffectiveMenuLayout()
15644        : (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
15645            ? window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'
15646            : (document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 'sidebar'));
15647
15648    const isMobileView = window.innerWidth <= 768; // Assuming 768px is your mobile breakpoint
15649
15650    const mobileHeaderContent = document.querySelector('.mobile-header-content');
15651    if (!mobileHeaderContent) {
15652        console.warn("updateMobileHeaderContent: .mobile-header-content element not found.");
15653        return;
15654    }
15655    
15656    // On mobile, center the content unless Space Colony (left-aligned title)
15657    if (isMobileView) {
15658        const isSpaceColonyTheme = typeof isSpaceColonySiteTheme === 'function' && isSpaceColonySiteTheme();
15659        mobileHeaderContent.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15660        // Also ensure logo-menu-container doesn't grow
15661        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
15662        if (logoMenuContainer) {
15663            logoMenuContainer.style.flexGrow = '0';
15664            logoMenuContainer.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15665        }
15666    }
15667
15668    console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`);
15669
15670    // Clear previous generic mobile content (like centered logo/title)
15671    const existingMobileImage = mobileHeaderContent.querySelector('.mobile-header-image');
15672    const existingMobileText = mobileHeaderContent.querySelector('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title');
15673    if (existingMobileImage) {
15674        // The img is always appended directly (never wrapped in an <a>). Remove it straight.
15675        // Also handle any legacy <a>-wrapped images left from older deployments.
15676        const imageParent = existingMobileImage.parentElement;
15677        if (imageParent && imageParent.tagName === 'A' && imageParent.parentElement === mobileHeaderContent) {
15678            imageParent.remove();
15679        } else {
15680            existingMobileImage.remove();
15681        }
15682    }
15683    // Sweep any stray empty <a> tags left by older deployments that used the wrapper approach
15684    Array.from(mobileHeaderContent.children).forEach(function(child) {
15685        if (child.tagName === 'A' && !child.querySelector('.mobile-header-image') && !(child.textContent || '').trim()) {
15686            child.remove();
15687        }
15688    });
15689    if (existingMobileText) existingMobileText.remove();
15690    
15691    if (currentLayout === 'horizontal' && !isMobileView) {
15692        // Desktop Horizontal Layout:
15693        // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo.
15694        // The menu items are in .horizontal-menu-container.
15695        // Remove stray mobile-only title/image nodes (duplicate of horizontal-header-logo).
15696        mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title, .mobile-header-image').forEach(function(el) {
15697            el.remove();
15698        });
15699        console.log("updateMobileHeaderContent: Desktop horizontal layout, re-rendering horizontal menu to ensure sidebar elements are available.");
15700        if (typeof renderHorizontalMenu === 'function') {
15701            // Wait for sidebar elements to be available before rendering
15702            const waitForSidebarElements = (attempts = 0) => {
15703                const maxAttempts = 10;
15704                if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
15705                    console.log("updateMobileHeaderContent: Sidebar elements available, rendering horizontal menu.");
15706                    renderHorizontalMenu();
15707                } else if (attempts < maxAttempts) {
15708                    const attemptNum = attempts + 1;
15709                    console.log("updateMobileHeaderContent: Waiting for sidebar elements (attempt " + attemptNum + "/" + maxAttempts + ")...");
15710                    setTimeout(() => waitForSidebarElements(attempts + 1), 100);
15711                } else {
15712                    console.warn("updateMobileHeaderContent: Sidebar elements not available after waiting, rendering anyway.");
15713                    renderHorizontalMenu();
15714                }
15715            };
15716            waitForSidebarElements();
15717        }
15718        return; 
15719    }
15720    
15721    // For Horizontal layout on mobile, we need to clear the horizontal menu items
15722    // and show only the mobile header logo/title
15723    if (currentLayout === 'horizontal' && isMobileView) {
15724        console.log("updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo.");
15725        
15726        // Clear horizontal menu items from mobile header
15727        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu
15727-container');
15728        if (horizontalMenuContainer) {
15729            console.log("updateMobileHeaderContent: Clearing horizontal menu container");
15730            horizontalMenuContainer.innerHTML = '';
15731        } else {
15732            console.log("updateMobileHeaderContent: No horizontal menu container found to clear");
15733        }
15734        
15735        // Clear horizontal header logo from mobile header
15736        const horizontalHeaderLogo = mobileHeaderContent.querySelector('.horizontal-header-logo');
15737        if (horizontalHeaderLogo) {
15738            console.log("updateMobileHeaderContent: Clearing horizontal header logo");
15739            horizontalHeaderLogo.innerHTML = '';
15740        } else {
15741            console.log("updateMobileHeaderContent: No horizontal header logo found to clear");
15742        }
15743        
15744        // Also clear any existing horizontal menu items that might be directly in mobile-header-content
15745        const existingHorizontalMenuItems = mobileHeaderContent.querySelectorAll('.horizontal-menu-item');
15746        if (existingHorizontalMenuItems.length > 0) {
15747            console.log('updateMobileHeaderContent: Removing ' + existingHorizontalMenuItems.length + ' existing horizontal menu items');
15748            existingHorizontalMenuItems.forEach(item => item.remove());
15749        }
15750    }
15751    
15752    // For Sidebar, Top, OR Horizontal layout on mobile:
15753    // Populate .mobile-header-content with the first sidebar element (logo/title).
15754    // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout.
15755
15756    console.log("updateMobileHeaderContent: Populating with first sidebar element.");
15757    
15758    // For horizontal layout on mobile, use the header-specific function to get logo/title
15759    // For other layouts, use the general function
15760    let firstElement;
15761    if (currentLayout === 'horizontal' && isMobileView) {
15762        firstElement = getFirstSidebarElementForHeader(); // Gets logo/title specifically
15763        console.log("updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout");
15764    } else {
15765        firstElement = getFirstSidebarElement(); // Gets any first element
15766        console.log("updateMobileHeaderContent: Using getFirstSidebarElement for other layouts");
15767    }
15768
15769    if (firstElement) {
15770        console.log('updateMobileHeaderContent: First element found:', JSON.stringify(firstElement));
15771        
15772        if (firstElement.type === 'image' && (firstElement.imageData || firstElement.imageUrl)) {
15773            console.log('updateMobileHeaderContent: Creating mobile header image');
15774            const img = document.createElement('img');
15775            img.src = firstElement.imageUrl || firstElement.imageData;
15776            img.alt = resolveBrandLogoAlt(firstElement);
15777            img.className = 'mobile-header-image sidebar-image'; // For specific styling if needed
15778
15779            let mobileDisplayNode = img;
15780            if (firstElement.colorizeWithSiteColors &&
15781                window.SidebarManager &&
15782                typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
15783              const buildData = Object.assign({}, firstElement);
15784              if (!buildData.imageWidth) {
15785                buildData.imageWidth = 'auto';
15786              }
15787              mobileDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
15788            }
15789
15790            // Opt-in per-image padding wrapper (no-op when unset)
15791            if (window.SidebarManager &&
15792                typeof window.SidebarManager._wrapWithImagePadding === 'function') {
15793              mobileDisplayNode = window.SidebarManager._wrapWithImagePadding(mobileDisplayNode, firstElement);
15794            }
15795            
15796            // Handle linking: always append the img directly so existing CSS centering is
15797            // not disrupted by an <a> wrapper (which the many !important positioning rules
15798            // were not written to handle). Navigation is wired via a click handler.
15799            if (firstElement.linkType && firstElement.linkType !== 'none') {
15800                let finalLinkHref = '#';
15801                let isValidLink = false;
15802                const linkTarget = firstElement.linkTarget || '_self';
15803                
15804                if (firstElement.linkType === 'external' && firstElement.linkUrl) {
15805                    finalLinkHref = firstElement.linkUrl;
15806                    isValidLink = true;
15807                } else if (firstElement.linkType === 'internal' && firstElement.linkPageId) {
15808                    const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15809                    const targetPage = allGalleries.find(g => g.id === parseInt(firstElement.linkPageId));
15810                    
15811                    if (targetPage) {
15812                        const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
15813                        if (isPreviewMode) {
15814                            const pathParts = window.location.pathname.split('/').filter(Boolean);
15815                            const siteGuid = pathParts[0];
15816                            if (siteGuid && targetPage.slug) {
15817                                finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
15818                                isValidLink = true;
15819                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15820                                finalLinkHref = targetPage.url;
15821                                isValidLink = true;
15822                            }
15823                        } else {
15824                            if (targetPage.slug) {
15825                                finalLinkHref = '/' + targetPage.slug;
15826                                isValidLink = true;
15827                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15828                                finalLinkHref = targetPage.url;
15829                                isValidLink = true;
15830                            }
15831                        }
15832                    }
15833                }
15834                
15835                if (isValidLink) {
15836                    mobileDisplayNode.style.cursor = 'pointer';
15837                    const capturedHref = finalLinkHref;
15838                    const capturedTarget = linkTarget;
15839                    const capturedLinkType = firstElement.linkType;
15840                    const capturedPageId = firstElement.linkPageId;
15841                    
15842                    mobileDisplayNode.addEventListener('click', (e) => {
15843                        e.preventDefault();
15844                        if (capturedLinkType === 'internal' && capturedPageId) {
15845                            const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15846                            const targetPage = allGalleries.find(g => g.id === parseInt(capturedPageId));
15847                            if (targetPage) {
15848                                if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
15849                                    window.loadPage(targetPage.id);
15850                                } else if (typeof window.loadGallery === 'function') {
15851                                    window.loadGallery(targetPage.id);
15852                                } else {
15853                                    window.location.href = capturedHref;
15854                                }
15855                            } else {
15856                                window.location.href = capturedHref;
15857                            }
15858                        } else {
15859                            if (capturedTarget === '_blank') {
15860                                window.open(capturedHref, '_blank');
15861                            } else {
15862                                window.location.href = capturedHref;
15863                            }
15864                        }
15865                    });
15866                }
15867            }
15868            mobileHeaderContent.appendChild(mobileDisplayNode);
15869        } else if (firstElement.type === 'text' && firstElement.textContent) {
15870            console.log('updateMobileHeaderContent: Creating mobile header text');
15871            const textDiv = document.createElement('div');
15872            textDiv.className = 'mobile-header-text'; // For specific styling
15873            textDiv.innerHTML = firstElement.textContent; // Use innerHTML if textContent can contain HTML
15874            const titleEl = textDiv.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
15875            if (titleEl && !titleEl.classList.contains('site-title-text')) {
15876              titleEl.classList.add('site-title-text');
15877            }
15878            mobileHeaderContent.appendChild(textDiv);
15879        } else if (firstElement.type === 'menu' && galleries && galleries.length > 0) {
15880            console.log('updateMobileHeaderContent: Creating mobile header menu title');
15881            // Fallback for 'menu' type, find the first visible gallery title
15882            const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer);
15883            const titleText = firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || 'Menu');
15884            const titleDiv = document.createElement('div');
15885            titleDiv.className = 'mobile-header-menu-title'; // For specific styling
15886            titleDiv.textContent = titleText;
15887            mobileHeaderContent.appendChild(titleDiv);
15888        } else { // Default fallback
15889            console.log('updateMobileHeaderContent: Creating mobile header title (fallback)');
15890            const titleDiv = document.createElement('div');
15891            titleDiv.className = 'mobile-header-title'; // For specific styling
15892            titleDiv.textContent = firstElement.title || (galleries && galleries.length > 0 && galleries[0].title) || 'Menu';
15893            mobileHeaderContent.appendChild(titleDiv);
15894        }
15895    } else {
15896        // Fallback if no sidebar elements exist
15897         const defaultTitle = document.createElement('div');
15898         defaultTitle.className = 'mobile-header-title';
15899         defaultTitle.textContent = 'Menu';
15900         mobileHeaderContent.appendChild(defaultTitle);
15901        console.log("updateMobileHeaderContent: No first sidebar element found, showing default title.");
15902    }
15903
15904    if (typeof syncContentMarginToHorizontalHeader === 'function') {
15905      syncContentMarginToHorizontalHeader();
15906    }
15907}
15908
15909// Helper function to get the first element in the sidebar
15910function getFirstSidebarElement() {
15911// Check if window.SidebarManager exists and has elements
15912if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
15913// Filter to only visible elements
15914const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false && el.type !== 'spacer');
15915
15916if (visibleElements.length > 0) {
15917  // Sort by position and return the first one
15918  return visibleElements.sort((a, b) => a.position - b.position)[0];
15919}
15920}
15921
15922// If no sidebar elements, return first gallery as fallback
15923if (galleries.length > 0) {
15924const firstVisibleGallery = galleries.find(g => g.visible !== false);
15925if (firstVisibleGallery) {
15926  return {
15927    type: 'menu',
15928    title: firstVisibleGallery.title
15929  };
15930}
15931}
15932
15933// Default fallback
15934return {
15935type: 'text',
15936title: 'Menu'
15937};
15938}
15939// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE
15940// Just replace the updateMobileTitle function and add our initialization code
15941
15942// Replace the existing updateMobileTitle function with this one
15943function updateMobileTitle() {
15944// Always call our implementation that shows the top sidebar element
15945updateMobileHeaderContent();
15946}
15947
15948// Add window resize listener to handle mobile header content updates
15949window.addEventListener('resize', () => {
15950  // Debounce the resize event to avoid excessive calls
15951  clearTimeout(window.resizeTimeout);
15952  window.resizeTimeout = setTimeout(() => {
15953    if (typeof updateMobileHeaderContent === 'function') {
15954      updateMobileHeaderContent();
15955    }
15956    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
15957      applyHorizontalLogoMenuLayoutStyles();
15958    }
15959    if (typeof syncContentMarginToHorizontalHeader === 'function') {
15960      syncContentMarginToHorizontalHeader();
15961    }
15962  }, 250);
15963});
15964
15965//separate initialization function that doesn't replace the original initMobileMenu
15966function initTopElementDisplay() {
15967console.log("Initializing top element display");
15968
15969// Run immediately
15970updateMobileHeaderContent();
15971
15972if (typeof syncContentMarginToHorizontalHeader === 'function') {
15973  setTimeout(syncContentMarginToHorizontalHeader, 150);
15974}
15975
15976// Also hook into loadGallery to ensure it runs whenever galleries change
15977const originalLoadGallery = window.loadGallery;
15978if (typeof originalLoadGallery === 'function') {
15979window.loadGallery = function(id, event, options) {
15980  // Forward all args (including post-save { forceReload: true }) to the underlying loader
15981  const result = originalLoadGallery(id, event, options);
15982  updateMobileHeaderContent();
15983  return result;
15984};
15985}
15986}
15987
15988
15989// Initialize our functionality after a short delay to ensure SidebarManager is ready
15990document.addEventListener('DOMContentLoaded', function() {
15991// Short delay to ensure SidebarManager is initialized
15992setTimeout(initTopElementDisplay, 100);
15993
15994// Also run a bit later to catch any late-loading elements
15995setTimeout(updateMobileHeaderContent, 500);
15996});
15997
15998// Run immediately if document is already loaded
15999if (document.readyState === 'complete' || document.readyState === 'interactive') {
16000setTimeout(initTopElementDisplay, 100);
16001}
16002
16003// Add a final fallback to ensure gallery visibility on window load
16004window.addEventListener('load', function() {
16005  setTimeout(ensureGalleryVisibility, 2000);
16006});
16007
16008
16009(function() {
16010  /**
16011   * EnhancedGallerySystem - Comprehensive gallery script and state management
16012   */
16013  class EnhancedGallerySystem {
16014    constructor() {
16015      // Core state tracking
16016      this.initialized = false;
16017      this.initPromise = null;
16018      this.isLoading = false;
16019      this.currentGalleryId = null;
16020      this.version = '260923-002'; // Increment when gallery scripts change (matches current versioned filenames)
16021
16022      // Track loaded resources
16023      this.loadedScripts = new Set();
16024      this.loadedStyles = new Set();
16025      
16026      // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues)
16027      const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
16028      this.essentialResources = {
16029        scripts: [
16030          { id: 'neon-gallery-main', src: galleryScriptsBase + 'neon-gallery-main-v260923-002.js' },
16031          { id: 'neon-gallery-hp', src: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.js' }
16032        ],
16033        styles: [
16034          { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' }
16035        ]
16036      };
16037      
16038      // Keep track of original initialization functions
16039      this.originalInitFunctions = {};
16040      
16041      // Keep reference to gallery state between changes
16042      this.previousGalleryState = null;
16043      
16044      // Track last initialization time per gallery ID to prevent duplicates
16045      this.lastInitializationTime = new Map();
16046      this._loadInProgress = null;
16047      this._loadingGalleryId = null;
16048    }
16049
16050    /**
16051 * Setup the correct gallery container structure
16052 * @param {HTMLElement} container - The main container element
16053 * @returns {boolean} - Success indicator
16054 */
16055setupCorrectGalleryContainer(container) {
16056  console.log('EnhancedGallerySystem: Setting up correct gallery container structure');
16057  
16058  // Validate container
16059  if (!container) {
16060    console.error('EnhancedGallerySystem: Main gallery container is null');
16061    return false;
16062  }
16063  
16064  try {
16065    // Clear the container
16066    container.innerHTML = '';
16067    
16068    // Create the exact container structure expected by the gallery script
16069    const neonContainer = document.createElement('div');
16070    neonContainer.id = 'neon-gallery-container';
16071    neonContainer.style.width = '100%';
16072    neonContainer.style.height = '100%';
16073    neonContainer.style.position = 'relative';
16074    
16075    // Add the container to the DOM
16076    container.appendChild(neonContainer);
16077    
16078    // Set visibility
16079    container.style.opacity = '1';
16080    container.style.display = 'block';
16081    
16082    console.log('EnhancedGallerySystem: Gallery container structure created');
16083    return true;
16084  } catch (error) {
16085    console.error('EnhancedGallerySystem: Error setting up gallery container:', error);
16086    return false;
16087  }
16088}
16089
16090    /**
16091     * Ensure #neon-gallery-container exists before gallery scripts execute.
16092     * neon-gallery-main bails early without it; hp may leave neonGalleryUtils as {}.
16093     */
16094    ensureGalleryContainerPlaceholder(container) {
16095      if (document.getElementById('neon-gallery-container')) {
16096        return document.getElementById('neon-gallery-container');
16097      }
16098      const host = container || document.querySelector('.gallery-container') || document.getElementById('galleryFrame')?.parentElement;
16099      if (!host) {
16100        return null;
16101      }
16102      const neonContainer = document.createElement('div');
16103      neonContainer.id = 'neon-gallery-container';
16104      neonContainer.className = 'gallery-direct-content';
16105      neonContainer.style.width = '100%';
16106      neonContainer.style.height = '100%';
16107      neonContainer.style.position = 'relative';
16108      host.appendChild(neonContainer);
16109      console.log('EnhancedGallerySystem: Created placeholder neon-gallery-container before script load');
16110      return neonContainer;
16111    }
16112
16113    /**
16114     * Wait for neon-gallery-main to expose loadGallerySettings; reload main if needed.
16115     */
16116    async ensureNeonGalleryUtilsReady(container) {
16117      const hasLoadGallerySettings = () =>
16118        typeof window.neonGalleryUtils?.loadGallerySettings === 'function';
16119
16120      if (hasLoadGallerySettings()) {
16121        return true;
16122      }
16123
16124      const mainScript = this.essentialResources.scripts.find(s => s.id === 'neon-gallery-main');
16125      if (!mainScript) {
16126        return false;
16127      }
16128
16129      console.warn('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings missing — reloading main script');
16130      this.ensureGalleryContainerPlaceholder(container);
16131
16132      document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
16133        if (el.parentNode) el.parentNode.removeChild(el);
16134      });
16135      this.loadedScripts.delete('neon-gallery-main');
16136      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16137
16138      await new Promise((resolve, reject) => {
16139        const scriptElement = document.createElement('script');
16140        scriptElement.id = 'neon-gallery-main';
16141        scriptElement.src = `${mainScript.src}?v=${this.version}&reload=${Date.now()}`;
16142        scriptElement.async = false;
16143        scriptElement.onload = () => {
16144          this.loadedScripts.add('neon-gallery-main');
16145          resolve();
16146        };
16147        scriptElement.onerror = (err) => reject(err);
16148        document.body.appendChild(scriptElement);
16149      });
16150
16151      for (let i = 0; i < 50; i++) {
16152        if (hasLoadGallerySettings()) {
16153          return true;
16154        }
16155        await new Promise(r => setTimeout(r, 100));
16156      }
16157
16158      return hasLoadGallerySettings();
16159    }
16160
16161    /**
16162     * Preload styles only — defer script loading until a gallery load provides a container.
16163     */
16164    preloadStylesOnly() {
16165      return this.loadStyles();
16166    }
16167
16168    /**
16169     * Reset initialization tracking when gallery scripts are removed from the DOM
16170     * (e.g. loadPage cleanup). Without this, a pending initPromise never resolves and
16171     * subsequent loadGallery calls hang with isLoading:true, initialized:false.
16172     */
16173    resetInitializationState() {
16174      console.log('EnhancedGallerySystem: Resetting initialization state');
16175      this.initPromise = null;
16176      this.initialized = false;
16177      this.isLoading = false;
16178      this.loadedScripts.clear();
16179      this.loadedStyles.clear();
16180      // Allow neon-gallery-main bootstrap wipe once again after scripts are re-injected
16181      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16182    }
16183
16184    /**
16185     * Initialize the gallery system
16186     * @returns {Promise} Resolves when all core dependencies are loaded
16187     */
16188    initialize() {
16189      // Return existing promise if already initializing or complete
16190      if (this.initPromise) {
16191        return this.initPromise;
16192      }
16193
16194      console.log('EnhancedGallerySystem: Initializing gallery framework');
16195      this.isLoading = true;
16196
16197      // Create a promise to track loading completion
16198      this.initPromise = (async () => {
16199        try {
16200          // CRITICAL: Initialize neonGalleryState before anything else
16201          // This prevents "Cannot set properties of undefined" errors
16202          if (!window.neonGalleryState) {
16203            window.neonGalleryState = {
16204              initialized: false,
16205              initializing: false,
16206              domInitialized: false,
16207              configApplied: false,
16208              initializationPromise: null
16209            };
16210            console.log('EnhancedGallerySystem: Initialized neonGalleryState');
16211          }
16212
16213          // 1. First ensure all essential styles are loaded
16214          await this.loadStyles();
16215
16216          // 2. Preload all scripts to hint to the browser
16217          this.preloadScripts();
16218
16219          // 3. Ensure gallery container exists before scripts that require it
16220          this.ensureGalleryContainerPlaceholder(
16221            document.querySelector('.gallery-container')
16222          );
16223
16224          // 4. Load all essential scripts
16225          await this.loadScripts();
16226          
16227          // 5. Initialize the gallery events
16228          this.setupEvents();
16229          
16230          // Mark as fully initialized
16231          this.initialized = true;
16232          this.isLoading = false;
16233          
16234          console.log('EnhancedGallerySystem: Successfully initialized');
16235          return true;
16236        } catch (error) {
16237          console.error('EnhancedGallerySystem: Initialization failed:', error);
16238          this.isLoading = false;
16239          throw error;
16240        }
16241      })();
16242
16243      return this.initPromise;
16244    }
16245
16246    /**
16247     * Load all necessary stylesheet resources
16248     */
16249    async loadStyles() {
16250      console.log('EnhancedGallerySystem: Loading essential stylesheets');
16251      
16252      const stylePromises = this.essentialResources.styles.map(style => {
16253        // Skip if already loaded
16254        if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) {
16255          console.log(`Style already loaded: ${style.id}`);
16256          this.loadedStyles.add(style.id);
16257          return Promise.resolve();
16258        }
16259        
16260        return new Promise((resolve, reject) => {
16261          const link = document.createElement('link');
16262          link.id = style.id;
16263          link.rel = 'stylesheet';
16264          // Use proxy URL if flag is set, otherwise use CDN URL
16265          let styleUrl = `${style.href}?v=${this.version}`;
16266          if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes('cdn.neonsky.app')) {
16267            styleUrl = window.convertCdnToProxyUrl(style.href) + '?v=' + this.version;
16268            console.log('🔍 Using proxy URL for style:', style.id, styleUrl);
16269          }
16270          
16271          link.href = styleUrl;
16272          
16273          link.onload = () => {
16274            console.log(`✅ Style loaded: ${style.id}`);
16275            this.loadedStyles.add(style.id);
16276            resolve();
16277          };
16278          
16279          link.onerror = (err) => {
16280            console.error(`❌ Failed to load style: ${style.id}`, err);
16281            if (style.href.includes('cdn.neonsky.app') && !styleUrl.includes('/cdn-proxy/')) {
16282              console.error('🔄 Retrying via proxy:', style.href);
16283              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16284              link.href = proxyUrl + '?v=' + this.version;
16285              link.onerror = function() {
16286                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16287                link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
16288                // Don't reject - let it try storage
16289              };
16290            } else {
16291              reject(new Error(`Failed to load ${style.id}`));
16292            }
16293          };
16294          
16295          document.head.appendChild(link);
16296        });
16297      });
16298      
16299      // Wait for all styles to load
16300      await Promise.all(stylePromises);
16301      console.log('EnhancedGallerySystem: All stylesheets loaded');
16302    }
16303    
16304    /**
16305     * Preload scripts to improve browser loading priority
16306     */
16307    preloadScripts() {
16308      this.essentialResources.scripts.forEach(script => {
16309        // Enhanced check for already preloaded scripts
16310        const scriptName = script.src.split('/').pop();
16311        const alreadyPreloaded = document.querySelector(`link[rel="preload"][href*="${scriptName}"]`) ||
16312                                document.querySelector(`script[src*="${scriptName}"]`);
16313        
16314        if (alreadyPreloaded) {
16315          console.log(`Script already preloaded or loaded: ${script.id}`);
16316          return;
16317        }
16318        
16319        const preload = document.createElement('link');
16320        preload.rel = 'preload';
16321        preload.href = `${script.src}?v=${this.version}`;
16322        preload.as = 'script';
16323        document.head.appendChild(preload);
16324        
16325        console.log(`Script preloaded: ${script.id}`);
16326      });
16327    }
16328    
16329    /**
16330     * Load all necessary scripts
16331     */
16332    async loadScripts() {
16333      console.log('EnhancedGallerySystem: Loading essential scripts');
16334      
16335      // Load sequentially (main before hp) so neon-gallery-main assigns window.neonGalleryUtils
16336      // before hp's initNeonGallery can run; parallel async scripts could execute hp before main finishes.
16337      const loadOne = (script) => {
16338        const alreadyLoaded = this.loadedScripts.has(script.id) || 
16339                             document.getElementById(script.id) ||
16340                             document.querySelector(`script[src*="${script.src.split('/').pop()}"]`);
16341        
16342        if (alreadyLoaded) {
16343          const mainNeedsReload =
16344            script.id === 'neon-gallery-main' &&
16345            typeof window.neonGalleryUtils?.loadGallerySettings !== 'function';
16346          if (mainNeedsReload) {
16347            console.warn(`EnhancedGallerySystem: ${script.id} in DOM but neonGalleryUtils incomplete — reloading`);
16348            document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
16349              if (el.parentNode) el.parentNode.removeChild(el);
16350            });
16351            this.loadedScripts.delete(script.id);
16352            try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16353          } else {
16354          console.log(`Script already loaded: ${script.id}`);
16355          this.loadedScripts.add(script.id);
16356          return Promise.resolve();
16357          }
16358        }
16359        
16360        return new Promise((resolve, reject) => {
16361          const scriptElement = document.createElement('script');
16362          scriptElement.id = script.id;
16363          
16364          let scriptUrl = `${script.src}?v=${this.version}`;
16365          if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes('cdn.neonsky.app')) {
16366            scriptUrl = window.convertCdnToProxyUrl(script.src) + '?v=' + this.version;
16367            console.log('🔍 Using proxy URL for script:', script.id, scriptUrl);
16368          }
16369          
16370          scriptElement.src = scriptUrl;
16371          scriptElement.async = false;
16372          
16373          scriptElement.onload = () => {
16374            console.log(`✅ Script loaded: ${script.id}`);
16375            this.loadedScripts.add(script.id);
16376            resolve();
16377          };
16378          
16379          scriptElement.onerror = (err) => {
16380            console.error(`❌ Failed to load script: ${script.id}`, err);
16381            if (script.src.includes('cdn.neonsky.app') && !scriptUrl.includes('/cdn-proxy/')) {
16382              console.error('🔄 Retrying script via proxy:', script.id);
16383              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16384              scriptElement.src = proxyUrl + '?v=' + this.version;
16385              scriptElement.onerror = function() {
16386                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16387                scriptElement.src = scriptElement.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
16388              };
16389            } else {
16390              reject(new Error(`Failed to load ${script.id}`));
16391            }
16392          };
16393          
16394          document.body.appendChild(scriptElement);
16395        });
16396      };
16397      
16398      for (const script of this.essentialResources.scripts) {
16399        await loadOne(script);
16400      }
16401      console.log('EnhancedGallerySystem: All scripts loaded');
16402      
16403      // Save references to original initialization functions after scripts are loaded
16404      if (window.initNeonGallery && typeof window.initNeonGallery === 'function') {
16405        this.originalInitFunctions.initNeonGallery = window.initNeonGallery;
16406      }
16407      
16408      if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave === 'function') {
16409        this.originalInitFunctions.reinitializeGalleryAfterSave = window.reinitializeGalleryAfterSave;
16410      }
16411    }
16412
16413    /**
16414     * Set up enhanced event listeners
16415     */
16416    setupEvents() {
16417      // Listen for gallery save events to capture latest state
16418      document.addEventListener('gallery-saved', (event) => {
16419        if (event.detail && event.detail.settings) {
16420          this.captureGalleryState(event.detail.settings);
16421        }
16422      });
16423      
16424      // Override existing functions to capture state changes
16425      this.enhanceExistingFunctions();
16426    }
16427    
16428    /**
16429     * Enhance existing gallery functions to work with our system
16430     */
16431    enhanceExistingFunctions() {
16432      // Save original functions
16433      const originalInit = window.initNeonGallery;
16434      const originalReinit = window.reinitializeGalleryAfterSave;
16435      
16436      // Only enhance if they exist
16437      if (typeof originalInit === 'function') {
16438        // Track if initNeonGallery is currently executing to prevent concurrent calls
16439        let isExecuting = false;
16440        let lastExecutionTime = 0;
16441        
16442        window.initNeonGallery = async (...args) => {
16443          // Prevent duplicate initializations within 1 second
16444          const now = Date.now();
16445          
16446          // Check if already executing or called recently
16447          if (isExecuting) {
16448            console.log('EnhancedGallerySystem: initNeonGallery already executing, skipping duplicate call');
16449            return Promise.resolve();
16450          }
16451          
16452          if (lastExecutionTime && (now - lastExecutionTime) < 1000) {
16453            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call (last call ' + (now - lastExecutionTime) + 'ms ago)');
16454            return Promise.resolve();
16455          }
16456          
16457          // Set flag and timestamp immediately to prevent concurrent calls
16458          isExecuting = true;
16459          lastExecutionTime = now;
16460          this.lastInitializationTime.set('initNeonGallery', now);
16461          
16462          try {
16463            // Call original with all arguments
16464            const result = await originalInit.apply(this, args);
16465            
16466            // Capture state after initialization
16467            if (window.neonGalleryConfig) {
16468              this.captureGalleryState(window.neonGalleryConfig);
16469            }
16470            
16471            return result;
16472          } finally {
16473            // Clear execution flag after a short delay to allow for async operations
16474            setTimeout(() => {
16475              isExecuting = false;
16476            }, 100);
16477          }
16478        };
16479      }
16480      
16481      if (typeof originalReinit === 'function') {
16482        window.reinitializeGalleryAfterSave = async (savedSettings) => {
16483          // Call original function
16484          const result = await originalReinit(savedSettings);
16485          
16486          // Capture updated state
16487          this.captureGalleryState(savedSettings);
16488          
16489          return result;
16490        };
16491      }
16492    }
16493    
16494    /**
16495     * Capture current gallery state for potential reuse
16496     */
16497    captureGalleryState(settings) {
16498      if (!settings) return;
16499      
16500      // Store a deep copy of the settings to avoid reference issues
16501      try {
16502        this.previousGalleryState = JSON.parse(JSON.stringify(settings));
16503        console.log('EnhancedGallerySystem: Captured gallery state for possible reuse');
16504      } catch (e) {
16505        console.error('EnhancedGallerySystem: Failed to capture gallery state:', e);
16506      }
16507    }
16508
16509    /**
16510     * Load a specific gallery using the enhanced system
16511     * @param {Object} gallery The gallery configuration
16512     * @param {HTMLElement} container The container to load the gallery into
16513     */
16514    async loadGallery(gallery, container, options) {
16515      if (!gallery) {
16516        console.error('EnhancedGallerySystem: Invalid gallery configuration');
16517        return false;
16518      }
16519
16520      const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
16521
16522      // Coalesce concurrent loads for the same gallery — await instead of wiping mid-flight
16523      if (this._loadInProgress && this._loadingGalleryId === gallery.id && !forceGalleryReload) {
16524        const pageViewStillShowing = window._activeContentType === 'page' ||
16525          !!document.querySelector('.gallery-container .page-container, .page-container');
16526        if (!pageViewStillShowing) {
16527          console.log('EnhancedGallerySystem: Awaiting in-flight load for gallery ' + gallery.id);
16528          return this._loadInProgress;
16529        }
16530        console.log('EnhancedGallerySystem: Abandoning standalone in-flight load for page embed', gallery.id);
16531        this._loadInProgress = null;
16532        this._loadingGalleryId = null;
16533      }
16534
16535      const now = Date.now();
16536      const lastInitTime = this.lastInitializationTime.get(gallery.id);
16537      if (!forceGalleryReload && lastInitTime && (now - lastInitTime) < 1000) {
16538        const hasContent = !!document.querySelector('.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry');
16539        const pageViewStillShowing = window._activeContentType === 'page' ||
16540          !!document.querySelector('.gallery-container .page-container, .page-container');
16541        if (hasContent && !pageViewStillShowing) {
16542          console.log('EnhancedGallerySystem: Gallery ' + gallery.id + ' recently loaded with content, skipping re-init');
16543          if (container) container.style.opacity = '1';
16544          if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
16545            window.syncGalleryOptionsButtonVisibility();
16546          }
16547          return true;
16548        }
16549      }
16550
16551      if (forceGalleryReload) {
16552        console.log('EnhancedGallerySystem: force reload requested for gallery ' + gallery.id);
16553        this.lastInitializationTime.delete(gallery.id);
16554        this._loadInProgress = null;
16555        this._loadingGalleryId = null;
16556      }
16557
16558      const loadTask = this._executeGalleryLoad(gallery, container, now, options);
16559      this._loadInProgress = loadTask;
16560      this._loadingGalleryId = gallery.id;
16561      try {
16562        return await loadTask;
16563      } finally {
16564        if (this._loadInProgress === loadTask) {
16565          this._loadInProgress = null;
16566          this._loadingGalleryId = null;
16567        }
16568      }
16569    }
16570
16571    async _executeGalleryLoad(gallery, container, startedAt, options) {
16572      this.lastInitializationTime.set(gallery.id, startedAt);
16573      const contentLoadGeneration = options && options.contentLoadGeneration != null
16574        ? options.contentLoadGeneration
16575        : window.__hydraContentLoadGeneration;
16576      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16577        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16578        : contentLoadGeneration === window.__hydraContentLoadGeneration &&
16579          (!window._activeContentType || window._activeContentType === 'gallery') &&
16580          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16581
16582      stopAutoAdvanceTimer();
16583
16584      // CRITICAL: Verify container exists
16585      if (!container) {
16586        console.error('EnhancedGallerySystem: Gallery container is null or undefined');
16587        return false;
16588      }
16589
16590  // Create a unique instance ID for this gallery load (needed before script init)
16591  const galleryInstanceId = Date.now();
16592
16593  try {
16594    // Clean up previous gallery state and prepare container BEFORE loading scripts
16595    this.cleanupGalleryState(container);
16596    const containerSetup = this.setupCorrectGalleryContainer(container);
16597    if (!containerSetup) {
16598      throw new Error('Failed to set up gallery container');
16599    }
16600    this.setupGalleryParameters(gallery, galleryInstanceId);
16601  } catch (prepError) {
16602    console.error('EnhancedGallerySystem: Failed to prepare gallery container:', prepError);
16603    return false;
16604  }
16605
16606  // Make sure the system is initialized (scripts load with container already in DOM)
16607  try {
16608    await this.initialize();
16609  } catch (error) {
16610    console.error('EnhancedGallerySystem: Failed to initialize gallery framework:', error);
16611    //this.showErrorMessage(container, 'Failed to initialize gallery system. Please try again later.');
16612    return false;
16613  }
16614
16615  if (!stillCurrent()) {
16616    console.log('EnhancedGallerySystem: Aborting stale load after framework initialize for', gallery.id);
16617    return false;
16618  }
16619
16620  if (!(await this.ensureNeonGalleryUtilsReady(container))) {
16621    console.error('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings unavailable after script load');
16622    return false;
16623  }
16624
16625  if (!stillCurrent()) {
16626    console.log('EnhancedGallerySystem: Aborting stale load after utils ready for', gallery.id);
16627    return false;
16628  }
16629
16630  // Update current gallery tracking
16631  this.currentGalleryId = gallery.id;
16632  
16633  console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`);
16634
16635  try {
16636    // Determine which initialization method to use
16637    if (this.canUseReinitForGallery(gallery, options)) {
16638      const result = await this.reinitializeGallery(gallery, container, options);
16639      return stillCurrent() ? result : false;
16640    } else {
16641      const result = await this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16642      return stillCurrent() ? result : false;
16643    }
16644  } catch (error) {
16645    console.error('EnhancedGallerySystem: Error loading gallery:', error);
16646    if (container) {
16647      console.error('EnhancedGallerySystem: Error loading gallery:', error);
16648      //this.showErrorMessage(container, 'There was a problem loading the gallery. Please try again later.');
16649    }
16650    return false;
16651  }
16652}
16653    
16654    normalizeGalleryIdentityValue(value) {
16655      if (value === undefined || value === null || value === '') return '';
16656      return String(value);
16657    }
16658
16659    previousGalleryStateBelongsTo(gallery, previousState) {
16660      if (!gallery || !previousState) return false;
16661
16662      const targetGalleryId = this.normalizeGalleryIdentityValue(gallery.id);
16663      const targetPageId = this.normalizeGalleryIdentityValue(gallery.pageId);
16664      if (!targetGalleryId && !targetPageId) return false;
16665
16666      const previousGalleryId = this.normalizeGalleryIdentityValue(
16667        previousState.galleryId || previousState.loadedGalleryId
16668      );
16669      const previousPageId = this.normalizeGalleryIdentityValue(
16670        previousState.pageId || previousState.initialPageUuid
16671      );
16672
16673      if (!previousGalleryId && !previousPageId) return false;
16674      if (previousGalleryId && targetGalleryId && previousGalleryId !== targetGalleryId) {
16675        return false;
16676      }
16677      if (previousPageId && targetPageId && previousPageId !== targetPageId) {
16678        return false;
16679      }
16680
16681      return (
16682        (previousGalleryId && targetGalleryId && previousGalleryId === targetGalleryId) ||
16683        (previousPageId && targetPageId && previousPageId === targetPageId)
16684      );
16685    }
16686
16687    /**
16688     * Determine if we can use reinitialization for this gallery
16689     */
16690    canUseReinitForGallery(gallery, options) {
16691      if (options?.forceReload === true || window._forceGalleryReload === true) {
16692        return false;
16693      }
16694      // Check if reinitializeGalleryAfterSave function exists
16695      if (typeof window.reinitializeGalleryAfterSave !== 'function') {
16696        return false;
16697      }
16698      
16699      // Check if we have a previous state to reuse
16700      if (!this.previousGalleryState) {
16701        return false;
16702      }
16703
16704      if (!this.previousGalleryStateBelongsTo(gallery, this.previousGalleryState)) {
16705        console.log('EnhancedGallerySystem: Cannot use reinit - previous gallery state does not match target gallery');
16706        return false;
16707      }
16708      
16709      // Check if the gallery has the necessary properties for reinit
16710      if (!gallery.pageId || !gallery.galleryOptions) {
16711        return false;
16712      }
16713      
16714      // CRITICAL: Check if the gallery wrapper actually exists in the DOM
16715      // Reinitialization requires an existing gallery structure
16716      const galleryWrapper = document.querySelector('.gallery-wrapper');
16717      const neonContainer = document.getElementById('neon-gallery-container');
16718      if (!galleryWrapper || !neonContainer) {
16719        console.log('EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found');
16720        return false;
16721      }
16722      
16723      return true;
16724    }
16725    
16726    /**
16727     * Reinitialize a gallery using the reinitialization process
16728     */
16729async reinitializeGallery(gallery, container, options) {
16730  console.log('EnhancedGallerySystem: Using reinitialization process for gallery', gallery.id);
16731  
16732  try {
16733    // CRITICAL: Ensure CSS is loaded before attempting reinitialization
16734    ensureGalleryStylesLoaded();
16735    
16736    // We should already have the proper container, but just in case:
16737    if (!document.getElementById('neon-gallery-container')) {
16738      this.setupCorrectGalleryContainer(container);
16739    }
16740    
16741    // CRITICAL: Ensure config is set up with all required fields before reinitialization
16742    const galleryOptions = gallery.galleryOptions || {};
16743    window.neonGalleryConfig = {
16744      useData: true,
16745      useCDN: true,
16746      version: 'live',
16747      manualCollectionName: galleryOptions.manualCollectionName || "mod",
16748      layoutType: galleryOptions.layoutType || "grid",
16749      ...galleryOptions,
16750      siteId: window.siteId || gallery.siteId || '',
16751      galleryId: gallery.id,
16752      // Explicitly include all permanent storage fields
16753      isPerma: galleryOptions.isPerma,
16754      permaURL: galleryOptions.permaURL,
16755      loadTxId: galleryOptions.loadTxId,
16756      loadPermaURL: galleryOptions.loadPermaURL,
16757      containerSelector: '#neon-gallery-container'
16758    };
16759    
16760    // Prepare settings by merging previous state with new gallery options.
16761    // Identity fields always come from the menu target, never leftover snapshot ids.
16762    const settings = {
16763      ...this.previousGalleryState,
16764      ...gallery.galleryOptions,
16765      pageId: gallery.pageId,
16766      initialPageUuid: gallery.pageId,
16767      galleryId: gallery.id,
16768      loadedGalleryId: gallery.id,
16769      forceReload: true,
16770      containerSelector: '#neon-gallery-container' // Add this explicitly
16771    };
16772    
16773    // Call reinitializeGalleryAfterSave with the merged settings
16774    window._reinitializeGalleryFromSystem = true;
16775    let result;
16776    try {
16777      result = await window.reinitializeGalleryAfterSave(settings);
16778    } finally {
16779      window._reinitializeGalleryFromSystem = false;
16780    }
16781    
16782    if (result) {
16783      console.log('EnhancedGallerySystem: Gallery reinitialization completed successfully');
16784      // Fade in the container
16785      container.style.opacity = '1';
16786      return true;
16787    } else {
16788      throw new Error('Reinitialization did not complete successfully');
16789    }
16790  } catch (error) {
16791    console.error('EnhancedGallerySystem: Error reinitializing gallery:', error);
16792    // Fall back to standard initialization - ensure config is set up properly
16793    const galleryInstanceId = Date.now();
16794    // Re-setup parameters and config for the fallback
16795    this.setupGalleryParameters(gallery, galleryInstanceId);
16796    return this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16797  }
16798}
16799    
16800    /**
16801     * Initialize a gallery from scratch (fallback method)
16802     */
16803    async initializeNewGallery(gallery, container, galleryInstanceId, options) {
16804      console.log('EnhancedGallerySystem: Using standard initialization for gallery', gallery.id);
16805      const contentLoadGeneration = options && options.contentLoadGeneration != null
16806        ? options.contentLoadGeneration
16807        : window.__hydraContentLoadGeneration;
16808      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16809        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16810        : (!window._activeContentType || window._activeContentType === 'gallery') &&
16811          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16812      
16813      try {
16814        // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements
16815        ensureGalleryStylesLoaded();
16816        
16817        // Clean up container
16818        if (container) {
16819          container.innerHTML = '';
16820        } else {
16821          console.error('EnhancedGallerySystem: Container is null in initializeNewGallery');
16822          return false;
16823        }
16824        
16825        // Create gallery container with unique ID
16826        const galleryContainerId = `gallery-container-${gallery.id}-${galleryInstanceId}`;
16827        const neonGalleryContainer = document.createElement('div');
16828        neonGalleryContainer.id = 'neon-gallery-container';
16829        neonGalleryContainer.className = 'gallery-direct-content';
16830        neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
16831        neonGalleryContainer.setAttribute('data-instance-id', galleryInstanceId.toString());
16832        neonGalleryContainer.setAttribute('data-unique-id', galleryContainerId);
16833        neonGalleryContainer.style.width = '100%';
16834        neonGalleryContainer.style.height = '100%';
16835        container.appendChild(neonGalleryContainer);
16836        
16837        // CRITICAL: Initialize neonGalleryState if it doesn't exist
16838        if (!window.neonGalleryState) {
16839          window.neonGalleryState = {
16840            initialized: false,
16841            initializing: false,
16842            domInitialized: false,
16843            configApplied: false,
16844            initializationPromise: null
16845          };
16846        }
16847
16848        // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields
16849        // Don't rely on conditional logic - always set it correctly here
16850        const galleryOptions = gallery.galleryOptions || {};
16851        window.neonGalleryConfig = {
16852          useData: true,
16853          useCDN: true,
16854          version: 'live',
16855          manualCollectionName: galleryOptions.manualCollectionName || "mod",
16856          layoutType: galleryOptions.layoutType || "grid",
16857          ...galleryOptions,
16858          siteId: window.siteId || gallery.siteId || '',
16859          galleryId: gallery.id,
16860          galleryInstanceId: galleryInstanceId,
16861          containerSelector: '#neon-gallery-container',
16862          // Explicitly include all permanent storage fields (critical for proper initialization)
16863          isPerma: galleryOptions.isPerma,
16864          permaURL: galleryOptions.permaURL,
16865          loadTxId: galleryOptions.loadTxId,
16866          loadPermaURL: galleryOptions.loadPermaURL
16867        };
16868        
16869        // Call the initialization function directly if available
16870        if (typeof window.initNeonGallery === 'function') {
16871          console.log('EnhancedGallerySystem: Calling direct initialization method');
16872          
16873          // Additional check: prevent duplicate initNeonGallery calls within 1 second
16874          const initKey = 'init_' + gallery.id + '_' + galleryInstanceId;
16875          const lastInitCall = this.lastInitializationTime.get(initKey);
16876          const now = Date.now();
16877          if (lastInitCall && (now - lastInitCall) < 1000) {
16878            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call for gallery ' + gallery.id + ' 
16878(last call ' + (now - lastInitCall) + 'ms ago)');
16879            container.style.opacity = '1';
16880            return true;
16881          }
16882          this.lastInitializationTime.set(initKey, now);
16883          
16884          try {
16885            // Wait a tiny bit to ensure DOM updates have propagated
16886            await new Promise(resolve => setTimeout(resolve, 10));
16887            if (!stillCurrent()) {
16888              console.log('EnhancedGallerySystem: Aborting stale initializeNewGallery before initNeonGallery');
16889              return false;
16890            }
16891            
16892            const result = await window.initNeonGallery();
16893            if (!stillCurrent()) {
16894              console.log('EnhancedGallerySystem: Ignoring stale initializeNewGallery completion');
16895              return false;
16896            }
16897            console.log('EnhancedGallerySystem: Direct initialization successful');
16898            
16899            // Save state for future loads
16900            if (window.neonGalleryConfig) {
16901              this.captureGalleryState(window.neonGalleryConfig);
16902            }
16903            
16904            // Fade in the container
16905            container.style.opacity = '1';
16906            return true;
16907          } catch (e) {
16908            if (!stillCurrent()) {
16909              console.log('EnhancedGallerySystem: Ignoring stale direct initialization error');
16910              return false;
16911            }
16912            console.error('EnhancedGallerySystem: Direct initialization failed:', e);
16913            throw e;
16914          }
16915        } else {
16916          // If initNeonGallery isn't available, use event-based initialization
16917          console.log('EnhancedGallerySystem: Using event-based initialization fallback');
16918          
16919          // Create an event to trigger initialization
16920          const initEvent = new CustomEvent('neon-gallery-init', { 
16921            detail: { 
16922              galleryId: gallery.id,
16923              instanceId: galleryInstanceId,
16924              containerId: galleryContainerId
16925            }
16926          });
16927          document.dispatchEvent(initEvent);
16928          
16929          // Wait a short time to see if initialization completes
16930          await new Promise(resolve => setTimeout(resolve, 500));
16931          if (!stillCurrent()) {
16932            console.log('EnhancedGallerySystem: Ignoring stale event-based gallery initialization');
16933            return false;
16934          }
16935          
16936          // Fade in regardless of result - gallery script should handle displaying errors
16937          container.style.opacity = '1';
16938          return true;
16939        }
16940      } catch (error) {
16941        console.error('EnhancedGallerySystem: Error in standard initialization:', error);
16942        if (!stillCurrent()) {
16943          console.log('EnhancedGallerySystem: Ignoring stale standard initialization error');
16944          return false;
16945        }
16946        if (container) {
16947          this.showErrorMessage(container, 'Failed to load gallery content. Please try again later.');
16948        }
16949        return false;
16950      }
16951    }
16952
16953    /**
16954     * Clean up gallery state without removing loaded scripts
16955     */
16956    cleanupGalleryState(container) {
16957      console.log('EnhancedGallerySystem: Cleaning up gallery state');
16958      
16959      // Stop any ongoing singles autoplay from the previous gallery
16960      if (typeof window.stopSinglesAutoplay === 'function') {
16961        try {
16962          window.stopSinglesAutoplay(); 
16963          console.log('EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.');
16964        } catch (error) {
16965          console.warn('EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:', error);
16966        }
16967      } else {
16968        console.warn('EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.');
16969      }
16970
16971      // 1. Reset global gallery state variables
16972      const resetGlobals = [
16973        'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
16974        'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryCache', 'neonGalleryImages', 
16975        'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 
16976        'neonGalleryInstance', 'currentGalleryId', 'galleryData', 
16977        'imageCache', 'thumbnailCache'
16978      ];
16979      
16980      resetGlobals.forEach(prop => {
16981        if (window[prop] !== undefined) {
16982          window[prop] = null;
16983        }
16984      });
16985      
16986      // 2. Reset neonGalleryState if it's a proper object with properties
16987      if (window.neonGalleryState && typeof window.neonGalleryState === 'object') {
16988        window.neonGalleryState = {
16989          initialized: false,
16990          initializing: false,
16991          domInitialized: false,
16992          configApplied: false,
16993          initializationPromise: null
16994        };
16995      }
16996      
16997      // 3. Remove lightbox elements and overlays
16998      document.querySelectorAll('#neon-lightbox, .neon-lightbox, .lightbox-container').forEach(el => {
16999        if (el && el.parentNode) {
17000          el.parentNode.removeChild(el);
17001        }
17002      });
17003
17004      const staleOptionsOverlay = document.getElementById('optionsOverlay');
17005      if (staleOptionsOverlay && staleOptionsOverlay.parentNode) {
17006        staleOptionsOverlay.parentNode.removeChild(staleOptionsOverlay);
17007      }
17008      window.optionsVisible = false;
17009      
17010      // 4. Remove any gallery-specific DOM elements
17011      document.querySelectorAll(
17012        '.neon-gallery-wrapper, .fullscreen-overlay, ' + 
17013        '.neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, ' +
17014        '.neon-thumbnails, .neon-image, .neon-caption, ' +
17015        '.neon-controls, .neon-pagination'
17016      ).forEach(el => {
17017        if (el && el.parentNode) {
17018          el.parentNode.removeChild(el);
17019        }
17020      });
17021      
17022      // 5. Clear container if provided and valid
17023      if (container) {
17024        container.innerHTML = '';
17025        container.style.opacity = '0';
17026      }
17027      
17028      // 6. Clear keyboard listeners to prevent multiple listeners from different galleries
17029      if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
17030        window.removeAllLightboxKeyboardListeners();
17031        console.log('EnhancedGallerySystem: Cleared all lightbox keyboard listeners during cleanup');
17032      } else if (window.currentLightboxKeyboardListener) {
17033        document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
17034        window.currentLightboxKeyboardListener = null;
17035        console.log('EnhancedGallerySystem: Cleared keyboard listeners during cleanup');
17036      }
17037      window.lightboxKeyboardListenerActive = false;
17038    }
17039    
17040    /**
17041     * Prepare container for gallery loading
17042     */
17043    prepareContainer(container, gallery, instanceId) {
17044      // CRITICAL: Add null check for container
17045      if (!container) {
17046        console.error('EnhancedGallerySystem: Container is null in prepareContainer');
17047        return;
17048      }
17049      
17050      try {
17051        // Add attributes and data
17052        container.setAttribute('data-gallery-id', gallery.id.toString());
17053        container.setAttribute('data-instance-id', instanceId.toString());
17054        
17055        // Add loading indicator
17056        const loadingIndicator = document.createElement('div');
17057        loadingIndicator.className = 'gallery-loading';
17058        loadingIndicator.innerHTML = `
17059          <div class="spinner"></div>
17060          <div class="loading-text">Loading Gallery...</div>
17061        `;
17062        
17063        container.appendChild(loadingIndicator);
17064      } catch (error) {
17065        console.error('EnhancedGallerySystem: Error preparing container:', error);
17066      }
17067    }
17068    
17069
17070
17071
17072setupGalleryParameters(gallery, galleryInstanceId) {
17073  // Set up gallery parameters with enhanced uniqueness
17074  window.Parameters = window.Parameters || {};
17075  
17076  // Clone and extend existing Parameters to avoid losing important values
17077  const existingParams = { ...window.Parameters };
17078  
17079  window.Parameters = {
17080    ...existingParams,
17081    SiteAlias: window.location.hostname,
17082    InitialPageUuid: gallery.pageId,
17083    InitialPageAlias: gallery.slug || this.slugify(gallery.title),
17084    // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode
17085    // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active
17086    isInEditor: window.isEditing || 
17087                localStorage.getItem('hydra_is_admin') === 'true' || 
17088                window.isAdmin === true || 
17089                document.body.classList.contains('hydra-admin'),
17090    siteId: window.siteId || gallery.siteId || '',
17091    isHydra: true,
17092    
17093    // CRITICAL: These properties help the gallery script find the container
17094    galleryContainerId: 'neon-gallery-container',
17095    containerSelector: '#neon-gallery-container',
17096    
17097    // Additional properties
17098    galleryInstanceId: galleryInstanceId,
17099    galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`,
17100    loadTimestamp: galleryInstanceId,
17101    loadedGalleryId: gallery.id
17102  };
17103
17104  // Set up gallery config - CRITICAL: Include all permanent storage fields
17105  const galleryOptions = gallery.galleryOptions || {};
17106  window.neonGalleryConfig = {
17107    useData: true,
17108    useCDN: true,
17109    version: 'live',
17110    manualCollectionName: galleryOptions.manualCollectionName || "mod",
17111    layoutType: galleryOptions.layoutType || "grid",
17112    ...galleryOptions,
17113    siteId: window.siteId || gallery.siteId || '',
17114    galleryId: gallery.id,
17115    galleryInstanceId: galleryInstanceId,
17116    
17117    // Add container selector here too
17118    containerSelector: '#neon-gallery-container',
17119    
17120    // CRITICAL: Explicitly include all permanent storage fields to ensure they're available
17121    // These are needed for proper gallery initialization and data loading
17122    isPerma: galleryOptions.isPerma,
17123    permaURL: galleryOptions.permaURL,
17124    loadTxId: galleryOptions.loadTxId,
17125    loadPermaURL: galleryOptions.loadPermaURL
17126  };
17127}
17128    
17129    /**
17130     * Show error message in container
17131     */
17132    showErrorMessage(container, message) {
17133      // CRITICAL: Add null check for container
17134      if (!container) {
17135        console.error('EnhancedGallerySystem: Container is null in showErrorMessage');
17136        return;
17137      }
17138      
17139      try {
17140        // Clear container
17141        container.innerHTML = '';
17142        
17143        // Create error message element
17144        const errorElement = document.createElement('div');
17145        errorElement.className = 'gallery-error';
17146        errorElement.innerHTML = `
17147          <h3>Error Loading Gallery</h3>
17148          <p>${message}</p>
17149        `;
17150        container.appendChild(errorElement);
17151        container.style.opacity = '1';
17152      } catch (error) {
17153        console.error('EnhancedGallerySystem: Error showing error message:', error);
17154      }
17155    }
17156    
17157    /**
17158     * Utility function to convert title to slug
17159     */
17160    slugify(text) {
17161      return text
17162        .toLowerCase()
17163        .replace(/s+/g, '-')
17164        .replace(/[^w-]+/g, '')
17165        .replace(/--+/g, '-')
17166        .trim();
17167    }
17168  }
17169
17170  // Create a singleton instance
17171  window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem();
17172
17173  /**
17174   * Enhanced loadGallery function using the EnhancedGallerySystem
17175   * This replaces the existing loadGallery function
17176   */
17177const enhancedLoadGallery = async function(id, event, options) {
17178    const originalLoadGallery = window.originalLoadGallery;
17179    const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
17180
17181    stopAutoAdvanceTimer();
17182    if (typeof dismissGalleryPasswordOverlay === 'function') {
17183      dismissGalleryPasswordOverlay();
17184    }
17185
17186    if (event && event.target && event.target.isConnected === false) {
17187      console.log('enhancedLoadGallery: Ignoring navigation from detached element');
17188      return;
17189    }
17190
17191    if (event) {
17192      event.preventDefault();
17193      event.stopPropagation();
17194    }
17195
17196    let galleryContainer = document.querySelector('.gallery-container');
17197
17198    if (!galleryContainer) {
17199      galleryContainer = document.getElementById('galleryFrame')?.parentElement ||
17200                        document.getElementById('main-content') ||
17201                        document.getElementById('content');
17202
17203      if (!galleryContainer) {
17204        console.error('Gallery container not found, falling back to original function');
17205        if (typeof originalLoadGallery === 'function') {
17206          return originalLoadGallery(id, event, options);
17207        } else {
17208          console.error('Gallery container not found and no fallback available');
17209          return;
17210        }
17211      }
17212    }
17213
17214    // Bail out before hiding/clearing when the same gallery is already loading or rendered.
17215    // Post-save passes { forceReload: true } to bypass this path without affecting navigation.
17216    const egEarly = window.EnhancedGallerySystem;
17217    if (egEarly && !forceGalleryReload) {
17218      if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) {
17219        const pageViewStillShowing = window._activeContentType === 'page' ||
17220          !!document.querySelector('.gallery-container .page-container, .page-container');
17221        if (pageViewStillShowing) {
17222          console.log('enhancedLoadGallery: abandoning standalone in-flight load for page view', id);
17223          egEarly._loadInProgress = null;
17224          egEarly._loadingGalleryId = null;
17225        } else {
17226          console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id);
17227          await egEarly._loadInProgress;
17228          if (galleryContainer) galleryContainer.style.opacity = '1';
17229          return;
17230        }
17231      }
17232      const hasRenderedGallery = !!document.querySelector(
17233        '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry'
17234      );
17235      // Embedded galleries on a page share currentGalleryId; that is not a standalone gallery view.
17236      const pageViewStillShowing = window._activeContentType === 'page' ||
17237        !!document.querySelector('.gallery-container .page-container, .page-container');
17238      if (hasRenderedGallery && egEarly.currentGalleryId === id && !pageViewStillShowing) {
17239        console.log('enhancedLoadGallery: gallery already rendered, skipping reload');
17240        if (galleryContainer) galleryContainer.style.opacity = '1';
17241        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17242          window.syncGalleryOptionsButtonVisibility();
17243        }
17244        return;
17245      }
17246    } else if (forceGalleryReload) {
17247      console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip');
17248      if (egEarly?.lastInitializationTime?.delete) {
17249        egEarly.lastInitializationTime.delete(id);
17250      }
17251      if (egEarly) {
17252        egEarly._loadInProgress = null;
17253        egEarly._loadingGalleryId = null;
17254      }
17255    }
17256
17257    if (galleryContainer) {
17258      galleryContainer.style.opacity = '0';
17259    }
17260
17261    console.log('Loading gallery with ID (enhancedLoadGallery):', id);
17262
17263    const gallery = findGalleryById(galleries, id);
17264
17265    if (!gallery) {
17266      console.warn('No gallery found with ID:', id);
17267      if (galleryContainer) {
17268        galleryContainer.innerHTML = `
17269          <div class="gallery-error">
17270            <h3>Gallery Not Found</h3>
17271            <p>The requested gallery could not be found.</p>
17272          </div>
17273        `;
17274        galleryContainer.style.opacity = '1';
17275      }
17276      return;
17277    }
17278
17279    if (isPageMenuItem(gallery)) {
17280      return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event);
17281    }
17282
17283    window._activeContentType = 'gallery';
17284
17285    if (gallery.isSubmenu) {
17286      console.log('Gallery is submenu, skipping URL update');
17287      toggleSubmenu(id, event || { stopPropagation: () => {} });
17288      if (galleryContainer){
17289      galleryContainer.style.opacity = '1';
17290      }
17291      return;
17292    }
17293
17294    const contentLoadGeneration = beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
17295    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
17296      window.PageManager.clearCurrentPage();
17297    }
17298
17299    console.log('Found gallery:', gallery.title);
17300
17301    // Check for gallery-level password protection (only for visitors, not in edit mode)
17302    if (!isInEditMode() && gallery.passwordProtected) {
17303      // Use PageManager's gallery password protection methods
17304      if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && 
17305          typeof window.PageManager.createGalleryPasswordOverlay === 'function') {
17306        
17307        if (!window.PageManager.checkGalleryPasswordAccess(gallery)) {
17308          console.log('Gallery is password protected, showing overlay');
17309          window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => {
17310            if (accessGranted) {
17311              // Reload the gallery now that access is granted
17312              enhancedLoadGallery(id, event);
17313            }
17314          });
17315          return; // Exit early, will reload after password is entered
17316        }
17317      }
17318    }
17319
17320    window.activeGalleryId = id;
17321    activeGalleryId = id;
17322
17323    // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery.
17324    // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(),
17325    // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original
17326    // loadGallery) and clear the container so page content does not block gallery rendering.
17327    if (typeof window.removeGalleryScriptsWithPause === 'function') {
17328      await window.removeGalleryScriptsWithPause();
17329    }
17330    if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17331      console.log('enhancedLoadGallery: Aborting stale load after cleanup for', gallery.id);
17332      return;
17333    }
17334    if (galleryContainer) {
17335      galleryContainer.innerHTML = '';
17336    }
17337
17338    const bodyEl = document.body;
17339    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
17340        bodyEl.classList.add('menu-hidden-on-page');
17341    } else {
17342        bodyEl.classList.remove('menu-hidden-on-page');
17343    }
17344    applyMenuOverlayClassForGallery(gallery);
17345
17346    try {
17347      if (typeof updateActiveStates === 'function') updateActiveStates();
17348      if (typeof updateMobileTitle === 'function') updateMobileTitle();
17349      if (typeof closeMobileMenu === 'function') closeMobileMenu();
17350      // Close the gallery options panel when navigating to a different gallery
17351      if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) {
17352        window.closeOptionsPanel();
17353      }
17354      const staleOptionsOverlay = document.getElementById('optionsOverlay');
17355      if (staleOptionsOverlay) {
17356        staleOptionsOverlay.remove();
17357      }
17358      window.optionsVisible = false;
17359      if (typeof updateURLWithGallerySlug === 'function') {
17360        updateURLWithGallerySlug(gallery);
17361      }
17362    } catch (uiError) {
17363      console.error('Error updating UI state:', uiError);
17364    }
17365
17366    try {
17367      // Add detailed logging about EnhancedGallerySystem availability
17368      console.log('🔍 Checking EnhancedGallerySystem availability:', {
17369        exists: !!window.EnhancedGallerySystem,
17370        hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17371        initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized),
17372        isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading)
17373      });
17374      
17375      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') {
17376        console.log('✅ EnhancedGallerySystem available, calling loadGallery');
17377        const result = await window.EnhancedGallerySystem.loadGallery(
17378          gallery,
17379          galleryContainer,
17380          {
17381            ...(options || {}),
17382            ...(forceGalleryReload ? { forceReload: true } : {}),
17383            contentLoadGeneration
17384          }
17385        );
17386        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17387          console.log('enhancedLoadGallery: Ignoring stale gallery completion for', gallery.id);
17388          return;
17389        }
17390        console.log('📊 EnhancedGallerySystem.loadGallery result:', result);
17391
17392        if (!result && typeof originalLoadGallery === 'function') {
17393          console.log('⚠️ Enhanced gallery loading failed, falling back to original function');
17394          return originalLoadGallery(id, event, options);
17395        }
17396        if (galleryContainer) {
17397          galleryContainer.style.opacity = '1';
17398        }
17399        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17400          window.syncGalleryOptionsButtonVisibility();
17401          setTimeout(window.syncGalleryOptionsButtonVisibility, 350);
17402        }
17403      } else {
17404        console.error('❌ EnhancedGallerySystem or its loadGallery method is not available.', {
17405          EnhancedGallerySystem: !!window.EnhancedGallerySystem,
17406          loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17407          useCdnProxy: window.useCdnProxy || false
17408        });
17409        
17410        // Fallback: Try using createGalleryContext directly
17411        console.log('🔄 Attempting fallback: createGalleryContext');
17412        if (typeof createGalleryContext === 'function') {
17413          createGalleryContext(gallery, galleryContainer);
17414          if (galleryContainer) {
17415            galleryContainer.style.opacity = '1';
17416          }
17417          return;
17418        }
17419        
17420        if (galleryContainer) {
17421            galleryContainer.innerHTML = `<div class="gallery-error">Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}</div>`;
17422            galleryContainer.style.opacity = '1';
17423        }
17424        return;
17425      }
17426    } catch (error) {
17427      console.error('Error loading gallery:', error);
17428      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17429        console.log('enhancedLoadGallery: Ignoring stale gallery error for', gallery.id);
17430        return;
17431      }
17432      if (galleryContainer) {
17433        try {
17434          galleryContainer.innerHTML = `
17435            <div class="gallery-error">
17436              <h3>Error Loading Gallery</h3>
17437              <p>There was a problem loading the gallery. Please try again later.</p>
17438              <p class="error-details">${error.message}</p>
17439            </div>
17440          `;
17441          galleryContainer.style.opacity = '1';
17442        } catch (containerError) {
17443          console.error('Error showing error message in container:', containerError);
17444        }
17445      }
17446      if (typeof originalLoadGallery === 'function') {
17447        console.log('Falling back to original loadGallery after error');
17448        return originalLoadGallery(id, event, options);
17449      }
17450    }
17451  };
17452
17453  /**
17454   * Initialization function to set up the gallery system
17455   */
17456  function initializeGallerySystem() {
17457    console.log('Initializing enhanced gallery system');
17458    
17459    // Store reference to original function before replacing
17460    if (typeof window.loadGallery === 'function') {
17461      window.originalLoadGallery = window.loadGallery;
17462    }
17463    
17464    // Replace the existing loadGallery function with our optimized version
17465    window.loadGallery = enhancedLoadGallery;
17466    
17467    // Add this CSS for loading/error states
17468    addGallerySystemStyles();
17469    
17470    // Preload styles only at boot — scripts load when first gallery opens (with container ready)
17471    window.EnhancedGallerySystem.preloadStylesOnly().catch(error => {
17472      console.warn('Gallery styles preloading failed:', error);
17473    });
17474  }
17475
17476  /**
17477   * Add CSS styles for loading indicators and error messages
17478   */
17479  function addGallerySystemStyles() {
17480    const styleId = 'enhanced-gallery-system-styles';
17481    
17482    // Skip if already added
17483    if (document.getElementById(styleId)) {
17484      return;
17485    }
17486    
17487    const style = document.createElement('style');
17488    style.id = styleId;
17489    style.textContent = `
17490      /* Loading indicator */
17491      .gallery-loading {
17492        display: flex;
17493        flex-direction: column;
17494        justify-content: center;
17495        align-items: center;
17496        width: 100%;
17497        height: 100%;
17498        min-height: 200px;
17499        opacity: 0 !important;
17500      }
17501      
17502      .gallery-loading .spinner {
17503        width: 50px;
17504        height: 50px;
17505        border: 3px solid rgba(0, 0, 0, 0.1);
17506        border-radius: 50%;
17507        border-top-color: #4682B4;
17508        animation: spin 1s ease-in-out infinite;
17509        margin-bottom: 20px;
17510        opacity: 0 !important;
17511      }
17512      
17513      @keyframes spin {
17514        to { transform: rotate(360deg); }
17515      }
17516      
17517      .gallery-loading .loading-text {
17518        font-family: Arial, sans-serif;
17519        font-size: 16px;
17520        color: #555;
17521        opacity: 0;
17522      }
17523      
17524      /* Error message */
17525      .gallery-error {
17526        display: flex;
17527        flex-direction: column;
17528        justify-content: center;
17529        align-items: center;
17530        width: 100%;
17531        height: 100%;
17532        min-height: 200px;
17533        padding: 30px;
17534        text-align: center;
17535      }
17536      
17537      .gallery-error h3 {
17538        font-family: Arial, sans-serif;
17539        font-size: 24px;
17540        color:rgba(211, 47, 47, 0.25);
17541        margin-bottom: 20px;
17542      }
17543      
17544      .gallery-error p {
17545        font-family: Arial, sans-serif;
17546        font-size: 16px;
17547        color: #555;
17548        max-width: 600px;
17549        margin-bottom: 10px;
17550      }
17551      
17552      .gallery-error .error-details {
17553        font-size: 14px;
17554        color: #888;
17555        font-style: italic;
17556      }
17557    `;
17558    
17559    document.head.appendChild(style);
17560  }
17561
17562  // Initialize gallery system immediately if possible
17563  initializeGallerySystem();
17564})();
17565
17566
17567/**
17568 * Enhanced Gallery Link Interceptor
17569 * Handles clicks on gallery links to prevent full page reloads
17570 */
17571(function() {
17572  // Store any loaded gallery data for quick reference
17573  const GALLERY_DATA_CACHE = {
17574    initialized: false,
17575    galleries: [],
17576    lastUpdate: 0
17577  };
17578
17579  // Initialize when DOM is ready to catch early clicks
17580  document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor);
17581  
17582  // Also initialize when window loads to ensure complete gallery data
17583  window.addEventListener('load', function() {
17584    // Use a small delay to ensure galleries have fully initialized
17585    setTimeout(initGalleryLinkInterceptor, 200);
17586  });
17587  
17588  // Additional init on first user interaction
17589  document.addEventListener('click', function initOnFirstClick() {
17590    initGalleryLinkInterceptor();
17591    // Remove this listener after first click
17592    document.removeEventListener('click', initOnFirstClick);
17593  }, { once: true });
17594  
17595  // Main initialization function
17596  function initGalleryLinkInterceptor() {
17597    // Prevent multiple initializations
17598    if (window._galleryLinkInterceptorInitialized) {
17599      // If already initialized, just refresh the gallery data
17600      refreshGalleryCache();
17601      return;
17602    }
17603    
17604    window._galleryLinkInterceptorInitialized = true;
17605    console.log('Initializing enhanced gallery link interceptor');
17606    
17607    // Update our cache with current galleries data
17608    refreshGalleryCache();
17609    
17610    // Get primary containers that might contain links
17611    const galleryContainer = document.querySelector('.gallery-container');
17612    const mainContainer = document.querySelector('.container') || document.body;
17613    
17614    if (!galleryContainer && !mainContainer) {
17615      console.warn('No gallery containers found, link interceptor partially initialized');
17616    }
17617    
17618    // Add global click handler to catch all link clicks
17619    document.addEventListener('click', handleGlobalLinkClick, true);
17620    
17621    // Try to intercept links from iframes - critical for gallery content
17622    interceptAllIframeLinks();
17623    
17624    // Set up a mutation observer to handle dynamically added content
17625    setupMutationObserver();
17626    
17627    // Log initialization with site details
17628    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17629    console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`);
17630    
17631    // Create global helper for debugging
17632    window.debugGalleryLinks = function() {
17633      return {
17634        inPreviewMode: isPreviewMode,
17635        interceptorInitialized: window._galleryLinkInterceptorInitialized,
17636        cachedGalleries: GALLERY_DATA_CACHE.galleries.length,
17637        urlPath: window.location.pathname,
17638        siteGuid: getSiteGuidFromPath()
17639      };
17640    };
17641  }
17642  
17643  // Update the gallery cache when gallery data changes
17644function refreshGalleryCache() {
17645  // Initialize GALLERY_DATA_CACHE if it doesn't exist
17646  if (!window.GALLERY_DATA_CACHE) {
17647    window.GALLERY_DATA_CACHE = {
17648      initialized: false,
17649      galleries: [],
17650      lastUpdate: 0
17651    };
17652  }
17653  
17654  // Check for global galleries variable first
17655  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
17656    // Use the global galleries array
17657    window.GALLERY_DATA_CACHE.galleries = galleries;
17658    window.GALLERY_DATA_CACHE.initialized = true;
17659    window.GALLERY_DATA_CACHE.lastUpdate = Date.now();
17660    window.galleries = galleries;
17661
17662    console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`);
17663    return;
17664  }
17665    
17666    // Debug log
17667    console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`);
17668  }
17669  
17670  // Global click handler - catches all link clicks
17671  function handleGlobalLinkClick(e) {
17672    // Find if a link was clicked
17673    const link = e.target.closest('a');
17674    if (!link || !link.href) return;
17675    
17676    // Skip if the link uses special targets or modifiers
17677    if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
17678    
17679    // Skip for hash links or javascript: links
17680    if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
17681    
17682    try {
17683      // Parse the URL to work with it
17684      const url = new URL(link.href);
17685      
17686      // Only intercept links to the same domain
17687      if (url.hostname !== window.location.hostname) return;
17688      
17689      // Skip API and resource URLs
17690      if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return;
17691      
17692      console.log('Link intercepted:', url.pathname);
17693      
17694      // For preview URLs, we need to handle the path specially
17695      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17696      const siteGuid = getSiteGuidFromPath();
17697      
17698      console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid);
17699
17700      // Site Graph card links carry data-sg-target-id — use SPA navigation (not preview iframe fallback)
17701      if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) {
17702        let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries))
17703          ? galleries
17704          : window.galleries;
17705        const sgItem = Array.isArray(galleriesData)
17706          ? findGalleryById(galleriesData, link.dataset.sgTargetId)
17707          : null;
17708        if (sgItem) {
17709          e.preventDefault();
17710          e.stopPropagation();
17711          if (sgItem.isPage && window.loadPage) {
17712            window.loadPage(sgItem.id);
17713          } else if (window.loadGallery) {
17714            window.loadGallery(sgItem.id);
17715          }
17716          let targetUrl = url.href;
17717          if (isPreviewMode && siteGuid) {
17718            const pathname = url.pathname.startsWith(`/${siteGuid}/`) ?
17719              url.pathname :
17720              `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17721            const newUrl = new URL(url);
17722            newUrl.pathname = pathname;
17723            targetUrl = newUrl.href;
17724          }
17725          window.history.pushState(
17726            { galleryId: sgItem.id },
17727            sgItem.title || document.title,
17728            targetUrl
17729          );
17730          document.title = formatGalleryDocumentTitle(sgItem.title);
17731          window.scrollTo(0, 0);
17732          return;
17733        }
17734      }
17735      
17736      // Check if this is internal navigation first
17737      let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17738      
17739      console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none');
17740      
17741      if (matchingItem) {
17742        // Prevent default navigation (full page reload)
17743        e.preventDefault();
17744        e.stopPropagation();
17745        
17746        console.log('Found matching gallery for link:', matchingItem.title);
17747
17748        const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function'
17749          ? window.isNestedGalleryLinkModeEnabled()
17750          : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes);
17751        const isGridGalleryLink = !!(link.closest('.masonry-item') ||
17752          (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') &&
17753           link.querySelector('img') &&
17754           !link.closest('#neon-lightbox, .neon-lightbox')));
17755
17756        if (!matchingItem.isPage &&
17757            nestedLinkModeActive &&
17758            isGridGalleryLink &&
17759            typeof window.openNestedGalleryLightbox === 'function') {
17760          void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => {
17761            if (!opened) {
17762              console.warn('Nested gallery lightbox failed — staying on parent gallery (no loadGallery fallback)');
17763            }
17764          });
17765          return;
17766        }
17767        
17768        // Use the existing navigation functions
17769        if (matchingItem.isPage && window.loadPage) {
17770          window.loadPage(matchingItem.id);
17771        } else if (window.loadGallery) {
17772          window.loadGallery(matchingItem.id);
17773        }
17774        
17775        // Construct proper URL for preview mode
17776        let targetUrl = url.href;
17777        if (isPreviewMode && siteGuid) {
17778          // Ensure we keep the GUID in the URL path
17779          const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17780            url.pathname : 
17781            `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17782          
17783          const newUrl = new URL(url);
17784          newUrl.pathname = pathname;
17785          targetUrl = newUrl.href;
17786        }
17787        
17788        // Update URL without reload
17789        window.history.pushState(
17790          { galleryId: matchingItem.id },
17791          matchingItem.title || document.title,
17792          targetUrl
17793        );
17794        
17795        // Update page title
17796        document.title = formatGalleryDocumentTitle(matchingItem.title);
17797        
17798        // Scroll to top
17799        window.scrollTo(0, 0);
17800        
17801        return;
17802      } 
17803      // Special case for preview mode - even if no matching gallery found
17804      else if (isPreviewMode && siteGuid) {
17805        // We're in preview mode with a GUID - intercept the navigation
17806        // to maintain the GUID in the URL. Only do this when #galleryFrame
17807        // can load content; otherwise allow SPA onclick handlers to run.
17808        
17809        console.log('No direct match but in preview mode - handling navigation');
17810
17811        const frame = document.getElementById('galleryFrame');
17812
17813        if (!frame) {
17814          console.warn('Gallery frame not found - allowing default/SPA handlers');
17815        } else {
17816          // Prevent default navigation
17817          e.preventDefault();
17818          e.stopPropagation();
17819          
17820          // Construct a URL that preserves the GUID
17821          let newPathname = url.pathname;
17822          if (!newPathname.startsWith(`/${siteGuid}/`)) {
17823            // Add the GUID to the path if not already there
17824            newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17825          }
17826          
17827          // Create the preview-friendly URL for browser history
17828          const browserUrl = new URL(window.location.origin);
17829          browserUrl.pathname = newPathname;
17830          
17831          console.log('Constructing preview URL with GUID:', browserUrl.href);
17832          
17833          // IMPORTANT: For gallery navigation, we need to create a special URL
17834          // that tells the gallery system to load the content directly
17835          console.log('Loading preview path in iframe:', newPathname);
17836          
17837          // First update browser URL to show the correct path
17838          window.history.pushState(
17839            { customPath: newPathname },
17840            document.title,
17841            browserUrl.href
17842          );
17843          
17844          // Create direct path to the gallery content
17845          const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
17846          
17847          // For direct gallery paths, we need to load the gallery with special parameters
17848          // This tells the gallery system to load content from this path
17849          const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`;
17850          
17851          // Two approaches to loading the content:
17852          
17853          // 1. Use the gallery wrapper approach
17854          const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
17855            `?siteId=${siteGuid}` +
17856            `&path=${encodeURIComponent(cleanPath)}` + 
17857            `&directPath=true` +
17858            `&t=${Date.now()}`; // Cache buster
17859          
17860          console.log('Loading gallery via wrapper URL:', wrapperUrl);
17861          frame.src = wrapperUrl;
17862          
17863          // Clear any active gallery ID since we're navigating directly
17864          if (window.activeGalleryId) {
17865            console.log('Clearing active gallery ID during direct navigation');
17866            window.activeGalleryId = null;
17867          }
17868          
17869          return; // Critical: prevent further navigation
17870        }
17871      }
17872      
17873      console.log('No matching gallery or special handling - allow normal navigation');
17874    } catch (error) {
17875      console.error('Error processing link click:', error);
17876    }
17877  }
17878  
17879  // Handle all iframes on the page
17880  function interceptAllIframeLinks() {
17881    // Look for any iframe that might contain gallery content
17882    const iframes = document.querySelectorAll('iframe');
17883    
17884    iframes.forEach(iframe => {
17885      if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) {
17886        // Primary gallery iframe - high priority
17887        setupIframeIntercept(iframe, true);
17888      } else {
17889        // Any other iframe - still try to intercept
17890        setupIframeIntercept(iframe, false);
17891      }
17892    });
17893    
17894    console.log(`Attempted to intercept links in ${iframes.length} iframes`);
17895  }
17896  
17897  // Setup link interception for a specific iframe
17898  function setupIframeIntercept(iframe, isPrimary) {
17899    try {
17900      // Skip if iframe is not fully loaded yet
17901      if (!iframe.contentWindow || !iframe.contentDocument) {
17902        // Add load event to try again when loaded
17903        iframe.addEventListener('load', function() {
17904          setupIframeIntercept(iframe, isPrimary);
17905        });
17906        return;
17907      }
17908      
17909      // Try to access the iframe content - might fail due to same-origin policy
17910      try {
17911        const iframeDoc = iframe.contentDocument;
17912        const iframeWin = iframe.contentWindow;
17913        
17914        // Add event listener for all link clicks in the iframe
17915        iframeDoc.addEventListener('click', function(e) {
17916          const link = e.target.closest('a');
17917          if (!link || !link.href) return;
17918          
17919          // Skip special cases
17920          if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
17921          if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
17922          
17923          try {
17924            const url = new URL(link.href);
17925            
17926            // Check if same domain or relative URL
17927            const isSameDomain = url.hostname === window.location.hostname || 
17928                                url.hostname === iframe.src.hostname || 
17929                                !url.hostname;
17930            
17931            if (isSameDomain) {
17932              const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17933              const siteGuid = getSiteGuidFromPath();
17934              
17935              // First try to find a matching gallery/page
17936              const matchingItem = parent.findGalleryByPath ? 
17937                parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : 
17938                findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17939              
17940              if (matchingItem) {
17941                // We found a match - prevent default and navigate
17942                e.preventDefault();
17943                e.stopPropagation();
17944                
17945                console.log('Iframe: Found matching content for link:', matchingItem.title);
17946                
17947                // Access parent window functions
17948                if (matchingItem.isPage && parent.loadPage) {
17949                  parent.loadPage(matchingItem.id);
17950                } else if (parent.loadGallery) {
17951                  parent.loadGallery(matchingItem.id);
17952                }
17953                
17954                // Update URL in parent window
17955                let targetUrl = url.href;
17956                if (isPreviewMode && siteGuid) {
17957                  // Ensure GUID is preserved in URL
17958                  const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17959                    url.pathname : 
17960                    `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17961                  
17962                  const newUrl = new URL(window.location.origin + pathname);
17963                  targetUrl = newUrl.href;
17964                }
17965                
17966                // Update browser URL
17967                parent.history.pushState(
17968                  { galleryId: matchingItem.id },
17969                  matchingItem.title || document.title,
17970                  targetUrl
17971                );
17972                
17973                // Update parent window title
17974                parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent);
17975                
17976                // Scroll parent to top
17977                parent.scrollTo(0, 0);
17978                return;
17979              } 
17980              // Special handling for preview mode - critical fix here!
17981              else if (isPreviewMode && siteGuid) {
17982                // Special handling for preview URLs
17983                e.preventDefault();
17984                e.stopPropagation();
17985                
17986                console.log('Iframe: No direct match but in preview mode - handling navigation');
17987                
17988                // Construct a URL that preserves the GUID
17989                let newPathname = url.pathname;
17990                if (!newPathname.startsWith(`/${siteGuid}/`)) {
17991                  newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17992                }
17993                
17994                // Create a URL for the browser history
17995                const browserUrl = new URL(window.location.origin);
17996                browserUrl.pathname = newPathname;
17997                
17998                console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href);
17999                
18000                // Update browser URL in parent
18001                parent.history.pushState(
18002                  { customPath: newPathname },
18003                  document.title,
18004                  browserUrl.href
18005                );
18006                
18007                // Handle the navigation in the parent window
18008                if (isPrimary && parent.document.getElementById('galleryFrame')) {
18009                  const galleryFrame = parent.document.getElementById('galleryFrame');
18010                  
18011                  // Create clean path for the gallery system
18012                  const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
18013                  
18014                  // Create wrapper URL for gallery content
18015                  const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
18016                    `?siteId=${siteGuid}` +
18017                    `&path=${encodeURIComponent(cleanPath)}` + 
18018                    `&directPath=true` +
18019                    `&t=${Date.now()}`; // Cache buster
18020                  
18021                  console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl);
18022                  galleryFrame.src = wrapperUrl;
18023                  
18024                  // Clear any active gallery ID since we're navigating directly
18025                  if (parent.activeGalleryId) {
18026                    console.log('Iframe: Clearing active gallery ID during direct navigation');
18027                    parent.activeGalleryId = null;
18028                  }
18029                } else {
18030                  // Fallback - navigate normally in case we can't handle it
18031                  console.log('Iframe: Gallery frame not found or not primary, navigating normally');
18032                  window.location.href = browserUrl.href;
18033                }
18034                return;
18035              }
18036            }
18037          } catch (iframeError) {
18038            console.error('Error handling iframe link:', iframeError);
18039          }
18040        });
18041        
18042        if (isPrimary) {
18043          console.log('Successfully intercepted links in primary gallery iframe');
18044        }
18045      } catch (sameOriginError) {
18046        // This is expected for cross-origin iframes
18047        if (isPrimary) {
18048          console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.');
18049          
18050          // Apply a fallback method - add postMessage listener
18051          setupPostMessageListener(iframe);
18052        }
18053      }
18054    } catch (error) {
18055      console.warn('Error setting up iframe link interception:', error);
18056    }
18057  }
18058  
18059  // Fallback: use postMessage for cross-origin iframes
18060  function setupPostMessageListener(iframe) {
18061    // Listen for messages from the iframe
18062    window.addEventListener('message', function(event) {
18063      try {
18064        // Verify the message is from our iframe
18065        if (event.source !== iframe.contentWindow) return;
18066        
18067        const data = event.data;
18068        
18069        // Check if this is a link click event
18070        if (data && data.type === 'linkClick' && data.href) {
18071          console.log('Received link click message from iframe:', data.href);
18072          
18073          // Create a URL object to work with the link
18074          const url = new URL(data.href);
18075          
18076          // Check if it's a local navigation
18077          if (url.hostname === window.location.hostname) {
18078            const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
18079            const siteGuid = getSiteGuidFromPath();
18080            
18081            // Try to find a matching gallery
18082            const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
18083            
18084            if (matchingItem) {
18085              console.log('Found matching gallery for postMessage link:', matchingItem.title);
18086              
18087              // Navigate using the appropriate function
18088              if (matchingItem.isPage && window.loadPage) {
18089                window.loadPage(matchingItem.id);
18090              } else if (window.loadGallery) {
18091                window.loadGallery(matchingItem.id);
18092              }
18093              
18094              // Update URL without reload
18095              let targetUrl = url.href;
18096              if (isPreviewMode && siteGuid) {
18097                // Ensure GUID is preserved
18098                const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
18099                  url.pathname : 
18100                  `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
18101                
18102                const newUrl = new URL(window.location.origin + pathname);
18103                targetUrl = newUrl.href;
18104              }
18105              
18106              window.history.pushState(
18107                { galleryId: matchingItem.id },
18108                matchingItem.title || document.title,
18109                targetUrl
18110              );
18111              
18112              // Update page title
18113              document.title = formatGalleryDocumentTitle(matchingItem.title);
18114              
18115              return;
18116            } 
18117            // Special handling for preview mode - Fixed!
18118            else if (isPreviewMode && siteGuid) {
18119              console.log('No direct match but in preview mode - handling navigation via postMessage');
18120              
18121              // Construct URL that preserves the GUID
18122              let newPathname = url.pathname;
18123              if (!newPathname.startsWith(`/${siteGuid}/`)) {
18124                newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
18125              }
18126              
18127              // Create browser URL
18128              const browserUrl = new URL(window.location.origin);
18129              browserUrl.pathname = newPathname;
18130              
18131              // Update browser URL 
18132              window.history.pushState(
18133                { customPath: newPathname },
18134                document.title,
18135                browserUrl.href
18136              );
18137              
18138              // Get the main gallery frame - NOT the iframe that sent the message
18139              const galleryFrame = document.getElementById('galleryFrame');
18140              if (galleryFrame) {
18141                // Create clean path without GUID
18142                const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
18143                
18144                // Create wrapper URL
18145                const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
18146                  `?siteId=${siteGuid}` +
18147                  `&path=${encodeURIComponent(cleanPath)}` + 
18148                  `&directPath=true` +
18149                  `&t=${Date.now()}`; // Cache buster
18150                
18151                console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl);
18152                galleryFrame.src = wrapperUrl;
18153                
18154                // Clear any active gallery ID
18155                if (window.activeGalleryId) {
18156                  window.activeGalleryId = null;
18157                }
18158                
18159                return;
18160              } else {
18161                console.warn('Gallery frame not found - cannot load content via postMessage');
18162              }
18163            }
18164          }
18165        }
18166      } catch (error) {
18167        console.error('Error processing postMessage from iframe:', error);
18168      }
18169    });
18170    
18171    // Inject a script into the iframe if possible to send messages back
18172    try {
18173      // We might be able to inject a script tag even in cross-origin scenarios
18174      // if the iframe is not yet loaded or using document.write
18175      iframe.addEventListener('load', function() {
18176        try {
18177          const script = iframe.contentDocument.createElement('script');
18178          script.textContent = `
18179            // Link interception script injected from parent
18180            document.addEventListener('click', function(e) {
18181              const link = e.target.closest('a');
18182              if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
18183                // Send message to parent window
18184                window.parent.postMessage({
18185                  type: 'linkClick',
18186                  href: link.href,
18187                  pathname: link.pathname
18188                }, '*');
18189                
18190                // Prevent default navigation
18191                e.preventDefault();
18192              }
18193            });
18194            console.log('Gallery link interceptor injected from parent');
18195          `;
18196          iframe.contentDocument.head.appendChild(script);
18197        } catch (e) {
18198          // This will likely fail for cross-origin iframes
18199          console.log('Could not inject script into iframe - expected for cross-origin frames');
18200        }
18201      });
18202    } catch (error) {
18203      console.warn('Error trying to inject script into iframe:', error);
18204    }
18205  }
18206  
18207  // Observer for new content/iframes
18208  function setupMutationObserver() {
18209    if (!window.MutationObserver) return;
18210    
18211    const observer = new MutationObserver(function(mutations) {
18212      let newIframesFound = false;
18213      
18214      mutations.forEach(function(mutation) {
18215        if (mutation.type === 'childList' && mutation.addedNodes.length) {
18216          // Check if any new iframes were added
18217          mutation.addedNodes.forEach(function(node) {
18218            // Check if the node is an iframe or contains iframes
18219            if (node.nodeName === 'IFRAME') {
18220              newIframesFound = true;
18221              setupIframeIntercept(node, node.id === 'galleryFrame');
18222            } else if (node.querySelectorAll) {
18223              const iframes = node.querySelectorAll('iframe');
18224              if (iframes.length > 0) {
18225                newIframesFound = true;
18226                iframes.forEach(function(iframe) {
18227                  setupIframeIntercept(iframe, iframe.id === 'galleryFrame');
18228                });
18229              }
18230            }
18231          });
18232        }
18233      });
18234      
18235      // If new iframes were found, update our cache
18236      if (newIframesFound) {
18237        refreshGalleryCache();
18238      }
18239    });
18240    
18241    // Observe the entire document
18242    observer.observe(document.body, {
18243      childList: true,
18244      subtree: true
18245    });
18246  }
18247  
18248  // Helper to check if a URL is a resource rather than a page
18249  function isResourceUrl(path) {
18250    const resourceExtensions = [
18251      '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', 
18252      '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2'
18253    ];
18254    return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext));
18255  }
18256  
18257  // Get the site GUID from the path in preview mode
18258  function getSiteGuidFromPath() {
18259    if (window.location.hostname === 'preview.neonsky.app') {
18260      const pathParts = window.location.pathname.split('/').filter(
18260Boolean);
18261      if (pathParts.length > 0) {
18262        return pathParts[0];
18263      }
18264    }
18265    return null;
18266  }
18267  
18268
18269/**
18270 * Enhanced gallery path matching with improved accuracy:
18271 * 
18272 * This function finds the appropriate gallery based on URL path with better
18273 * prioritization and more precise matching criteria.
18274 * 
18275 * @param {string} path - The URL path to match
18276 * @param {boolean} isPreviewMode - Whether we're in preview mode
18277 * @param {string} siteGuid - The site GUID (for preview mode)
18278 * @returns {Object|null} The matching gallery or null if none found
18279 */
18280function findGalleryByPath(path, isPreviewMode, siteGuid) {
18281  // Ensure path is a string
18282  if (typeof path !== 'string') {
18283    console.warn('Invalid path provided to findGalleryByPath:', path);
18284    return null;
18285  }
18286
18287  // Determine which galleries array to use - check global variable first
18288  let galleriesData;
18289  let galleryCount = 0;
18290  
18291  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
18292    galleriesData = galleries;
18293    galleryCount = galleries.length;
18294  } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
18295    galleriesData = window.galleries;
18296    galleryCount = window.galleries.length;
18297  } else if (window.GALLERY_DATA_CACHE && 
18298             window.GALLERY_DATA_CACHE.initialized && 
18299             Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
18300    galleriesData = window.GALLERY_DATA_CACHE.galleries;
18301    galleryCount = window.GALLERY_DATA_CACHE.galleries.length;
18302  } else {
18303    console.warn('No galleries array available');
18304    return null;
18305  }
18306  
18307  console.log(`Searching for match among ${galleryCount} galleries`);
18308
18309  // Strip leading/trailing slashes without regex — template-literal embedding mangles / escapes
18310  // (e.g. /^/|/$/g becomes /^/|/$/g → 0), which left "/" unnormalized and broke home matching.
18311  function stripEdgeSlashes(value) {
18312    return String(value || '').split('/').filter(Boolean).join('/');
18313  }
18314  
18315  // Normalize the path for comparison (handle trailing/leading slashes)
18316  let normalizedPath = stripEdgeSlashes(path).toLowerCase();
18317  const originalNormalizedPath = normalizedPath; // Keep original for logging
18318  
18319  // If this is a preview URL, we need to handle the path differently
18320  if (isPreviewMode && siteGuid) {
18321    // Check if the path starts with the GUID
18322    if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) {
18323      // Remove the GUID prefix
18324      normalizedPath = normalizedPath.substring(siteGuid.length + 1);
18325    } else if (normalizedPath === siteGuid.toLowerCase()) {
18326      // This is just the GUID with no additional path
18327      normalizedPath = '';
18328    }
18329  }
18330  
18331  // Create a dash-normalized version (all sequences of dashes become single dashes)
18332  // This handles both double-dash patterns and any other dash inconsistencies
18333  const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-');
18334  
18335  console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`);
18336  
18337  // For empty paths, try to find the homepage
18338  if (!normalizedPath) {
18339    // Look for a gallery marked as home page
18340    const homePage = galleriesData.find(g => g.isHomePage === true);
18341    if (homePage) {
18342      console.log('Empty path - returning homepage:', homePage.title);
18343      return homePage;
18344    }
18345  }
18346
18347  // Array to store potential matches with their confidence levels
18348  const potentialMatches = [];
18349  
18350  // Process each gallery to find potential matches with confidence levels
18351  galleriesData.forEach(gallery => {
18352    // Skip invalid galleries
18353    if (!gallery || !gallery.id) return;
18354    
18355    // Store matching details for debugging
18356    const matchInfo = {
18357      gallery,
18358      confidence: 0,
18359      matchType: null,
18360      usedNormalizedDashes: false
18361    };
18362    
18363    // 1. EXACT SLUG MATCH (highest confidence)
18364    if (gallery.slug) {
18365      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18366      
18367      // Try original path
18368      if (gallerySlug === normalizedPath) {
18369        matchInfo.confidence = 100; // Max confidence
18370        matchInfo.matchType = 'exact_slug';
18371        potentialMatches.push({...matchInfo});
18372        return; // Continue to next gallery
18373      }
18374      
18375      // Try dash-normalized path
18376      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18377      if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) {
18378        matchInfo.confidence = 95; // Very high confidence
18379        matchInfo.matchType = 'exact_slug_normalized_dashes';
18380        matchInfo.usedNormalizedDashes = true;
18381        potentialMatches.push({...matchInfo});
18382        return; // Continue to next gallery
18383      }
18384    }
18385    
18386    // 2. EXACT URL PATH MATCH (very high confidence)
18387    if (gallery.url) {
18388      let galleryPath = gallery.url.toLowerCase();
18389      
18390      if (galleryPath.startsWith('http')) {
18391        try {
18392          const urlObj = new URL(galleryPath);
18393          galleryPath = urlObj.pathname;
18394        } catch (e) {
18395          // If URL parsing fails, use the string as is
18396        }
18397      }
18398      
18399      galleryPath = stripEdgeSlashes(galleryPath);
18400      
18401      // Try original path
18402      if (galleryPath === normalizedPath) {
18403        matchInfo.confidence = 90; // Very high confidence
18404        matchInfo.matchType = 'exact_url_path';
18405        potentialMatches.push({...matchInfo});
18406        return; // Continue to next gallery
18407      }
18408      
18409      // Try dash-normalized path
18410      const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-');
18411      if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) {
18412        matchInfo.confidence = 85; // High confidence
18413        matchInfo.matchType = 'exact_url_path_normalized_dashes';
18414        matchInfo.usedNormalizedDashes = true;
18415        potentialMatches.push({...matchInfo});
18416        return; // Continue to next gallery
18417      }
18418    }
18419    
18420    // 3. TITLE SLUG MATCH (high confidence)
18421    if (gallery.title) {
18422      const titleSlug = gallery.title.toLowerCase()
18423        .replace(/s+/g, '-')
18424        .replace(/[^w-]+/g, '')
18425        .replace(/--+/g, '-')
18426        .trim();
18427      
18428      // Try original path
18429      if (titleSlug === normalizedPath) {
18430        matchInfo.confidence = 80; // High confidence
18431        matchInfo.matchType = 'title_slug_match';
18432        potentialMatches.push({...matchInfo});
18433        return; // Continue to next gallery
18434      }
18435      
18436      // Try dash-normalized path
18437      const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-');
18438      if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) {
18439        matchInfo.confidence = 75; // Good confidence
18440        matchInfo.matchType = 'title_slug_match_normalized_dashes';
18441        matchInfo.usedNormalizedDashes = true;
18442        potentialMatches.push({...matchInfo});
18443        return; // Continue to next gallery
18444      }
18445    }
18446    
18447    // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA
18448    if (gallery.slug && normalizedPath.length > 5) {
18449      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18450      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18451      
18452      // Only if paths are substantial (to avoid false positives)
18453      if (normalizedPath.length >= 5 && gallerySlug.length >= 5) {
18454        // Try matching significant portions with dash normalization
18455        
18456        // If the normalized paths contain each other with substantial overlap
18457        if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || 
18458            gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) {
18459          
18460          // Calculate overlap ratio based on the shorter string
18461          const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / 
18462                               Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length);
18463          
18464          // Only consider matches with substantial overlap (over 70%)
18465          if (overlapRatio >= 0.7) {
18466            const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap
18467            
18468            matchInfo.confidence = score;
18469            matchInfo.matchType = 'partial_match_normalized';
18470            matchInfo.usedNormalizedDashes = true;
18471            matchInfo.overlapRatio = overlapRatio;
18472            potentialMatches.push({...matchInfo});
18473          }
18474        }
18475      }
18476    }
18477  });
18478  
18479  // No potential matches found
18480  if (potentialMatches.length === 0) {
18481    console.log('No gallery match found for path:', path);
18482    return null;
18483  }
18484  
18485  // Sort potential matches by confidence (highest first)
18486  potentialMatches.sort((a, b) => b.confidence - a.confidence);
18487  
18488  // Get the best match
18489  const bestMatch = potentialMatches[0];
18490  
18491  // Log detailed debugging info about the match
18492  console.log(`Best match for "${normalizedPath}":`, {
18493    title: bestMatch.gallery.title,
18494    id: bestMatch.gallery.id,
18495    matchType: bestMatch.matchType,
18496    confidence: bestMatch.confidence,
18497    usedNormalizedDashes: bestMatch.usedNormalizedDashes,
18498    totalMatches: potentialMatches.length
18499  });
18500  
18501  // If we have multiple close matches, log them for debugging
18502  if (potentialMatches.length > 1 && 
18503      potentialMatches[1].confidence >= bestMatch.confidence - 10) {
18504    console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({
18505      title: m.gallery.title,
18506      id: m.gallery.id,
18507      matchType: m.matchType,
18508      confidence: m.confidence
18509    }
18509)));
18510  }
18511  
18512  // Return the best matching gallery
18513  return bestMatch.gallery;
18514}
18515  
18516  // Make helpful functions available globally
18517  window.findGalleryByPath = findGalleryByPath;
18518  window.findGalleryById = findGalleryById;
18519  window.refreshGalleryCache = refreshGalleryCache;
18520  window.getSiteGuidFromPath = getSiteGuidFromPath;
18521  if (typeof getSiteGalleriesArray === 'function') {
18522    window.getSiteGalleriesArray = getSiteGalleriesArray;
18523  }
18524})();
18525
18526
18527
18528    </script>
18528
18529  </div>
18530  
18531<!-- Sidebar element form for adding new elements -->
18532<div class="sidebar-element-form pe-edit-shell" id="sidebarElementForm" style="display: none;" data-nosnippet>
18533<p class="pe-edit-shell__title">Add sidebar element</p>
18534<p class="pe-hint">Select the type of element to add:</p>
18535
18536<div class="radio-group">
18537  <label>
18538    <input type="radio" name="sidebarElementType" value="text" checked> Text Block
18539  </label>
18540  <label>
18541    <input type="radio" name="sidebarElementType" value="image"> Image
18542  </label>
18543  <label>
18544    <input type="radio" name="sidebarElementType" value="social"> Social Icons
18545  </label>
18546  <label>
18547    <input type="radio" name="sidebarElementType" value="spacer"> Spacer
18548  </label>
18549</div>
18550
18551<div class="pe-footer-actions">
18552<button type="button" class="pe-btn pe-btn--primary" onclick="addSidebarElement()">Add element</button>
18553<button type="button" class="pe-btn" onclick="toggleSidebarElementForm()">Cancel</button>
18554</div>
18555</div>
18556
18557 <div class="add-form pe-edit-shell" id="addForm" data-nosnippet>
18558  <p class="pe-edit-shell__title">Add to menu</p>
18559  <input type="text" id="galleryTitle" placeholder="Gallery Title">
18560 <div class="radio-group">
18561   <label>
18562    <input type="radio" name="galleryType" value="integrated" checked> Gallery
18563  </label>
18564    <label>
18565    <input type="radio" name="galleryType" value="page"> Page
18566  </label>
18567  <label>
18568    <input type="radio" name="galleryType" value="folder"> Folder
18569  </label>
18570  <label>
18571    <input type="radio" name="galleryType" value="spacer"> Spacer
18572  </label>
18573    <label>
18574    <input type="radio" name="galleryType" value="external"> External URL
18575  </label>
18576</div>
18577  <div id="urlInputContainer">
18578    <input type="text" id="galleryUrl" placeholder="Gallery URL">
18579  </div>
18580  <div id="galleryParentContainer" style="display: none;" aria-hidden="true">
18581    <label for="galleryParent">Parent Menu:</label>
18582    <select id="galleryParent">
18583      <option value="">None (Top Level)</option>
18584    </select>
18585  </div>
18586  <div class="pe-footer-actions">
18587    <button type="button" class="pe-btn pe-btn--primary" onclick="addGallery()">Save</button>
18588  </div>
18589    
18589<script>
18590
18591
18592      // Add event listeners to the radio buttons
18593      document.addEventListener('DOMContentLoaded', () => {
18594        const radioButtons = document.querySelectorAll('input[name="galleryType"]');
18595        radioButtons.forEach(radio => {
18596          radio.addEventListener('change', function() {
18597            const urlContainer = document.getElementById('urlInputContainer');
18598            if (this.value === 'external') {
18599              urlContainer.style.display = 'block';
18600            } else {
18601              urlContainer.style.display = 'none';
18602            }
18603          });
18604        });
18605      });
18606    </script>
18606
18607  </div>
18608
18609<div class="metadata-editor pe-edit-shell" id="metadataEditor" style="display: none; max-height: 90vh; overflow-y: auto;" data-nosnippet>
18610  <h3 class="pe-edit-shell__title">Site Metadata</h3>
18611  
18612  <div class="form-group">
18613    <label for="metaTitle">Page Title (Search Results & Browsers)</label>
18614    <input type="text" id="metaTitle" class="form-control" placeholder="Enter site title for search results">
18615    <small>Recommended length: 50-60 characters</small>
18616  </div>
18617  
18618  <div class="form-group">
18619    <label for="metaDescription">Meta Description</label>
18620    <textarea id="metaDescription" class="form-control" rows="3" placeholder="Enter site description for search results"></textarea>
18621    <small>Recommended length: 150-160 characters</small>
18622  </div>
18623
18624  <div class="form-group">
18625    <label for="ogImageUrl">Social preview image (Open Graph / X / iMessage)</label>
18626    <input type="hidden" id="ogImageUrl" class="form-control" value="">
18627    <button type="button" id="uploadOgImageBtn" class="pe-btn upload-image-btn">
18628      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18629        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18630        <polyline points="17 8 12 3 7 8"></polyline>
18631        <line x1="12" y1="3" x2="12" y2="15"></line>
18632      </svg>
18633      <span id="uploadOgImageBtnText">Upload image</span>
18634    </button>
18635    <div id="ogImagePreviewContainer"></div>
18636    <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>
18637  </div>
18638  
18639  <div class="form-group">
18640    <label for="googleAnalytics">Google Analytics Code</label>
18641    <input type="text" id="googleAnalytics" class="form-control" placeholder="Enter GA-XXXXXXXXXX">
18642    <small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X</small>
18643  </div>
18644  
18645  <div class="form-group">
18646    <label for="contentLanguage">Content Language</label>
18647    <select id="contentLanguage" class="form-control">
18648      <option value="en">English (en)</option>
18649      <option value="es">Spanish / Español (es)</option>
18650      <option value="fr">French / Français (fr)</option>
18651      <option value="ne">Nepali / नेपाली (ne)</option>
18652      <option value="de">German / Deutsch (de)</option>
18653      <option value="it">Italian / Italiano (it)</option>
18654      <option value="pt">Portuguese / Português (pt)</option>
18655      <option value="ru">Russian / Русский (ru)</option>
18656      <option value="zh">Chinese / 中文 (zh)</option>
18657      <option value="ja">Japanese / 日本語 (ja)</option>
18658      <option value="ko">Korean / 한국어 (ko)</option>
18659      <option value="ar">Arabic / العربية (ar)</option>
18660      <option value="hi">Hindi / हिन्दी (hi)</option>
18661    </select>
18662    <small>Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.</small>
18663  </div>
18664  
18665  <div class="form-group">
18666    <label>
18667      <input type="checkbox" id="noIndexToggle"> 
18668      Hide site from search engines (noindex)
18669    </label>
18670    <small>Use this if the site is under development or shouldn't appear in search results</small>
18671  </div>
18672
18673  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18674    <h4 style="margin-bottom: 8px;">Schema / Identity</h4>
18675    <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>
18676    <label style="display: block; margin-bottom: 6px;">This site represents:</label>
18677    <div style="display: flex; gap: 20px; margin-bottom: 4px;">
18678      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18679        <input type="radio" name="schemaType" id="schemaTypePerson" value="Person" onchange="updateSchemaFields()"> Person / Individual
18680      </label>
18681      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18682        <input type="radio" name="schemaType" id="schemaTypeOrganization" value="Organization" onchange="updateSchemaFields()"> Organization / Business
18683      </label>
18684    </div>
18685    <small>Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.</small>
18686  </div>
18687
18688  <div class="form-group" id="schemaJobTitleGroup" style="display: none;">
18689    <label for="schemaJobTitle">Job Title</label>
18690    <input type="text" id="schemaJobTitle" class="form-control" placeholder="">
18691    <small>Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").</small>
18692  </div>
18693
18694  <div class="form-group" id="schemaCityGroup" style="display: none;">
18695    <label for="schemaCity">City / Location</label>
18696    <input type="text" id="schemaCity" class="form-control" placeholder="">
18697    <small>Improves visibility in location-based searches.</small>
18698  </div>
18699
18700  <div class="form-group" id="schemaEmailGroup" style="display: none;">
18701    <label for="schemaEmail">Contact Email (for structured data)</label>
18702    <input type="text" id="schemaEmail" class="form-control" placeholder="e.g. [email protected]">
18703    <small>Embedded in structured data only — not displayed on the page.</small>
18704  </div>
18705
18706  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18707    <h4 style="margin-bottom: 8px;">
18707Image licensing (optional)</h4>
18708    <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>
18709    <label for="imageLicenseUrl">License page URL</label>
18710    <input type="url" id="imageLicenseUrl" class="form-control" placeholder="https://example.com/licensing">
18711    <small>A public page that states how images may be used (your terms, or a Creative Commons license).</small>
18712  </div>
18713
18714  <div class="form-group">
18715    <label for="acquireLicensePageUrl">Acquire license page URL</label>
18716    <input type="url" id="acquireLicensePageUrl" class="form-control" placeholder="https://example.com/contact">
18717    <small>A public page where someone can request or buy a license. Both URLs are required, or leave both empty.</small>
18718  </div>
18719
18720  <div class="form-group">
18721    <label>
18722      <input type="checkbox" id="metadataCopyrightEnabled">
18723      Enable sidebar copyright footer
18724    </label>
18725    <small>When enabled, the copyright text shows at the bottom of the sidebar.</small>
18726  </div>
18727
18728  <div class="form-group" id="metadataCopyrightTextGroup">
18729    <label for="metadataCopyrightText">Copyright Text</label>
18730    <input type="text" id="metadataCopyrightText" class="form-control" placeholder="© 2025 Your Name">
18731    <small>This text appears in the sidebar footer and inside the copyright overlay.</small>
18732  </div>
18733  
18734  <div class="form-group">
18735    <label for="faviconUrl">Favicon</label>
18736    <input type="hidden" id="faviconUrl" class="form-control">
18737    <button type="button" id="uploadFaviconBtn" class="pe-btn upload-image-btn">
18738      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18739        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18740        <polyline points="17 8 12 3 7 8"></polyline>
18741        <line x1="12" y1="3" x2="12" y2="15"></line>
18742      </svg>
18743      <span id="uploadFaviconBtnText">Upload Favicon</span>
18744    </button>
18745    <div id="faviconPreviewContainer"></div>
18746    <small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)</small>
18747  </div>
18748
18749  <div class="form-group" id="customPublicSiteBypassGroup" hidden style="display: none; border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18750    <h4 style="margin-bottom: 12px;">Custom HTML site (full bypass)</h4>
18751    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18752      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.
18753      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.
18754      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).
18755    </p>
18756    <label>
18757      <input type="checkbox" id="customPublicSiteEnabled" />
18758      Serve uploaded HTML as the public site (bypass Hydra shell on <code>/</code>)
18759    </label>
18760    <input type="hidden" id="customPublicSiteHtmlR2Key" value="" />
18761    <input type="hidden" id="customPublicSiteHtmlFilenameStore" value="" />
18762    <div class="form-group" style="margin-top: 10px;">
18763      <button type="button" id="uploadCustomPublicHtmlBtn" class="pe-btn">Upload HTML file</button>
18764      <button type="button" id="removeCustomPublicHtmlBtn" class="pe-btn">Clear file</button>
18765    </div>
18766    <p id="customPublicSiteHtmlFilenameLabel" style="font-size: 13px; color: #666; margin-top: 8px;">No HTML file uploaded yet.</p>
18767  </div>
18768
18769  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18770    <h4 style="margin-bottom: 8px;">Site-wide password (browser login)</h4>
18771    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18772      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
18773      <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.
18774    </p>
18775    <label>
18776      <input type="checkbox" id="siteWideBasicAuthEnabled" onchange="updateSiteWideBasicAuthFieldState()" />
18777      Require HTTP Basic password for the entire site
18778    </label>
18779    <div id="siteWideBasicAuthFields" style="display: none; margin-top: 12px;">
18780      <div class="form-group">
18781        <label for="siteWideBasicAuthUsername">Username</label>
18782        <input type="text" id="siteWideBasicAuthUsername" class="form-control" autocomplete="username" />
18783      </div>
18784      <div class="form-group">
18785        <label for="siteWideBasicAuthPassword">Password</label>
18786        <input type="password" id="siteWideBasicAuthPassword" class="form-control" autocomplete="new-password" />
18787        <small>Stored in your site configuration. Use a strong password; anyone with it can view the site.</small>
18788      </div>
18789      <div class="form-group">
18790        <label for="siteWideBasicAuthRealm">Realm (optional)</label>
18791        <input type="text" id="siteWideBasicAuthRealm" class="form-control" placeholder="Site" />
18792        <small>Label shown in the browser login dialog.</small>
18793      </div>
18794    </div>
18795  </div>
18796  
18797  <div class="form-actions pe-footer-actions">
18798    <button type="button" class="pe-btn pe-btn--primary" onclick="saveMetadata()">Save Metadata</button>
18799    <button type="button" class="pe-btn" onclick="toggleMetadataEditor()">Cancel</button>
18800  </div>
18801</div>
18802
18803<script>
18804var customPublicSiteOptionHeld = false;
18805var customPublicSiteOptionKeyBound = false;
18806
18807function isMetadataEditorOpen() {
18808  var editor = document.getElementById('metadataEditor');
18809  if (!editor) return false;
18810  if (window.currentOpenForm === 'metadataEditor') return true;
18811  return editor.style.display === 'block';
18812}
18813
18814function syncCustomPublicSiteBypassVisibility() {
18815  var group = document.getElementById('customPublicSiteBypassGroup');
18816  if (!group) return;
18817  var show = isMetadataEditorOpen() && customPublicSiteOptionHeld;
18818  group.hidden = !show;
18819  group.style.display = show ? 'block' : 'none';
18820}
18821
18822function bindCustomPublicSiteOptionKeyListeners() {
18823  if (customPublicSiteOptionKeyBound) return;
18824  customPublicSiteOptionKeyBound = true;
18825
18826  document.addEventListener('keydown', function(e) {
18827    if (!e.altKey) return;
18828    customPublicSiteOptionHeld = true;
18829    syncCustomPublicSiteBypassVisibility();
18830  }, true);
18831
18832  document.addEventListener('keyup', function(e) {
18833    var isOptionRelease =
18834      e.key === 'Alt' ||
18835      e.key === 'Option' ||
18836      e.code === 'AltLeft' ||
18837      e.code === 'AltRight';
18838    if (!isOptionRelease) return;
18839    customPublicSiteOptionHeld = false;
18840    syncCustomPublicSiteBypassVisibility();
18841  }, true);
18842}
18843
18844window.syncCustomPublicSiteBypassVisibility = syncCustomPublicSiteBypassVisibility;
18845window.bindCustomPublicSiteOptionKeyListeners = bindCustomPublicSiteOptionKeyListeners;
18846
18847function updateSiteWideBasicAuthFieldState() {
18848  const en = document.getElementById('siteWideBasicAuthEnabled');
18849  const fields = document.getElementById('siteWideBasicAuthFields');
18850  if (fields) fields.style.display = en && en.checked ? 'block' : 'none';
18851}
18852
18853function updateSchemaFields() {
18854  const isPerson = document.getElementById('schemaTypePerson') && document.getElementById('schemaTypePerson').checked;
18855  const isOrg = document.getElementById('schemaTypeOrganization') && document.getElementById('schemaTypeOrganization').checked;
18856  const anySelected = isPerson || isOrg;
18857  const jobTitleGroup = document.getElementById('schemaJobTitleGroup');
18858  const cityGroup = document.getElementById('schemaCityGroup');
18859  const emailGroup = document.getElementById('schemaEmailGroup');
18860  if (jobTitleGroup) jobTitleGroup.style.display = isPerson ? 'block' : 'none';
18861  if (cityGroup) cityGroup.style.display = anySelected ? 'block' : 'none';
18862  if (emailGroup) emailGroup.style.display = anySelected ? 'block' : 'none';
18863}
18864
18865function uploadCustomPublicHtmlFile() {
18866  const siteId = window.Parameters && window.Parameters.siteId ? window.Parameters.siteId : (window.siteId || '');
18867  if (!siteId) {
18868    alert('Site ID not found. Refresh the page and try again.');
18869    return;
18870  }
18871  const authToken = window.ImageUploader && window.ImageUploader.getAuthToken ? window.ImageUploader.getAuthToken() : null;
18872  if (!authToken) {
18873    alert('Please log in first (press + on the page).');
18874    return;
18875  }
18876  const input = document.createElement('input');
18877  input.type = 'file';
18878  input.accept = '.html,.htm,text/html';
18879  input.addEventListener('change', function() {
18880    const file = input.files && input.files[0];
18881    if (!file) return;
18882    const formData = new FormData();
18883    formData.append('file', file);
18884    formData.append('guid', siteId);
18885    formData.append('purpose', 'custom-public-site');
18886    formData.append('pageId', '_site');
18887    formData.append('elementId', 'custom-public-html');
18888    formData.append('folderPath', '');
18889    var relUpload = typeof window.getApiUrl === 'function' ? window.getApiUrl('/api/upload-file') : '/api/upload-file';
18890    var uploadUrl = window.location && window.location.origin ? new URL(relUpload, window.location.origin).href : relUpload;
18891    fetch(uploadUrl, {
18892      method: 'POST',
18893      headers: { 'Authorization': authToken, 'X-User-Email': localStorage.getItem('hydra_auth_email') || '' },
18894      body: formData
18895    }).then(function(r) {
18896      return r.text().then(function(text) {
18897        var ct = (r.headers.get('content-type') || '').toLowerCase();
18898        if (ct.indexOf('application/json') === -1 && /<!DOCTYPE/i.test(text)) {
18899          throw new Error(
18900            'Server returned HTML instead of JSON — POST /api/upload-file is not reaching the Hydra worker. ' +
18901            'In Cloudflare, ensure this hostname has a Worker route (e.g. neonsky.app/*) so API traffic is not sent to another host or Pages.'
18902          );
18903        }
18904        if (r.status === 403 && ct.indexOf('application/json') === -1) {
18905          throw new Error(
18906            '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.'
18907          );
18908        }
18909        try {
18910          return JSON.parse(text);
18911        } catch (parseErr) {
18912          throw new Error('Server did not return JSON: ' + text.slice(0, 120) + (text.length > 120 ? '...' : ''));
18913        }
18914      });
18915    }).then(function(res) {
18916      if (res.success && res.key) {
18917        var keyEl = document.getElementById('customPublicSiteHtmlR2Key');
18918        var fnEl = document.getElementById('customPublicSiteHtmlFilenameStore');
18919        var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
18920        if (keyEl) keyEl.value = res.key;
18921        if (fnEl) fnEl.value = file.name;
18922        if (lbl) lbl.textContent = 'Current file: ' + file.name + ' — click Save Metadata below to apply.';
18923        window.siteMetadata = window.siteMetadata || {};
18924        window.siteMetadata.customPublicSiteHtmlR2Key = res.key;
18925        window.siteMetadata.customPublicSiteHtmlFilename = file.name;
18926      } else {
18927        alert(res.error || res.details || 'Upload failed');
18928      }
18929    }).catch(function(e) {
18930      alert('Upload failed: ' + (e && e.message ? e.message : 'Unknown error'));
18931    });
18932  });
18933  input.click();
18934}
18935
18936function clearCustomPublicHtmlFile() {
18937  if (!confirm('Clear the uploaded HTML file reference? (You can upload again later.)')) return;
18938  var k = document.getElementById('customPublicSiteHtmlR2Key');
18939  var fn = document.getElementById('customPublicSiteHtmlFilenameStore');
18940  var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
18941  if (k) k.value = '';
18942  if (fn) fn.value = '';
18943  if (lbl) lbl.textContent = 'No HTML file uploaded yet.';
18944  if (window.siteMetadata) {
18945    window.siteMetadata.customPublicSiteHtmlR2Key = '';
18946    window.siteMetadata.customPublicSiteHtmlFilename = '';
18947  }
18948}
18949
18950// Handle favicon upload button click
18951document.addEventListener('DOMContentLoaded', function() {
18952  const uploadBtn = document.getElementById('uploadFaviconBtn');
18953  if (uploadBtn) {
18954    uploadBtn.addEventListener('click', function() {
18955      uploadFavicon();
18956    });
18957  }
18958  const uploadOgBtn = document.getElementById('uploadOgImageBtn');
18959  if (uploadOgBtn) {
18960    uploadOgBtn.addEventListener('click', function() {
18961      uploadSiteOgImage();
18962    });
18963  }
18964  var upHtml = document.getElementById('uploadCustomPublicHtmlBtn');
18965  if (upHtml) upHtml.addEventListener('click', uploadCustomPublicHtmlFile);
18966  var rmHtml = document.getElementById('removeCustomPublicHtmlBtn');
18967  if (rmHtml) rmHtml.addEventListener('click', clearCustomPublicHtmlFile);
18968  bindCustomPublicSiteOptionKeyListeners();
18969  syncCustomPublicSiteBypassVisibility();
18970  
18971  // Handle content language dropdown change
18972  const contentLanguageSelect = document.getElementById('contentLanguage');
18973  if (contentLanguageSelect) {
18974    contentLanguageSelect.addEventListener('change', function() {
18975      console.warn('Content language changed to:', this.value);
18976      // Update global variable immediately
18977      window.siteMetadata.contentLanguage = this.value;
18978      console.warn('Updated window.siteMetadata.contentLanguage:', window.siteMetadata.contentLanguage);
18979    });
18980  }
18981
18982  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
18983  if (copyrightToggle) {
18984    copyrightToggle.addEventListener('change', updateMetadataCopyrightFieldState);
18985  }
18986
18987  updateMetadataCopyrightFieldState();
18988});
18989
18990// Function to handle favicon upload using ImageUploader
18991function uploadFavicon() {
18992  console.log('Starting favicon upload...');
18993  
18994  // Check if ImageUploader is available
18995  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
18996    alert('Image uploader not available. Please enter a favicon URL manually.');
18997    return;
18998  }
18999  
19000  try {
19001    // Create options for the ImageUploader
19002    const options = {
19003      title: 'Upload Favicon',
19004      containerType: 'favicon',
19005      maxWidth: 64,  // Favicons are typically small
19006      maxHeight: 64,
19007      quality: 0.9
19008    };
19009    
19010    // Call the ImageUploader
19011    window.ImageUploader.open(options, function(imageResult, imageInfo) {
19012      console.log('Favicon upload callback received:', imageInfo);
19013      
19014      // Get the favicon URL input field
19015      let faviconInput = document.getElementById('faviconUrl');
19016      console.warn('=== UPLOAD CALLBACK DEBUG ===');
19017      console.warn('Favicon input element found:', !!faviconInput);
19018      console.warn('Favicon input element type:', faviconInput ? faviconInput.type : 'N/A');
19019      console.warn('Favicon input element id:', faviconInput ? faviconInput.id : 'N/A');
19020      
19021      // If element not found, wait a bit and try again (metadata editor might not be open yet)
19022      if (!faviconInput) {
19023        console.warn('Favicon input not found, waiting 100ms and trying again...');
19024        setTimeout(() => {
19025          faviconInput = document.getElementById('faviconUrl');
19026          console.warn('Retry - Favicon input element found:', !!faviconInput);
19027          if (faviconInput) {
19028            console.warn('Retry - Setting favicon input value to:', imageInfo.url);
19029            faviconInput.value = imageInfo.url;
19030            console.warn('Retry - Favicon input value after setting:', faviconInput.value);
19031          } else {
19032            console.error('Retry - Favicon input field still not found');
19033          }
19034        }, 100);
19035        return;
19036      }
19037      
19038      // Set the uploaded image URL
19039      if (imageInfo && imageInfo.type === 'r2' && imageInfo.url) {
19040        // R2 storage - use the URL
19041        console.warn('Setting favicon input value to:', imageInfo.url);
19042        faviconInput.value = imageInfo.url;
19043        console.warn('Favicon input value after setting:', faviconInput.value);
19044        
19045        // Also store in global variable as backup
19046        window.uploadedFaviconUrl = imageInfo.url;
19047        console.warn('Stored favicon URL in global variable:', window.uploadedFaviconUrl);
19048        
19049        console.log('Favicon URL set to:', imageInfo.url);
19050        
19051        // Update button text
19052        const btnText = document.getElementById('uploadFaviconBtnText');
19053        if (btnText) {
19054          btnText.textContent = 'Change Favicon';
19055        }
19056        
19057        // Show preview
19058        showFaviconPreview(imageInfo.url);
19059      } else if (imageResult) {
19060        // Legacy base64 storage
19061        faviconInput.value = imageResult;
19062        console.log('Favicon set to base64 data');
19063        
19064        // Update button text
19065        const btnText = document.getElementById('uploadFaviconBtnText');
19066        if (btnText) {
19067          btnText.textContent = 'Change Favicon';
19068        }
19069        
19070        // Show preview
19071        showFaviconPreview(imageResult);
19072      } else {
19073        console.error('No image data received from uploader');
19074        alert('Upload failed. Please try again.');
19075        return;
19076      }
19077      
19078      // Show success message
19079      console.log('Favicon uploaded successfully');
19080    });
19081    
19082  } catch (error) {
19083    console.error('Error opening favicon uploader:', error);
19084    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
19085  }
19086}
19087
19088// Function to show a preview of the uploaded favicon
19089function showFaviconPreview(imageUrl) {
19090  const container = document.getElementById('faviconPreviewContainer');
19091  if (!container) {
19092    console.error('Favicon preview container not found');
19093    return;
19094  }
19095  
19096  container.innerHTML = 
19097    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
19098      '<img src="' + imageUrl + '" alt="Favicon Preview" style="width: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;">' +
19099      '<div style="flex: 1;">' +
19100        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current Favicon</div>' +
19101        '<div style="font-size: 12px; color: #666; margin-top: 2px;">Preview (32x32px)</div>' +
19102      '</div>' +
19103      '<button type="button" onclick="removeFavicon()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
19104        'Remove' +
19105      '</button>' +
19106    '</div>';
19107}
19108
19109// Function to remove the favicon
19110function removeFavicon() {
19111  const faviconInput = document.getElementById('faviconUrl');
19112  if (faviconInput) {
19113    faviconInput.value = '';
19114  }
19115  
19116  const container = document.getElementById('faviconPreviewContainer');
19117  if (container) {
19118    container.innerHTML = '';
19119  }
19120  
19121  const btnText = document.getElementById('uploadFaviconBtnText');
19122  if (btnText) {
19123    btnText.textContent = 'Upload Favicon';
19124  }
19125  
19126  console.log('Favicon removed');
19127}
19128
19129function uploadSiteOgImage() {
19130  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
19131    alert('Image uploader not available. Refresh the page and try again.');
19132    return;
19133  }
19134  try {
19135    window.ImageUploader.open({
19136      title: 'Upload social preview image',
19137      containerType: 'page',
19138      quality: 0.92
19139    }, function(imageResult, imageInfo) {
19140      const inp = document.getElementById('ogImageUrl');
19141      const setUrl = function(url) {
19142        if (inp) inp.value = url;
19143        const btnText = document.getElementById('uploadOgImageBtnText');
19144        if (btnText) btnText.textContent = 'Change image';
19145        showOgImagePreview(url);
19146        window.siteMetadata = window.siteMetadata || {};
19147        window.siteMetadata.ogImageUrl = url;
19148      };
19149      if (imageInfo && imageInfo.url) {
19150        setUrl(imageInfo.url);
19151      } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) {
19152        setUrl(imageResult);
19153      } else {
19154        alert('Upload failed. Please try again.');
19155      }
19156    });
19157  } catch (error) {
19158    console.error('Error opening OG image uploader:', error);
19159    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
19160  }
19161}
19162
19163function showOgImagePreview(imageUrl) {
19164  const container = document.getElementById('ogImagePreviewContainer');
19165  if (!container) return;
19166  container.innerHTML =
19167    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
19168      '<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;">' +
19169      '<div style="flex: 1;">' +
19170        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current social preview image</div>' +
19171        '<div style="font-size: 12px; color: #666; margin-top: 2px;">This image overrides the first page/gallery image for social previews</div>' +
19172      '</div>' +
19173      '<button type="button" onclick="removeOgImage()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
19174        'Remove' +
19175      '</button>' +
19176    '</div>';
19177}
19178
19179function removeOgImage() {
19180  const inp = document.getElementById('ogImageUrl');
19181  if (inp) inp.value = '';
19182  if (window.siteMetadata) window.siteMetadata.ogImageUrl = '';
19183  const container = document.getElementById('ogImagePreviewContainer');
19184  if (container) container.innerHTML = '';
19185  const btnText = document.getElementById('uploadOgImageBtnText');
19186  if (btnText) btnText.textContent = 'Upload image';
19187}
19188</script>
19188
19189
19190  <!-- Import Classic Form -->
19191<div class="import-classic-form" id="importClassicForm" style="display: none;" data-nosnippet>
19192  <h3>Import Galleries from JSON or Classic GUID</h3>
19193  <p>Paste the full site JSON into the text area below, <strong>OR</strong> enter the GUID of a classic gallery collection.</p>
19194  
19195  <div class="form-group">
19196    <label for="pastedJson">Pasted Site JSON:</label>
19197    <textarea id="pastedJson" rows="6" placeholder="Paste the entire site JSON here..."></textarea>
19198  </div>
19199
19200  <div class="import-separator">OR</div>
19201
19202  <div class="form-group">
19203    <label for="classicGuid">Classic GUID or Import ID:</label>
19204    <input type="text" id="classicGuid" placeholder="Enter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)">
19205    <small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.</small>
19206  </div>
19207
19208    <div class="form-group">
19209      <label>
19210        <input type="checkbox" id="replaceMenuCheckbox" onchange="document.getElementById('importParent').disabled = this.checked; document.getElementById('createSubmenu').disabled = this.checked;">
19211        Replace existing menu structure
19212      </label>
19213      <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>
19214  </div>
19215
19216  <div class="form-group">
19217    <label for="importParent">Import under:</label>
19218    <select id="importParent">
19219      <option value="">Top Level (No Parent)</option>
19220    </select>
19221  </div>
19222
19223  <div class="form-group">
19224    <label>
19225      <input type="checkbox" id="createSubmenu" checked>
19226      Create submenu for all imported galleries
19227    </label>
19228  </div>
19229
19230  <div class="form-group" id="submenuTitleContainer">
19231    <label for="submenuTitle">Submenu title:</label>
19232    <input type="text" id="submenuTitle" placeholder="e.g., Classic Galleries">
19233  </div>
19234
19235  <div id="importStatus" class="import-status" style="display: none;">
19236    <div class="import-progress"><div class="progress-bar"></div></div>
19237    <div class="status-message">Processing...</div>
19238  </div>
19239
19240  <div class="form-actions">
19241    <button class="btn btn-primary" onclick="importClassicGalleries()">Import from Paste/GUID</button>
19242    <button class="btn" onclick="downloadCurrentSiteJSON()">Download Current Site JSON</button>
19243    <button class="btn" onclick="toggleImportClassicForm()">Cancel</button>
19244  </div>
19245</div>
19246  <div class="tree-container">
19247    <div id="galleryTree" data-help-id="menu-tree"></div>
19248  </div>
19249
19250    <!-- Sidebar Elements Container will be inserted here by the sidebar-manager.js -->
19251  <div class="sidebar-elements-container"></div>
19252</div>
19253<div class="gallery-container" id="hydra-main-content" role="main" tabindex="-1">
19254  <iframe class="gallery-iframe" id="galleryFrame" title="ATHLETES | People - Adena Stevens Photography"></iframe>
19255</div>
19256</div>
19257
19258<!-- Cookie Notice (compliance mode — only when Display Cookie Notice is enabled) -->
19259
19260
19261<script>
19262// Reveal content once layout is applied, CSS is loaded, AND gallery is initialized
19263(function() {
19264  let hasRevealed = false; // Ensure we only reveal once per page load
19265  
19266  function revealContent() {
19267    if (hasRevealed) return; // Prevent multiple calls
19268    hasRevealed = true;
19269    document.documentElement.classList.add('hydra-ready');
19270    document.body.classList.add('hydra-ready');
19271    console.log('✅ Content revealed - hydra-ready class added');
19272  }
19273  
19274  // Check if this page has a gallery container
19275  function hasGalleryContainer() {
19276    return !!document.getElementById('neon-gallery-container') || 
19277           !!document.querySelector('.gallery-wrapper') ||
19278           !!document.querySelector('.masonry-container');
19279  }
19280  
19281  // Check if gallery is already initialized
19282  function isGalleryInitialized() {
19283    return window.neonGalleryState?.initialized || 
19284           window.neonGalleryState?.domInitialized ||
19285           (window.neonGalleryUtils && window.neonGalleryUtils.isInitialized && window.neonGalleryUtils.isInitialized());
19286  }
19287  
19288  // Main reveal logic
19289  function attemptReveal() {
19290    const hasGallery = hasGalleryContainer();
19291    const galleryInitialized = isGalleryInitialized();
19292    
19293    // If no gallery, reveal immediately after CSS loads
19294    if (!hasGallery) {
19295      if (document.readyState === 'complete') {
19296        revealContent();
19297      } else {
19298        window.addEventListener('load', revealContent, { once: true });
19299      }
19300      return;
19301    }
19302    
19303    // If gallery exists and is initialized, reveal
19304    if (hasGallery && galleryInitialized) {
19305      revealContent();
19306      return;
19307    }
19308    
19309    // If gallery exists but not initialized, wait for initialization
19310    if (hasGallery && !galleryInitialized) {
19311      console.log('⏳ Waiting for gallery initialization before revealing content...');
19312      
19313      // Listen for gallery-loaded event
19314      window.addEventListener('gallery-loaded', function() {
19315        console.log('✅ Gallery-loaded event received');
19316        setTimeout(revealContent, 100); // Small delay to ensure DOM is updated
19317      }, { once: true });
19318      
19319      // Listen for page-loaded event
19320      window.addEventListener('page-loaded', function() {
19321        console.log('✅ Page-loaded event received');
19322        setTimeout(revealContent, 100);
19323      }, { once: true });
19324      
19325      // Fallback: Poll for gallery initialization (max 10 seconds)
19326      let pollAttempts = 0;
19327      const maxPollAttempts = 100; // 10 seconds (100 * 100ms)
19328      const pollInterval = setInterval(function() {
19329        pollAttempts++;
19330        if (isGalleryInitialized()) {
19331          console.log('✅ Gallery initialized detected via polling');
19332          clearInterval(pollInterval);
19333          revealContent();
19334        } else if (pollAttempts >= maxPollAttempts) {
19335          console.warn('⚠️ Gallery initialization timeout - revealing content anyway');
19336          clearInterval(pollInterval);
19337          revealContent(); // Reveal anyway to prevent blank screen
19338        }
19339      }, 100);
19340      
19341      // Safety fallback: Always reveal after 8 seconds max (prevents permanent blank screen)
19342      setTimeout(function() {
19343        if (!hasRevealed) {
19344          console.warn('⚠️ Safety timeout reached - revealing content to prevent blank screen');
19345          revealContent();
19346        }
19347      }, 8000);
19348    }
19349  }
19350  
19351  // Start the reveal process
19352  if (document.readyState === 'loading') {
19353    document.addEventListener('DOMContentLoaded', function() {
19354      // Wait a bit for scripts to initialize
19355      setTimeout(attemptReveal, 100);
19356    });
19357  } else if (document.readyState === 'interactive' || document.readyState === 'complete') {
19358    // DOM already ready, wait a bit for scripts
19359    setTimeout(attemptReveal, 100);
19360  } else {
19361    // Fallback
19362    window.addEventListener('load', attemptReveal, { once: true });
19363  }
19364})();
19365</script>
19365
19366
19367<script>
19368// ============================================
19369// LANGUAGE TRANSLATION SYSTEM
19370// ============================================
19371
19372(function() {
19373  // CRITICAL: Only initialize translation system if a languages element exists
19374  // This ensures sites without languages element have ZERO overhead
19375  const checkForLanguagesElement = function() {
19376    // Wait for SidebarManager to be initialized
19377    if (!window.SidebarManager || !window.SidebarManager.elements) {
19378      return false;
19379    }
19380    
19381    // Check if any languages element exists
19382    const hasLanguagesElement = window.SidebarManager.elements.some(el => el.type === 'languages');
19383    
19384    if (!hasLanguagesElement) {
19385      console.warn('No languages element found - translation system disabled');
19386      return false;
19387    }
19388    
19389    return true;
19390  };
19391  
19392  // Check for languages element, retry if SidebarManager not ready yet
19393  let initAttempts = 0;
19394  const maxAttempts = 10;
19395  
19396  function tryInitialize() {
19397    if (checkForLanguagesElement()) {
19398      console.warn('✅ Languages element found - initializing translation system');
19399      initializeTranslationSystem();
19400    } else {
19401      initAttempts++;
19402      if (initAttempts < maxAttempts) {
19403        // Retry in 500ms (total max wait: 5 seconds)
19404        setTimeout(tryInitialize, 500);
19405      } else {
19406        console.warn('Translation system not needed for this site - no languages element found after 5 seconds');
19407      }
19408    }
19409  }
19410  
19411  // Start checking after initial delay
19412  setTimeout(tryInitialize, 500);
19413  
19414  function initializeTranslationSystem() {
19415    console.warn('✅ Translation system initialized successfully');
19416    
19417    let currentLanguage = localStorage.getItem('hydra_language') || 'en';
19418    const originalTexts = new Map();
19419    
19420    // Expose test function for debugging
19421    window.testTranslation = function(langCode) {
19422      console.warn('🧪 Manual translation test triggered for:', langCode);
19423      console.warn('🔍 Dispatching test event...');
19424      window.dispatchEvent(new CustomEvent('hydra:language-change', {
19425        detail: { code: langCode || 'es', name: 'Test' }
19426      }));
19427      console.warn('✅ Test event dispatched - check if listener caught it above');
19428    };
19429    
19430    console.warn('✅ Event listener for "hydra:language-change" is now active');
19431    
19432    // Watch for edit mode changes and restore default language
19433    const editModeObserver = new MutationObserver(function(mutations) {
19434    mutations.forEach(function(mutation) {
19435      if (mutation.attributeName === 'class') {
19436        const isEditMode = document.body.classList.contains('edit-mode-active');
19437        if (isEditMode) {
19438          console.warn('Edit mode activated - restoring default language');
19439          // Restore original text
19440          restoreOriginalText();
19441          
19442          // Update language buttons to show default language
19443          const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19444          const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19445          
19446          document.querySelectorAll('.language-button-live').forEach(btn => {
19447            if (btn.getAttribute('data-lang-code') === defaultLang) {
19448              btn.style.background = 'var(--menu-active-color, #4682B4)';
19449            } else {
19450              btn.style.background = 'transparent';
19451            }
19452          });
19453        }
19454      }
19455    });
19456  });
19457  
19458  // Start observing body for class changes
19459  if (document.body) {
19460    editModeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
19461  }
19462  
19463  // Watch for dynamically loaded content (galleries, pages, etc.) and translate it
19464  const contentObserver = new MutationObserver(function(mutations) {
19465    // Only process if we're not in default language and not in edit mode
19466    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19467    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19468    const isEditMode = document.body.classList.contains('edit-mode-active');
19469    
19470    if (currentLanguage === defaultLang || isEditMode) {
19471      return; // Don't translate if in default language or edit mode
19472    }
19473    
19474    // Check if new text content was added
19475    let hasNewTextContent = false;
19476    mutations.forEach(function(mutation) {
19477      if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
19478        mutation.addedNodes.forEach(function(node) {
19479          if (node.nodeType === Node.ELEMENT_NODE) {
19480            // Check if the added element contains text we should translate
19481            const textElements = node.querySelectorAll('p, h1, h2, h3, .page-text');
19482            if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) {
19483              hasNewTextContent = true;
19484            }
19485          }
19486        });
19487      }
19488    });
19489    
19490    if (hasNewTextContent) {
19491      console.warn('New content detected - translating to:', currentLanguage);
19492      // Debounce to avoid translating multiple times rapidly
19493      clearTimeout(window._translationDebounce);
19494      window._translationDebounce = setTimeout(function() {
19495        translatePage(currentLanguage, defaultLang);
19496      }, 300);
19497    }
19498  });
19499  
19500  // Observe the gallery container and page content areas for new content
19501  const galleryContainer = document.querySelector('.gallery-container');
19502  if (galleryContainer) {
19503    contentObserver.observe(galleryContainer, { 
19504      childList: true, 
19505      subtree: true 
19506    });
19507  }
19508  
19509  // Also observe the main content area
19510  const mainContent = document.querySelector('.container');
19511  if (mainContent) {
19512    contentObserver.observe(mainContent, { 
19513      childList: true, 
19514      subtree: true 
19515    });
19516  }
19517  
19518  // Listen for language change events
19519  window.addEventListener('hydra:language-change', async function(e) {
19520    const { code, name } = e.detail;
19521    console.warn('🌐 Language changed to:', code, name);
19522    
19523    // CRITICAL: Ignore language changes in edit mode
19524    if (document.body.classList.contains('edit-mode-active')) {
19525      console.warn('⚠️ Edit mode active - ignoring language change');
19526      // Reset to default language
19527      const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19528      const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19529      
19530      // Update button states to show default language
19531      document.querySelectorAll('.language-button-live').forEach(btn => {
19532        if (btn.getAttribute('data-lang-code') === defaultLang) {
19533          btn.style.background = 'var(--menu-active-color, #4682B4)';
19534        } else {
19535          btn.style.background = 'transparent';
19536        }
19537      });
19538      
19539      // Show message to user
19540      console.warn('Language switching is disabled in edit mode');
19541      return;
19542    }
19543    
19544    // Update all language buttons
19545    document.querySelectorAll('.language-button-live').forEach(btn => {
19546      if (btn.getAttribute('data-lang-code') === code) {
19547        btn.style.background = 'var(--menu-active-color, #4682B4)';
19548      } else {
19549        btn.style.background = 'transparent';
19550      }
19551    });
19552    
19553    // Save preference
19554    localStorage.setItem('hydra_language', code);
19555    currentLanguage = code;
19556    
19557    // Get default language from languages element
19558    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19559    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19560    
19561    if (code === defaultLang) {
19562      // Restore original language
19563      restoreOriginalText();
19564    } else {
19565      // Translate page
19566      await translatePage(code, defaultLang);
19567    }
19568  });
19569  
19570  async function translatePage(targetLang, sourceLang) {
19571    console.warn(`🔄 Translating page from ${sourceLang} to ${targetLang}`);
19572    
19573    // Elements to translate
19574    const selectors = [
19575      '.sidebar li a:not(.edit-mode-active .sidebar li a)', // Menu items (not in edit mode)
19576      '.page-text:not(.edit-mode-active .page-text)', // Page content (not in edit mode)
19577      'h1:not(.edit-mode-active h1)', 
19578      'h2:not(.edit-mode-active h2)', 
19579      'h3:not(.edit-mode-active h3)',
19580      'p:not(.edit-mode-active p)'
19581    ];
19582    
19583    const elements = document.querySelectorAll(selectors.join(', '));
19584    
19585    for (const element of elements) {
19586      // Skip if already translated to this language
19587      if (element.dataset.translatedTo === targetLang) continue;
19588      
19589      // Skip if element is empty
19590      const text = element.textContent.trim();
19591      if (!text || text.length === 0) continue;
19592      
19593      // Store original text
19594      if (!originalTexts.has(element)) {
19595        originalTexts.set(element, text);
19596      }
19597      
19598      // Check translation cache
19599      const cacheKey = `translate_${text}_${targetLang}`;
19600      let translated = getFromCache(cacheKey);
19601      
19602      if (translated) {
19603        element.textContent = translated;
19604        element.dataset.translatedTo = targetLang;
19605      } else {
19606        // Translate
19607        try {
19608          translated = await translate(text, sourceLang, targetLang);
19609          if (translated && translated !== text) {
19610            element.textContent = translated;
19611            element.dataset.translatedTo = targetLang;
19612            saveToCache(cacheKey, translated);
19613          }
19614        } catch (error) {
19615          console.error('Translation error:', error);
19616        }
19617      }
19618    }
19619  }
19620  
19621  function restoreOriginalText() {
19622    originalTexts.forEach((originalText, element) => {
19623      if (element && element.isConnected) {
19624        element.textContent = originalText;
19625        delete element.dataset.translatedTo;
19626      }
19627    });
19628  }
19629  
19630  async function translate(text, sourceLang, targetLang) {
19631    if (!text || text.trim().length === 0) return text;
19632    
19633    // Try Browser Translation API (Free, Chrome 130+)
19634    if ('translation' in self && 'createTranslator' in self.translation) {
19635      try {
19636        console.warn('🔄 Using Browser Translation API');
19637        const translator = await self.translation.createTranslator({
19638          sourceLanguage: sourceLang,
19639          targetLanguage: targetLang
19640        });
19641        const result = await translator.translate(text);
19642        console.warn('✅ Translation successful via Browser API');
19643        return result;
19644      } catch (error) {
19645        console.warn('❌ Browser translation failed:', error);
19646      }
19647    } else {
19648      console.warn('⚠️ Browser Translation API not available');
19649      console.warn('   To enable in Chrome: chrome://flags/#translation-api');
19650      console.warn('   OR add Google Translate API key to use paid translation');
19651    }
19652    
19653    // TODO: Add Google Translate API fallback here
19654    // For now, return original text
19655    console.warn('⚠️ No translation provider configured - returning original text');
19656    return text;
19657  }
19658  
19659  // Cache management
19660  function getFromCache(key) {
19661    try {
19662      const item = localStorage.getItem(`hydra_${key}`);
19663      if (!item) return null;
19664      
19665      const { value, expiry } = JSON.parse(item);
19666      if (expiry && Date.now() > expiry) {
19667        localStorage.removeItem(`hydra_${key}`);
19668        return null;
19669      }
19670      return value;
19671    } catch {
19672      return null;
19673    }
19674  }
19675  
19676  function saveToCache(key, value) {
19677    try {
19678      const item = {
19679        value,
19680        expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
19681      };
19682      localStorage.setItem(`hydra_${key}`, JSON.stringify(item));
19683    } catch (error) {
19684      console.warn('Cache save failed:', error);
19685    }
19686  }
19687  
19688  } // End of initializeTranslationSystem
19689})();
19690</script>
19690
19691
19692
19693
19694<script>
19695window.HYDRA_HELP_ENABLED = true;
19696function loadHelpSystemScript() {
19697  if (window.HelpSystem && window.HelpSystem.openHub) return Promise.resolve();
19698  return new Promise(function(resolve, reject) {
19699    var existing = document.querySelector('script[data-hydra-help-system]');
19700    if (existing) {
19701      existing.addEventListener('load', function() { resolve(); });
19702      existing.addEventListener('error', reject);
19703      return;
19704    }
19705    var s = document.createElement('script');
19706    s.src = '/api/help-system.js?v=686c85345749';
19707    s.async = true;
19708    s.setAttribute('data-hydra-help-system', '1');
19709    s.onload = function() { resolve(); };
19710    s.onerror = reject;
19711    document.head.appendChild(s);
19712  });
19713}
19714function openHydraHelp(opts) {
19715  opts = opts || {};
19716  if (!window.HYDRA_HELP_ENABLED) return;
19717  var token = window.didToken || localStorage.getItem('hydra_auth_token');
19718  var isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
19719  if (!token || !isAdmin) return;
19720  var p = loadHelpSystemScript();
19721  Promise.resolve(p).then(function() {
19722    if (window.HelpSystem && window.HelpSystem.init) return window.HelpSystem.init();
19723  }).then(function() {
19724    if (window.HelpSystem && window.HelpSystem.openHub) {
19725      window.HelpSystem.openHub(opts.initialTab ? { initialTab: opts.initialTab } : undefined);
19726    }
19727  }).catch(function(err) {
19728    console.error('HelpSystem init failed:', err);
19729  });
19730}
19731window.openHydraHelp = openHydraHelp;
19732</script>
19732
19733
19734<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>
19734
19735</body>
19736</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.