PageSourceSearch

https://davidcarsonphotos.com/life

html davidcarsonphotos.com collected 2026-10-04 05:56:30 UTC 953,323 bytes, 19,826 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="52octf5t">
7  
8  <!-- Primary Meta Tags -->
9  <title>Life | Simple Image Page</title>
10  <meta name="description" content="Scroll Horizontally-->">
11  <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
12  <meta name="author" content="davidcarsonphotos.com">
13  <meta name="language" content="en">
14  
15  <!-- Canonical URL -->
16  <link rel="canonical" href="https://davidcarsonphotos.com/life">
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://davidcarsonphotos.com/life">
27  <meta property="og:title" content="Life | Simple Image Page">
28  <meta property="og:description" content="Scroll Horizontally-->">
29  <meta property="og:site_name" content="davidcarsonphotos.com">
30  <meta property="og:locale" content="en_US">
31  <meta property="og:image" content="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg">
32  <meta property="og:image:secure_url" content="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.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="Life | Simple Image Page">
37  
38  <!-- Twitter / X Card -->
39  <meta name="twitter:card" content="summary_large_image">
40  <meta name="twitter:site" content="@davidcarsonphotos.com">
41  <meta name="twitter:url" content="https://davidcarsonphotos.com/life">
42  <meta name="twitter:title" content="Life | Simple Image Page">
43  <meta name="twitter:description" content="Scroll Horizontally-->">
44  <meta name="twitter:image" content="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg">
45  <meta name="twitter:image:alt" content="Life | Simple Image Page">
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="Life | Simple Image Page">
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": "Life | Simple Image Page",
75  "description": "Scroll Horizontally--\\u003E",
76  "url": "https://davidcarsonphotos.com/life",
77  "datePublished": "2026-10-04T05:56:30.101Z",
78  "dateModified": "2026-10-04T05:56:30.101Z",
79  "inLanguage": "en-US",
80  "isPartOf": {
81    "@type": "WebSite",
82    "@id": "https://davidcarsonphotos.com/#website",
83    "name": "davidcarsonphotos.com",
84    "url": "https://davidcarsonphotos.com/",
85    "description": "Scroll Horizontally--\\u003E",
86    "publisher": {
87      "@type": "Organization",
88      "name": "davidcarsonphotos.com",
89      "url": "https://davidcarsonphotos.com/"
90    }
91  },
92  "breadcrumb": {
93    "@type": "BreadcrumbList",
94    "itemListElement": [
95      {
96        "@type": "ListItem",
97        "position": 1,
98        "name": "Home",
99        "item": "https://davidcarsonphotos.com/"
100      },
101      {
102        "@type": "ListItem",
103        "position": 2,
104        "name": "Life | Simple Image Page",
105        "item": "https://davidcarsonphotos.com/life"
106      }
107    ]
108  },
109  "associatedMedia": [
110    {
111      "@type": "ImageObject",
112      "name": "Supermoon St. Louis, Arch",
113      "description": "The supermoon rises over the Arch in St. Louis as seen from the Compton Hill Water Tower on Sunday, Nov. 13, 2016. The supermoon will peak on early Monday morning shortly before sunrise, it will be the nearest supermoon in almost 70 years and will be the largest until Nov. 25, 2034. A supermoon happens when a full moon makes its closest pass to Earth appearing up to 14 percent bigger and 30 percent brighter in the sky. Viewing of the moon should also be good on Monday after the sun sets and the moon rises out of the east.\\nPhoto by David Carson, [email protected]",
114      "caption": "Supermoon St. Louis, Arch",
115      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg"
116    },
117    {
118      "@type": "ImageObject",
119      "name": "Prayer Shot",
120      "description": "",
121      "caption": "Prayer Shot",
122      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/34-60-08-CarsD-04a.jpg"
123    },
124    {
125      "@type": "ImageObject",
126      "name": "Memorial Day scouts",
127      "description": "Cub Scout Nick Kollar, 8, from Eureka, places flags in front of head stones at Jefferson Barracks National Cemetery. Kollar was one of more than 5,000 local scouts who showed up to help place flags at the more than 139,000 head stones at the cemetery. This was the 61st annual Memorial Day \\\"Good Turn\\\" event that was sponsored by the Gravois Trail District of the Greater St. Louis Area Council, Boy Scouts of America.",
128      "caption": "Memorial Day scouts",
129      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Scout-Flags-1a-copy.jpg"
130    },
131    {
132      "@type": "ImageObject",
133      "name": "Flipping out",
134      "description": "Dwight Jones, 13, tumbles over an old mattress he and his friends were playing with in south St. Louis. Jones says he likes the energy of tumbling and learned his skills from his brother and lots of practice.\\nDavid Carson | Post-Dispatch",
135      "caption": "Flipping out",
136      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/57-CD-DC-Tumble-Kids1.jpg"
137    },
138    {
139      "@type": "ImageObject",
140      "name": "Baby Robbins",
141      "description": "A Robin comes back to the nest to feed it's brood in the Compton Heights neighborhood of St. Louis. The babies hatched over the weekend and the parents have been busy feeding the three chicks.",
142      "caption": "Baby Robbins",
143      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/a04-CD-DC-Robins-1.jpg"
144    },
145    {
146      "@type": "ImageObject",
147      "name": "Dock",
148      "description": "Friends Patrick Schertzer, left, Brett Ware, center, and Curtis Vitale take flying leaps into a man made lake at the New Town development in St. Charles County. Tempatures are expected to remain in the upper 80's and lower 90's for the through the weekend.",
149      "caption": "Dock",
150      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/54-CD-DC-New-Town-Kids.jpg"
151    },
152    {
153      "@type": "ImageObject",
154      "name": "Frosty morning",
155      "description": "Ice crystals form on the windshield of a car in south St. Louis city on Tuesday. Frosty weather is expected again today as temperatures will struggle to get above freezing.",
156      "caption": "Frosty morning",
157      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Frosty-December-2.jpg"
158    },
159    {
160      "@type": "ImageObject",
161      "name": "Santa hatch",
162      "description": "After greeting children from the roof of the Hard Rock Cafe Santa Claus, played by Dana Wayne Morris of St. Louis, squeezes down a hatch and into the restaurant to hear the children's Christmas wishes.",
163      "caption": "Santa hatch",
164      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---022.jpg"
165    },
166    {
167      "@type": "ImageObject",
168      "name": "070413 DC Fireworks 2A",
169      "description": "Fair St. Louis fireworks light up the St. Louis skyline on Thursday, July 4, 2013. This picture was shot from the roof of the Pierre Chouteau building in 4400 Lindell Blvd.",
170      "caption": "Fair St. Louis fireworks light up the St. Louis skyline on Thursday, July 4, 2013. This picture was shot from the roof of the Pierre Chouteau building in 4400 Lindell Blvd.",
171      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/070413-DC-Fireworks-2A.jpg"
172    },
173    {
174      "@type": "ImageObject",
175      "name": "Memorial Day",
176      "description": "CIVIL WAR RE-ENACTOR Civil War re-enactor Peggie Avery plays the role of a Union Army widow during Memorial Day services at Jefferson Barracks National Cemetery.",
177      "caption": "Memorial Day",
178      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---018.jpg"
179    },
180    {
181      "@type": "ImageObject",
182      "name": "00011184-GNN-004",
183      "description": "ABES Abraham Lincoln impersonators gather at the enterance to the dedication ceremonies for the Abraham Lincoln Presidential Museum in Springfield, Ill.",
184      "caption": "00011184-GNN-004",
185      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/64-abes-.jpg"
186    },
187    {
188      "@type": "ImageObject",
189      "name": "Dance BWb",
190      "description": "",
191      "caption": "Dance BWb",
192      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Dance-BWb.jpg"
193    },
194    {
195      "@type": "ImageObject",
196      "name": "fashion show",
197      "description": "",
198      "caption": "fashion show",
199      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/fashion-show.jpg"
200    },
201    {
202      "@type": "ImageObject",
203      "name": "Big Surf waves",
204      "description": "",
205      "caption": "Big Surf waves",
206      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Big-Surf-waves.jpg"
207    },
208    {
209      "@type": "ImageObject",
210      "name": "Rainbow hop",
211      "description": "Antonie Baker, 8, leaps through the spray of a cooling station that was set up in front of the Gamble Community Center in St. Louis. An excessive heat warning was issued by the National Weather Service for the St. Louis area.",
212      "caption": "Rainbow hop",
213      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-STL-Heat.jpg"
214    },
215    {
216      "@type": "ImageObject",
217      "name": "Children of the corn",
218      "description": "Bethalto West Elementary School fourth grader Chris Williams, 9, is buried in the \\\"Corn Crib\\\" play area at the Great Godfrey Maze during a school field trip.",
219      "caption": "Children of the corn",
220      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-CHILDREN-of-Corn.jpg"
221    },
222    {
223      "@type": "ImageObject",
224      "name": "Graffiti eye",
225      "description": "A graffiti artist from Kansas City works on putting the finishing touches on his work on the flood wall south of the Arch on Wharf Street in St. Louis. About 140 graffiti artist from around the country came to St. Louis to paint the flood wall.",
226      "caption": "Graffiti eye",
227      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/062313-DC-Open-S-20041378.jpg"
228    },
229    {
230      "@type": "ImageObject",
231      "name": "131203 DC Santa",
232      "description": "Danny Wall, from Fenton, cruises down Interstate 70 West through downtown St. Louis on his way to deliver a present to a three-year-old boy in Granite City on Tuesday, Dec. 3, 2013.  \\\"I love seeing the expression of joy on people's faces as I drive by\\\" said Wall who started playing the role of Santa Claus eight years ago when his first grandchildren were born.  At first he visited just the homes of friends and family but he has visited 
232with groups Boy Scouts, Girl Scouts, and children at Rankin Jordan.  He says playing Santa Claus helps him get the Christmas spirit and relieve stress.  Wall is a riding a 2008 Honda Goldwing Trike.\\nPhoto By David Carson, [email protected]",
233      "caption": "Danny Wall, from Fenton, cruises down Interstate 70 West through downtown St. Louis on his way to deliver a present to a three-year-old boy in Granite City on Tuesday, Dec. 3, 2013.  \\\"I love seeing the expression of joy on people's faces as I drive by\\\" said Wall who started playing the role of Santa Claus eight years ago when his first grandchildren were born.  At first he visited just the homes of friends and family but he has visited with groups Boy Scouts, Girl Scouts, and children at Rankin Jordan.  He says playing Santa Claus helps him get the Christmas spirit and relieve stress.  Wall is a riding a 2008 Honda Goldwing Trike.\\nPhoto By David Carson, [email protected]",
234      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/131203-DC-Santa.jpg"
235    },
236    {
237      "@type": "ImageObject",
238      "name": "Burlesque",
239      "description": "Sturdy Gurlesque performs one of her burlesque routines that feature fire as part of the Albino Alley Cat Revue on stage at Rue 13 on Washington Ave in St. Louis.",
240      "caption": "Burlesque",
241      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/AE-DC-Burlesque-6.jpg"
242    },
243    {
244      "@type": "ImageObject",
245      "name": "Hispanics in St. Ann",
246      "description": "Martin McDrano (center) and Alberta Mejia dance to the music at the Acapulco Restaurant and Lounge in St. Ann. Emestina Zegar, owner of the Acapulco, thinks fewer people are frequenting her business because they fear being stopped by the St. Ann police.",
247      "caption": "Hispanics in St. Ann",
248      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-St-Ann-Dancing.jpg"
249    },
250    {
251      "@type": "ImageObject",
252      "name": "Walking dead",
253      "description": "Mathew Hebert (left), from Midnight Studios out of Mesa, Arizona, stops to talk with Janice Smith, from Ballwin, as he walks the floor of the 17th annual Halloween and Attractions Show at America's Center in St. Louis.",
254      "caption": "Walking dead",
255      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/2012-14-Haunt-1-update.jpg"
256    },
257    {
258      "@type": "ImageObject",
259      "name": "Swim lessons",
260      "description": "Mekkah Richards, 15, helps her brother Abel Richards, 9, put on his swim cap before the start of event featuring Olympic gold medalist swimmer Cullen Jones at the Mathews Dickey Boys and Girls Club in St. Louis. The event, Make A Splash, put on by the USA Swimming Foundation, is designed to raise awareness about the importance of learning to swim. The program has helped provide more than 845,000 kids with swimming lessons.",
261      "caption": "Swim lessons",
262      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/2012-10-Mathews-Dickey-Swim.jpg"
263    },
264    {
265      "@type": "ImageObject",
266      "name": "130911 DC OFallo 20215142",
267      "description": "Jeanne Tallman escorts her granddaughter Whitney Weeks, 3, as she hands a coloring she did to O'Fallon police officer Mike Plum after a ceremony honoring first responders in O'Fallon, Mo. on Wednesday, Sept. 11, 2013.  \\\"She wanted him (Plum) to know she was thinking about them\\\" said Tallman who said the coloring paid tribute to the fire fighters and police officers who gave their lives on September 11.  The City of O'Fallon, Mo. hosted a Patriot Day Ceremony, a Tribute to First Responders by a piece of the twisted steel from the World Trade Center at the Municipal Centre.\\nPhoto By David Carson, [email protected]",
268      "caption": "Jeanne Tallman escorts her granddaughter Whitney Weeks, 3, as she hands a coloring she did to O'Fallon police officer Mike Plum after a ceremony honoring first responders in O'Fallon, Mo. on Wednesday, Sept. 11, 2013.  \\\"She wanted him (Plum) to know she was thinking about them\\\" said Tallman who said the coloring paid tribute to the fire fighters and police officers who gave their lives on September 11.  The City of O'Fallon, Mo. hosted a Patriot Day Ceremony, a Tribute to First Responders by a piece of the twisted steel from the World Trade Center at the Municipal Centre.\\nPhoto By David Carson, [email protected]",
269      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/130911-DC-OFallo-20215142.jpg"
270    },
271    {
272      "@type": "ImageObject",
273      "name": "35 60 07 CarsD 01a 2",
274      "description": "During their first introduction the Bouldin family tries to coax a smile out of Dmitry, right, a 5-year-old Russia orphan.   Dmitry was assigned to stay with the family for three weeks to see if child and family were a match.",
275      "caption": "During their first introduction the Bouldin family tries to coax a smile out of Dmitry, right, a 5-year-old Russia orphan.   Dmitry was assigned to stay with the family for three weeks to see if child and family were a match.",
276      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/35-60-07-CarsD-01a_2.jpg"
277    },
278    {
279      "@type": "ImageObject",
280      "name": "2012 13 Contraceptive Choice4",
281      "description": "Tuesday June 28, 2011--Malachi Lawrence, 3, plays in the bedroom in his and his mother's, Deidre Woodberry, apartment in north St. Louis County.  Woodberry participated in Contraceptive Choice Project trial so
281on after the birth of her son Malachi.  Woodberry knew she didn't want to have more kids while she was trying to finish up her college degree.\\nDavid Carson    [email protected]",
282      "caption": "Tuesday June 28, 2011--Malachi Lawrence, 3, plays in the bedroom in his and his mother's, Deidre Woodberry, apartment in north St. Louis County.  Woodberry participated in Contraceptive Choice Project trial soon after the birth of her son Malachi.  Woodberry knew she didn't want to have more kids while she was trying to finish up her college degree.\\nDavid Carson    [email protected]",
283      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/2012-13-Contraceptive-Choice4.jpg"
284    },
285    {
286      "@type": "ImageObject",
287      "name": "53 CD DC Hose Spray 2",
288      "description": "Thursday June 25, 2009--Gerrell Lawson, 12, basks in the spray of a garden hose as he plays with is friends is Hillsdale on Thursday.  Thursday marked the eighth straight day of temperatures above 90 and Saturday is expect to top out close to 100 with some cooling into the upper 80s expected for Sunday.\\nDavid Carson    [email protected]",
289      "caption": "Thursday June 25, 2009--Gerrell Lawson, 12, basks in the spray of a garden hose as he plays with is friends is Hillsdale on Thursday.  Thursday marked the eighth straight day of temperatures above 90 and Saturday is expect to top out close to 100 with some cooling into the upper 80s expected for Sunday.\\nDavid Carson    [email protected]",
290      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/53-CD-DC-Hose-Spray-2.jpg"
291    },
292    {
293      "@type": "ImageObject",
294      "name": "070313 DC Fair STL1",
295      "description": "A Twin Beech airplane circles over the Arch trailing smoke after members of the Liberty Parachute Team jumped out from about 5,000 feet above St. Louis during a practice run for the Fair St. Louis air show on Wednesday, July 3, 2013.  This year's fair will feature air shows at 12 p.m. and 4 p.m. on Thursday and Friday and at 5 p.m. on Saturday.\\nPhoto By David Carson, [email protected]",
296      "caption": "A Twin Beech airplane circles over the Arch trailing smoke after members of the Liberty Parachute Team jumped out from about 5,000 feet above St. Louis during a practice run for the Fair St. Louis air show on Wednesday, July 3, 2013.  This year's fair will feature air shows at 12 p.m. and 4 p.m. on Thursday and Friday and at 5 p.m. on Saturday.\\nPhoto By David Carson, [email protected]",
297      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/070313-DC-Fair-STL1.jpg"
298    },
299    {
300      "@type": "ImageObject",
301      "name": "a11 CD DC Glowing Globesb",
302      "description": "Sunday January 31, 2010--Glowing globes fill the splash and spray water fountain area of Citygarden, a public sculpture park in downtown St. Louis, on Sunday.   The 25 globes are each lit by two LED lights and are programed to glow in an every changing a rainbow of hues.  The globes will be turned on every night through the end of February from 5pm-midnight.  The globes were installed by Hydro Dramatics, a St. Louis fountain design company.  To see more photos of the glowing globes go to stltoday.com/multimedia .\\nDavid Carson     [email protected]",
303      "caption": "Sunday January 31, 2010--Glowing globes fill the splash and spray water fountain area of Citygarden, a public sculpture park in downtown St. Louis, on Sunday.   The 25 globes are each lit by two LED lights and are programed to glow in an every changing a rainbow of hues.  The globes will be turned on every night through the end of February from 5pm-midnight.  The globes were installed by Hydro Dramatics, a St. Louis fountain design company.  To see more photos of the glowing globes go to stltoday.com/multimedia .\\nDavid Carson     [email protected]",
304      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/a11-CD-DC-Glowing-Globesb.jpg"
305    },
306    {
307      "@type": "ImageObject",
308      "name": "Portfolio 013",
309      "description": "Wednesday August 24, 2005--Honey bees flock to a field sun flowers off of Illinois Route 159 in Maryville on Wednesday.  Temperatures should remain in the 80's for the rest of the week and weekend with a chance of storms through Saturday.\\nPHOTO BY DAVID CARSON/PD",
310      "caption": "Wednesday August 24, 2005--Honey bees flock to a field sun flowers off of Illinois Route 159 in Maryville on Wednesday.  Temperatures should remain in the 80's for the rest of the week and weekend with a chance of storms through Saturday.\\nPHOTO BY DAVID CARSON/PD",
311      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---013.jpg"
312    },
313    {
314      "@type": "ImageObject",
315      "name": "Portfolio 012",
316      "description": "A meteor streaks across the heavens above a farm field in Highland Illinois during the Leonid meteor shower.  The field and tree are illuminated by light of from the full moon.",
317      "caption": "A meteor streaks across the heavens above a farm field in Highland Illinois during the Leonid meteor shower.  The field and tree are illuminated by light of from the full moon.",
318      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---012.jpg"
319    },
320    {
321      "@type": "ImageObject",
322      "name": "CD DC Slip Slide",
323      "description": "Grace Wickham (front), Rachel Hutson (center), Matt Wickham, and Reilly Newton get some help from Noah Cochran (left) as he hoses down the plastic sheeting while they launch themselves down a homemade Slip 'n Slide during a party at the Wickham's home in Weldon Spring on Monday May 28, 2012.  The Wickhams made the slide from a 100 foot roll of plastic sheeting, some sprinklers to wet the slide and dish soap to make it slippery.\\nPhoto By David Carson, [email protected]",
324      "caption": "Grace Wickham (front), Rachel Hutson (center), Matt Wickham, and Reilly Newton get some help from Noah Cochran (left) as he hoses down the plastic sheeting while they launch themselves down a homemade Slip 'n Slide during a party at the Wickham's home in Weldon Spring on Monday May 28, 2012.  The Wickhams made the slide from a 100 foot roll of plastic sheeting, some sprinklers to wet the slide and dish soap to make it slippery.\\nPhoto By David Carson, [email protected]",
325      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Slip-Slide.jpg"
326    },
327    {
328      "@type": "ImageObject",
329      "name": "CD DC SWIMMING HOLE 1",
330      "description": "Monday August 5, 2002--Jake Whitehead, 16, of Edwardsville, launches himself off a rope swing into a pond as his friend Shaun Fink, 16, of Glen Carbon wat
330ches.  \\\"We're just hanging out, cooling off and having fun\\\" said Fink of their mid-afternoon dip in a swimming hole off of  Oaklawn Road in Glen Carbon on Monday.  Tempature will cool off into the upper 80's for Tuesday but return to the mid-90's later in the week.\\nPhoto By David Carson/PD",
331      "caption": "CD DC SWIMMING HOLE 1",
332      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/51-Portfolio---002.jpg"
333    },
334    {
335      "@type": "ImageObject",
336      "name": "Arch Lightning",
337      "description": "Lightning streaks across the sky above the Gateway Memorial Arch in downtown St. Louis on Saturday.",
338      "caption": "Arch Lightning",
339      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---011.jpg"
340    },
341    {
342      "@type": "ImageObject",
343      "name": "00035114-ENT-001",
344      "description": "Terrance Bullock, of St. Louis, walks under the steam spouting from the lions head on the Bee Hat Lofts along N. 11th Street on Thursday.  Freezing temperatures with day time highs in the teens are expected to continue for the next three days.  The steam was added to the building as an ornamental detail when it was renovated into lofts from 2005-2006.",
345      "caption": "00035114-ENT-001",
346      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/00035114-ENT-001.jpg"
347    },
348    {
349      "@type": "ImageObject",
350      "name": "9912CircusElephant",
351      "description": "",
352      "caption": "9912CircusElephant",
353      "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/9912CircusElephant.jpg"
354    }
355  ],
356  "numberOfItems": 35,
357  "primaryImageOfPage": {
358    "@type": "ImageObject",
359    "url": "https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg",
360    "width": 1200,
361    "height": 630,
362    "caption": "Life | Simple Image Page"
363  },
364  "image": {
365    "@type": "ImageObject",
366    "url": "https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg",
367    "width": 1200,
368    "height": 630,
369    "caption": "Life | Simple Image Page"
370  }
371}
372  </script>
372
373  
374  
375  <!-- Preview mode helper script -->
376  
376<script>
377    // Immediately execute to patch request methods before any other script runs
378    (function() {
379      // Detect preview mode by checking hostname
380      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
381      
382      if (isPreviewMode) {
383        console.log("[Preview Helper] Preview mode detected");
384        
385        // Extract GUID from URL path (first segment)
386        const pathParts = window.location.pathname.split('/').filter(Boolean);
387        const previewGuid = pathParts.length > 0 ? pathParts[0] : '';
388        
389        if (!previewGuid) {
390          console.warn("[Preview Helper] No GUID found in path:", window.location.pathname);
391          return; // Exit if no GUID found
392        }
393        
394        console.log("[Preview Helper] Using GUID:", previewGuid);
395        
396        // Store these in window for other scripts
397        window.isPreviewMode = true;
398        window.previewGuid = previewGuid;
399        window.isHydraEnvironment = true;
400        window.Parameters = window.Parameters || {};
401        window.Parameters.isHydra = true;
402        
403        // Helper function that adds the GUID to API URLs and relative links
404        window.getApiUrl = function(endpoint) {
405          // For relative paths
406          if (endpoint.startsWith('/api/')) {
407            const newUrl = '/' + previewGuid + endpoint;
408            console.log("[Preview Helper] Transformed API URL:", endpoint, "→", newUrl);
409            return newUrl;
410          }
411          return endpoint;
412        };
413        
414        // Helper function that adds the GUID to any relative link
415        window.getPreviewUrl = function(url) {
416          // For relative paths (starts with /) that aren't already prefixed with GUID
417          if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
418            const newUrl = '/' + previewGuid + url;
419            console.log("[Preview Helper] Transformed relative link:", url, "→", newUrl);
420            return newUrl;
421          }
422          return url;
423        };
424        
425        // PATCH FETCH API with enhanced URL handling
426        const originalFetch = window.fetch;
427        window.fetch = function(resource, init) {
428          // Only transform string URLs
429          if (typeof resource === 'string') {
430            // Case 1: Handle relative paths starting with /api/
431            if (resource.startsWith('/api/')) {
432              const newUrl = window.getApiUrl(resource);
433              console.log("[Preview Helper] Patched fetch call (API):", resource, "→", newUrl);
434              return originalFetch(newUrl, init);
435            }
436            
437            // Case 2: Handle other relative paths (for content/gallery links)
438            else if (resource.startsWith('/') && !resource.startsWith('/' + previewGuid + '/')) {
439              const newUrl = window.getPreviewUrl(resource);
440              console.log("[Preview Helper] Patched fetch call (relative):", resource, "→", newUrl);
441              return originalFetch(newUrl, init);
442            }
443            
444            // Case 3: Handle absolute URLs to the same domain containing /api/
445            else if (resource.includes(window.location.host) && resource.includes('/api/')) {
446              try {
447                const urlObj = new URL(resource);
448                if (urlObj.hostname === window.location.hostname) {
449                  const path = urlObj.pathname;
450                  // Already has /{guid}/api/... (e.g. from new URL(getApiUrl(...), origin)) — do not double-prefix
451                  if (path.startsWith('/' + previewGuid + '/')) {
452                    return originalFetch(resource, init);
453                  }
454                  const newPath = '/' + previewGuid + path;
455                  urlObj.pathname = newPath;
456                  const newUrl = urlObj.toString();
457                  console.log("[Preview Helper] Patched fetch call (absolute API):", resource, "→", newUrl);
458                  return originalFetch(newUrl, init);
459                }
460              } catch (e) {
461                console.error("[Preview Helper] Error processing URL:", e);
462              }
463            }
464            
465            // Case 4: Handle absolute URLs to the same domain (non-API)
466            else if (resource.includes(window.location.host)) {
467              try {
468                const urlObj = new URL(resource);
469                if (urlObj.hostname === window.location.hostname && 
470                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
471                  const path = urlObj.pathname;
472                  const newPath = '/' + previewGuid + path;
473                  urlObj.pathname = newPath;
474                  const newUrl = urlObj.toString();
475                  console.log("[Preview Helper] Patched fetch call (absolute):", resource, "→", newUrl);
476                  return originalFetch(newUrl, init);
477                }
478              } catch (e) {
479                console.error("[Preview Helper] Error processing URL:", e);
480              }
481            }
482          }
483          return originalFetch(resource, init);
484        };
485        console.log("[Preview Helper] Patched fetch API");
486        
487        // PATCH XMLHttpRequest with enhanced URL handling
488        const originalOpen = XMLHttpRequest.prototype.open;
489        XMLHttpRequest.prototype.open = function(method, url, ...rest) {
490          // Only transform string URLs
491          if (typeof url === 'string') {
492            // Case 1: Handle relative paths starting with /api/
493            if (url.startsWith('/api/')) {
494              const newUrl = window.getApiUrl(url);
495              console.log("[Preview Helper] Patched XMLHttpRequest (API):", url, "→", newUrl);
496              return originalOpen.call(this, method, newUrl, ...rest);
497            }
498            
499            // Case 2: Handle other relative paths
500            else if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) {
501              const newUrl = window.getPreviewUrl(url);
502              console.log("[Preview Helper] Patched XMLHttpRequest (relative):", url, "→", newUrl);
503              return originalOpen.call(this, method, newUrl, ...rest);
504            }
505            
506            // Case 3: Handle absolute URLs to the same domain containing /api/
507            else if (url.includes(window.location.host) && url.includes('/api/')) {
508              try {
509                const urlObj = new URL(url);
510                if (urlObj.hostname === window.location.hostname) {
511                  const path = urlObj.pathname;
512                  if (path.startsWith('/' + previewGuid + '/')) {
513                    return originalOpen.call(this, method, url, ...rest);
514                  }
515                  const newPath = '/' + previewGuid + path;
516                  urlObj.pathname = newPath;
517                  const newUrl = urlObj.toString();
518                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute API):", url, "→", newUrl);
519                  return originalOpen.call(this, method, newUrl, ...rest);
520                }
521              } catch (e) {
522                console.error("[Preview Helper] Error processing URL:", e);
523              }
524            }
525            
526            // Case 4: Handle absolute URLs to the same domain (non-API)
527            else if (url.includes(window.location.host)) {
528              try {
529                const urlObj = new URL(url);
530                if (urlObj.hostname === window.location.hostname && 
531                    !urlObj.pathname.startsWith('/' + previewGuid + '/')) {
532                  const path = urlObj.pathname;
533                  const newPath = '/' + previewGuid + path;
534                  urlObj.pathname = newPath;
535                  const newUrl = urlObj.toString();
536                  console.log("[Preview Helper] Patched XMLHttpRequest (absolute):", url, "→", newUrl);
537                  return originalOpen.call(this, method, newUrl, ...rest);
538                }
539              } catch (e) {
540                console.error("[Preview Helper] Error processing URL:", e);
541              }
542            }
543          }
544          return originalOpen.call(this, method, url, ...rest);
545        };
546        console.log("[Preview Helper] Patched XMLHttpRequest");
547        
548        // Note: Link click handling is done by the existing gallery link interceptor
549        // The fetch and XMLHttpRequest patches above handle programmatic requests
550        
551        // Add debugging aid
552        window.debugPreviewUrls = function() {
553          return {
554            hostname: window.location.hostname,
555            pathname: window.location.pathname,
556            guid: window.previewGuid,
557            isPreviewMode: window.isPreviewMode,
558            sampleTransformations: {
559              relativeApi: window.getApiUrl('/api/menu-styles'),
560              relativeLink: window.getPreviewUrl('/gallery'),
561              absolute: new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/')
562            }
563          };
564        };
565        
566        // Test the transformation immediately
567        console.log("[Preview Helper] Test API transformation:", window.getApiUrl('/api/menu-styles'));
568        console.log("[Preview Helper] Test relative link transformation:", window.getPreviewUrl('/gallery'));
569        console.log("[Preview Helper] Test absolute URL transformation:", 
570          new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/'));
571      }
572    })();
573  </script>
573
574  
575  
576  
577  
578<!-- Early CDN retry helpers: MUST run before CDN script/link tags.
579     Changing script.src after a load error is unreliable (esp. Firefox defer);
580     always insert a new element pointing at /cdn-proxy/. -->
581<script>
582(function() {
583  window.useCdnProxy = window.useCdnProxy || false;
584  window.__hydraCdnRetryLog = window.__hydraCdnRetryLog || [];
585  function toProxy(url) {
586    if (!url || url.indexOf('cdn.neonsky.app') === -1) return url;
587    return url.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
588  }
589  function noteRetry(kind, fromUrl, toUrl) {
590    try {
591      window.useCdnProxy = true;
592      var entry = { kind: kind, from: fromUrl, to: toUrl, t: Date.now() };
593      window.__hydraCdnRetryLog.push(entry);
594      console.warn('🔄 CDN failed, retrying via new element:', kind, fromUrl, '->', toUrl);
595    } catch (e) {}
596  }
597  window.__hydraRetryCdnScript = function(el) {
598    try {
599      if (!el || el.getAttribute('data-retried') === 'true') return;
600      var original = el.src || '';
601      if (original.indexOf('cdn.neonsky.app') === -1) return;
602      el.setAttribute('data-retried', 'true');
603      el.onerror = null;
604      var proxyUrl = toProxy(original);
605      noteRetry('script', original, proxyUrl);
606      var s = document.createElement('script');
607      s.src = proxyUrl;
608      s.setAttribute('data-retried', 'true');
609      if (el.id) s.id = el.id;
610      // Dynamic scripts ignore defer; omit it so the proxy copy executes on load.
611      if (el.parentNode) {
612        el.parentNode.insertBefore(s, el.nextSibling);
613      } else {
614        (document.head || document.documentElement).appendChild(s);
615      }
616    } catch (e) {}
617  };
618  window.__hydraRetryCdnLink = function(el) {
619    try {
620      if (!el || el.getAttribute('data-retried') === 'true') return;
621      var original = el.href || '';
622      if (original.indexOf('cdn.neonsky.app') === -1) return;
623      el.setAttribute('data-retried', 'true');
624      el.onerror = null;
625      var proxyUrl = toProxy(original);
626      noteRetry('link', original, proxyUrl);
627      var link = document.createElement('link');
628      link.rel = el.rel || 'stylesheet';
629      if (el.id) link.id = el.id;
630      link.href = proxyUrl;
631      link.setAttribute('data-retried', 'true');
632      if (el.parentNode) {
633        el.parentNode.insertBefore(link, el.nextSibling);
634      } else {
635        (document.head || document.documentElement).appendChild(link);
636      }
637    } catch (e) {}
638  };
639})();
640</script>
640
641<link rel="stylesheet" href="https://cdn.neonsky.app/menu-styles-v260923-002.css?t=1791093390101" onerror="window.__hydraRetryCdnLink(this)">
642<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
vendor: 1 bytes, line 642
642
643<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
vendor: 1 bytes, line 643
643
644<script src="https://cdn.jsdelivr.net/npm/magic-sdk"></script>
vendor: 1 bytes, line 644
644
645<script src="https://cdn.jsdelivr.net/npm/@magic-ext/auth"></script>
vendor: 1 bytes, line 645
645
646<script src="https://cdn.neonsky.app/style-customizer-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
646
647<link rel="stylesheet" href="https://cdn.neonsky.app/palette-explorer-v260923-002.css?t=1791093390101" onerror="window.__hydraRetryCdnLink(this)">
648<script src="https://cdn.neonsky.app/palette-explorer-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 648
648
649<script src="https://cdn.neonsky.app/sidebar-manager-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 649
649
650<script src="https://cdn.neonsky.app/image-processor-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 650
650
651<script src="https://cdn.neonsky.app/image-uploader-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 651
651
652<script src="https://cdn.neonsky.app/page-layout-presets-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 652
652
653<script src="https://cdn.neonsky.app/site-graph-presets-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 653
653
654<script src="https://cdn.neonsky.app/site-graph-sync-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 654
654
655<script src="https://cdn.neonsky.app/page-manager-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
655
656<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
657<link rel="stylesheet" href="https://cdn.neonsky.app/page-styles-v260923-002.css?t=1791093390101" onerror="window.__hydraRetryCdnLink(this)">
658
659
660
661<!-- CDN Fallback Handler for CSS and JS files -->
662<script>
663(function() {
664  // Global flag: Set to true when any CDN resource fails and uses proxy
665  window.useCdnProxy = window.useCdnProxy || false;
666  
667  // Global base URL function: Returns proxy base if flag is set, otherwise CDN base
668  window.getCdnBaseUrl = function() {
669    return window.useCdnProxy 
670      ? window.location.origin + '/cdn-proxy/'
671      : 'https://cdn.neonsky.app/';
672  };
673  
674  // Utility function to convert CDN URLs to proxy URLs
675  window.convertCdnToProxyUrl = function(cdnUrl) {
676    if (!cdnUrl || !cdnUrl.includes('cdn.neonsky.app')) return cdnUrl;
677    return cdnUrl.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
678  };
679  
680  // Utility function to construct CDN URLs (uses proxy if flag is set)
681  window.buildCdnUrl = function(path) {
682    return window.getCdnBaseUrl() + path;
683  };
684  
685  // Resource tracking for debugging
686  window._resourceTracking = window._resourceTracking || {
687    resources: [],
688    proxyFlagChanges: [],
689    imageLoads: [],
690    networkErrors: []
691  };
692  
693  // Track when proxy flag changes - use a wrapper function
694  let proxyFlagChangeCount = 0;
695  let _useCdnProxyValue = window.useCdnProxy || false;
696  
697  // Create a setter function that tracks changes
698  window._setUseCdnProxy = function(newValue, reason) {
699    const oldValue = _useCdnProxyValue;
700    if (oldValue !== newValue) {
701      proxyFlagChangeCount++;
702      const changeInfo = {
703        timestamp: new Date().toISOString(),
704        changeNumber: proxyFlagChangeCount,
705        oldValue: oldValue,
706        newValue: newValue,
707        reason: reason || 'Unknown',
708        stack: new Error().stack?.split('\n').slice(1, 6).join('\n') || 'No stack'
709      };
710      window._resourceTracking.proxyFlagChanges.push(changeInfo);
711      console.error('🔍 PROXY_FLAG_CHANGE:', JSON.stringify(changeInfo, null, 2));
712      // Keep only last 50 changes
713      if (window._resourceTracking.proxyFlagChanges.length > 50) {
714        window._resourceTracking.proxyFlagChanges.shift();
715      }
716      _useCdnProxyValue = newValue;
717      window.useCdnProxy = newValue;
718    }
719  };
720  
721  // Track resource loading
722  function trackResource(url, type, status, details) {
723    const entry = {
724      timestamp: new Date().toISOString(),
725      url: url,
726      type: type, // 'script', 'link', 'image', etc.
727      status: status, // 'loading', 'success', 'failed', 'proxy', 'storage'
728      details: details || {},
729      useCdnProxy: window.useCdnProxy
730    };
731    window._resourceTracking.resources.push(entry);
732    console.error('📊 RESOURCE_TRACK:', JSON.stringify(entry, null, 2));
733    // Keep only last 200 resources
734    if (window._resourceTracking.resources.length > 200) {
735      window._resourceTracking.resources.shift();
736    }
737  }
738  
739  // Track image loads
740  function trackImageLoad(url, status, source) {
741    const entry = {
742      timestamp: new Date().toISOString(),
743      url: url,
744      status: status, // 'loading', 'success', 'failed'
745      source: source, // 'cdn', 'proxy', 'storage'
746      useCdnProxy: window.useCdnProxy
747    };
748    window._resourceTracking.imageLoads.push(entry);
749    console.error('🖼️ IMAGE_LOAD:', JSON.stringify(entry, null, 2));
750    // Keep only last 100 image loads
751    if (window._resourceTracking.imageLoads.length > 100) {
752      window._resourceTracking.imageLoads.shift();
753    }
754  }
755  
756  // Track network errors
757  function trackNetworkError(url, error, context) {
758    const entry = {
759      timestamp: new Date().toISOString(),
760      url: url,
761      error: {
762        message: error?.message || 'Unknown error',
763        name: error?.name || 'Unknown',
764        type: error?.type || 'unknown',
765        toString: String(error)
766      },
767      context: context || {},
768      useCdnProxy: window.useCdnProxy
769    };
770    window._resourceTracking.networkErrors.push(entry);
771    console.error('❌ NETWORK_ERROR:', JSON.stringify(entry, null, 2));
772    // Keep only last 100 errors
773    if (window._resourceTracking.networkErrors.length > 100) {
774      window._resourceTracking.networkErrors.shift();
775    }
776  }
777  
778  // Expose tracking functions globally
779  window.trackResource = trackResource;
780  window.trackImageLoad = trackImageLoad;
781  window.trackNetworkError = trackNetworkError;
782  
783  // Function to retry failed resources via proxy
784  // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag
785  // The proxy can handle both network errors and 404s, so it's safer to err on the side of using it
786  function retryViaProxy(element, originalSrc, errorEvent) {
787    if (!originalSrc) originalSrc = element.href || element.src;
788    if (!originalSrc || !originalSrc.includes('cdn.neonsky.app')) return;
789    
790    // Prevent multiple retries
791    if (element.getAttribute('data-retried')) {
792      return;
793    }
794    element.setAttribute('data-retried', 'true');
795    
796    // Check if element still exists in DOM
797    if (!element.parentNode) {
798      console.warn('⚠️ Element no longer in DOM, skipping proxy retry:', originalSrc);
799      return;
800    }
801    
802    // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s
803    // We'll be aggressive and set the flag for any CDN failure to ensure images also use proxy
804    // Check error message if available, but default to assuming network error
805    const hasExplicitNetworkError = errorEvent && errorEvent.message && (
806      errorEvent.message.includes('Failed to fetch') ||
807      errorEvent.message.includes('SSL') ||
808      errorEvent.message.includes('secure connection') ||
809      errorEvent.message.includes('network') ||
810      errorEvent.message.includes('ERR_') ||
811      errorEvent.message.includes('SSL_ERROR')
812    );
813    
814    // Track the error
815    trackNetworkError(originalSrc, errorEvent, {
816      elementType: element.tagName,
817      hasExplicitNetworkError: hasExplicitNetworkError,
818      errorMessage: errorEvent?.message || 'No error message',
819      errorName: errorEvent?.name || 'No error name'
820    });
821    
822    // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure)
823    // This covers ISP-affected clients where SSL handshake fails before HTTP request
824    const reason = hasExplicitNetworkError 
825      ? 'Explicit network/SSL error detected'
826      : (!errorEvent || !errorEvent.message)
827        ? 'Browser silent failure (likely SSL/network)'
828        : 'CDN resource failed (using proxy as fallback)';
829    
830    if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) {
831      window._setUseCdnProxy(true, reason);
832      console.error('🚩 CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests');
833      console.error('   Error details:', errorEvent ? (errorEvent.message || 'No error message (likely SSL/network failure)') : 'No error event');
834    } else {
835      // Even for other errors, set the flag to be safe - proxy can handle 404s too
836      window._setUseCdnProxy(true, reason);
837      console.error('🚩 CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback');
838    }
839    
840    const proxyUrl = originalSrc.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
841    console.error('🔄 CDN failed, retrying via proxy:', originalSrc, '->', proxyUrl);
842    
843    // Track the retry
844    trackResource(originalSrc, element.tagName.toLowerCase(), 'failed', {
845      retryUrl: proxyUrl,
846      error: errorEvent?.message || 'Unknown'
847    });
848    
849    if (element.tagName === 'SCRIPT') {
850      const newScript = document.createElement('script');
851      newScript.src = proxyUrl;
852      // Do not copy defer/async onto dynamically inserted scripts. After a CDN
853      // failure, DOMContentLoaded may already have fired; deferred dynamic
854      // scripts can fail to execute (seen on Firefox ISP/CDN-fail clients).
855      newScript.setAttribute('data-retried', 'true');
856      newScript.onload = function() {
857        trackResource(proxyUrl, 'script', 'proxy-success', { originalUrl: originalSrc });
858      };
859      newScript.onerror = function() {
860        trackResource(proxyUrl, 'script', 'proxy-failed', { originalUrl: originalSrc });
861        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
862        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
863        trackResource(storageUrl, 'script', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
864        newScript.src = storageUrl;
865        newScript.onload = function() {
866          trackResource(storageUrl, 'script', 'storage-success', { originalUrl: originalSrc });
867        };
868      };
869      element.parentNode.replaceChild(newScript, element);
870    } else if (element.tagName === 'LINK') {
871      const newLink = document.createElement('link');
872      newLink.rel = element.rel;
873      newLink.href = proxyUrl;
874      newLink.setAttribute('data-retried', 'true');
875      newLink.onload = function() {
876        trackResource(proxyUrl, 'link', 'proxy-success', { originalUrl: originalSrc });
877      };
878      newLink.onerror = function() {
879        trackResource(proxyUrl, 'link', 'proxy-failed', { originalUrl: originalSrc });
880        console.error('❌ Proxy also failed, trying storage:', proxyUrl);
881        const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app');
882        trackResource(storageUrl, 'link', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl });
883        newLink.href = storageUrl;
884        newLink.onload = function() {
885          trackResource(storageUrl, 'link', 'storage-success', { originalUrl: originalSrc });
886        };
887      };
888      element.parentNode.replaceChild(newLink, element);
889    }
890  }
891  
892  // Monitor script and link tags for errors
893  document.addEventListener('error', function(e) {
894    const target = e.target;
895    if ((target.tagName === 'SCRIPT' || target.tagName === 'LINK') && 
896        target.src && target.src.includes('cdn.neonsky.app')) {
897      retryViaProxy(target, target.src, e);
898    } else if (target.tagName === 'LINK' && target.href && target.href.includes('cdn.neonsky.app')) {
899      retryViaProxy(target, target.href, e);
900    }
901  }, true);
902  
903  // Also check existing scripts/links that may have failed before this script loaded
904  // For ISP-affected clients, we assume CDN failures are network/SSL errors
905  setTimeout(function() {
906    document.querySelectorAll('script[src*="cdn.neonsky.app"], link[href*="cdn.neonsky.app"]').forEach(function(el) {
907      if (el.tagName === 'SCRIPT' && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
908        el.addEventListener('error', function(errorEvent) {
909          // For ISP-affected clients, assume any CDN failure is a network/SSL error
910          // The proxy can handle both network errors and 404s
911          retryViaProxy(el, el.src, errorEvent);
912        });
913      } else if (el.tagName === 'LINK' && !el.sheet && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) {
914        // CSS link failed if it has no stylesheet - check after a delay
915        setTimeout(function() {
916          if (!el.sheet && !el.getAttribute('data-retried')) {
917            // For ISP-affected clients, assume any CDN failure is a network/SSL error
918            retryViaProxy(el, el.href, null);
919          }
920        }, 2000);
921      }
922    });
923  }, 100);
924})();
925</script>
925
926
927<!-- Log Capture System - S+J to download logs (works even if gallery doesn't load) -->
928<script>
929(function() {
930  'use strict';
931  
932  // Only initialize if not already initialized
933  if (window._indexLogCaptureInitialized) {
934    return;
935  }
936  window._indexLogCaptureInitialized = true;
937  
938  // Wait for DOM to be ready
939  function initLogCapture() {
940    // Store original console methods
941    const originalConsole = {
942      log: console.log,
943      warn: console.warn,
944      error: console.error,
945      info: console.info
946    };
947
948    // Array to store all console messages
949    window._capturedLogs = window._capturedLogs || [];
950
951    // Override console methods to capture logs
952    ['log', 'warn', 'error', 'info'].forEach(method => {
953      console[method] = function(...args) {
954        // Call original method
955        originalConsole[method].apply(console, args);
956        
957        // Capture the log with timestamp
958        window._capturedLogs.push({
959          timestamp: new Date().toISOString(),
960          level: method.toUpperCase(),
961          message: args.map(function(arg) {
962            if (arg !== null && arg !== undefined && Object(arg) === arg) {
963              try {
964                return JSON.stringify(arg, null, 2);
965              } catch (e) {
966                return String(arg);
967              }
968            }
969            return String(arg);
970          }).join(' ')
971        });
972        
973        // Keep only last 2000 logs to prevent memory issues
974        if (window._capturedLogs.length > 2000) {
975          window._capturedLogs.shift();
976        }
977      };
978    });
979
980    // Add keyboard shortcut to download logs (S+J)
981    let pressedKeys = new Set();
982    
983    document.addEventListener('keydown', function(event) {
984      const key = (event.key || '').toLowerCase();
985      
986      if (key === 's' || key === 'j') {
987        pressedKeys.add(key);
988        
989        // Check if both S and J are pressed
990        if (pressedKeys.has('s') && pressedKeys.has('j')) {
991          event.preventDefault();
992          console.log('S+J detected - downloading logs...');
993          
994          // Show a brief visual indicator
995          const indicator = document.createElement('div');
996          indicator.style.cssText = 
997            'position: fixed; top: 20px; right: 20px; z-index: 10000; ' +
998            'background: #4CAF50; color: white; padding: 10px 20px; ' +
999            'border-radius: 5px; font-family: Arial, sans-serif; ' +
1000            'font-size: 14px; font-weight: bold; ' +
1001            'box-shadow: 0 2px 10px rgba(0,0,0,0.3);';
1002          indicator.textContent = '📥 Downloading logs...';
1003          document.body.appendChild(indicator);
1004          
1005          setTimeout(() => {
1006            if (indicator.parentNode) {
1007              indicator.parentNode.removeChild(indicator);
1008            }
1009          }, 2000);
1010          
1011          downloadLogs();
1012          // Clear the set after triggering
1013          pressedKeys.clear();
1014        }
1015      }
1016    });
1017    
1018    document.addEventListener('keyup', function(event) {
1019      const key = (event.key || '').toLowerCase();
1020      if (key === 's' || key === 'j') {
1021        pressedKeys.delete(key);
1022      }
1023    });
1024
1025    console.log('📋 Log capture system initialized. Press S+J simultaneously to download logs.');
1026  }
1027
1028  // Download captured logs as a text file
1029  function downloadLogs() {
1030    try {
1031      // Get all scripts and links, categorized
1032      const allScripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src);
1033      const allLinks = Array.from(document.querySelectorAll('link[href]')).map(l => l.href);
1034      
1035      // Get additional debug info
1036      const debugInfo = {
1037        url: window.location.href,
1038        userAgent: navigator.userAgent,
1039        timestamp: new Date().toISOString(),
1040        useCdnProxy: window.useCdnProxy || false,
1041        windowLocation: window.location.toString(),
1042        documentReadyState: document.readyState,
1043        // All scripts/links (not just CDN)
1044        allScriptsLoaded: allScripts,
1045        allLinksLoaded: allLinks,
1046        // CDN-specific (for backward compatibility and CDN debugging)
1047        scriptsLoaded: allScripts.filter(src => src.includes('cdn.neonsky.app')),
1048        linksLoaded: allLinks.filter(href => href.includes('cdn.neonsky.app')),
1049        // Gallery scripts (loaded from Worker)
1050        galleryScriptsLoaded: allScripts.filter(src => src.includes('/gallery-scripts/')),
1051        galleryLinksLoaded: allLinks.filter(href => href.includes('/gallery-scripts/')),
1052        // Failed resources
1053        failedScripts: Array.from(document.querySelectorAll('script[src*="cdn.neonsky.app"][data-retried]')).map(s => s.src),
1054        failedLinks: Array.from(document.querySelectorAll('link[href*="cdn.neonsky.app"][data-retried]')).map(l => l.href),
1055        // Proxy resources
1056        proxyScriptsLoaded: allScripts.filter(src => src.includes('/cdn-proxy/')),
1057        proxyLinksLoaded: allLinks.filter(href => href.includes('/cdn-proxy/')),
1058        // Resource tracking data
1059        resourceTracking: window._resourceTracking || {
1060          resources: [],
1061          proxyFlagChanges: [],
1062          imageLoads: [],
1063          networkErrors: []
1064        },
1065        // Performance timing
1066        performanceTiming: performance.timing ? {
1067          navigationStart: performance.timing.navigationStart,
1068          domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
1069          loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart,
1070          domInteractive: performance.timing.domInteractive - performance.timing.navigationStart
1071        } : null,
1072        // Style / logo diagnostics (layout/color bug triage)
1073        styleDiagnostics: (function() {
1074          try {
1075            var rootStyle = getComputedStyle(document.documentElement);
1076            var logoNodes = Array.from(document.querySelectorAll('.sidebar img, .sidebar-element img, img[src*="robert-vizzini"], .horizontal-header-logo img'));
1077            return {
1078              siteBackgroundColor: rootStyle.getPropertyValue('--site-background-color').trim(),
1079              menuBackgroundColor: rootStyle.getPropertyValue('--menu-background-color').trim(),
1080              menuColor: rootStyle.getPropertyValue('--menu-color').trim(),
1081              inlineSiteBg: document.documentElement.style.getPropertyValue('--site-background-color'),
1082              inlineMenuBg: document.documentElement.style.getPropertyValue('--menu-background-color'),
1083              hasMenuStyleCustomizer: !!window.MenuStyleCustomizer,
1084              menuStyleCustomizerInitialized: !!(window.MenuStyleCustomizer && window.MenuStyleCustomizer._initialized),
1085              menuStyleSettingsBg: window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
1086                ? {
1087                    siteBackgroundColor: window.MenuStyleCustomizer.settings.siteBackgroundColor,
1088                    menuBackgroundColor: window.MenuStyleCustomizer.settings.menuBackgroundColor,
1089                    color: window.MenuStyleCustomizer.settings.color
1090                  }
1091                : null,
1092              hasSidebarManager: !!window.SidebarManager,
1093              sidebarElementCount: window.SidebarManager && window.SidebarManager.elements
1094                ? window.SidebarManager.elements.length
1095                : 0,
1096              hasPageManager: !!window.PageManager,
1097              pageManagerHasLoadPage: !!(window.PageManager && typeof window.PageManager.loadPage === 'function'),
1098              pageElementCount: (function() {
1099                try {
1100                  return document.querySelectorAll('.gallery-container .page-element, .page-container .page-element').length;
1101                } catch (e) { return -1; }
1102              })(),
1103              pageContainerHtmlLength: (function() {
1104                try {
1105                  var pc = document.querySelector('.gallery-container .page-container, .page-container');
1106                  return pc ? (pc.innerHTML || '').length : 0;
1107                } catch (e) { return -1; }
1108              })(),
1109              activeContentType: window._activeContentType || null,
1110              logoImageCount: logoNodes.length,
1111              logoImages: logoNodes.slice(0, 5).map(function(img) {
1112                return {
1113                  src: img.currentSrc || img.src || '',
1114                  naturalWidth: img.naturalWidth || 0,
1115                  display: getComputedStyle(img).display,
1116                  complete: !!img.complete
1117                };
1118              }),
1119              earlyCdnRetries: window.__hydraCdnRetryLog || []
1120            };
1121          } catch (e) {
1122            return { error: String(e && e.message ? e.message : e) };
1123          }
1124        })()
1125      };
1126
1127      // Create log content
1128      const logContent = [
1129        '=== INDEX.TS DEBUG LOGS ===',
1130        'Generated: ' + debugInfo.timestamp,
1131        'URL: ' + debugInfo.url,
1132        'User Agent: ' + debugInfo.userAgent,
1133        'useCdnProxy Flag: ' + debugInfo.useCdnProxy,
1134        'Document Ready State: ' + debugInfo.documentReadyState,
1135        '================================',
1136        'TIP: Search for [AUTH_SAVE] in CONSOLE LOGS below for authentication/save failure details.',
1137        'TIP: Search STYLE DIAGNOSTICS for layout/color/logo triage (backgrounds, MenuStyleCustomizer, SidebarManager, logo).',
1138        '================================',
1139        '',
1140        '=== DEBUG INFO ===',
1141        JSON.stringify(debugInfo, null, 2),
1142        '',
1143        '=== STYLE DIAGNOSTICS ===',
1144        JSON.stringify(debugInfo.styleDiagnostics, null, 2),
1145        '',
1146        '=== RESOURCE TRACKING SUMMARY ===',
1147        'Total Resources Tracked: ' + (debugInfo.resourceTracking.resources.length || 0),
1148        'Proxy Flag Changes: ' + (debugInfo.resourceTracking.proxyFlagChanges.length || 0),
1149        'Image Loads Tracked: ' + (debugInfo.resourceTracking.imageLoads.length || 0),
1150        'Network Errors Tracked: ' + (debugInfo.resourceTracking.networkErrors.length || 0),
1151        '',
1152        '=== PROXY FLAG CHANGES ===',
1153        JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2),
1154        '',
1155        '=== RESOURCE LOADING TRACKING ===',
1156        JSON.stringify(debugInfo.resourceTracking.resources, null, 2),
1157        '',
1158        '=== IMAGE LOADING TRACKING ===',
1159        JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2),
1160        '',
1161        '=== NETWORK ERRORS ===',
1162        JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2),
1163        '',
1164        '=== CONSOLE LOGS ===',
1165        ...(window._capturedLogs || []).map(log => 
1166          '[' + log.timestamp + '] ' + log.level + ': ' + log.message
1167        )
1168      ].join('\n');
1169
1170      // Create and download file
1171      const blob = new Blob([logContent], { type: 'text/plain' });
1172      const url = URL.createObjectURL(blob);
1173      const a = document.createElement('a');
1174      a.href = url;
1175      a.download = 'index-debug-logs-' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt';
1176      document.body.appendChild(a);
1177      a.click();
1178      document.body.removeChild(a);
1179      URL.revokeObjectURL(url);
1180
1181      console.log('✅ Debug logs downloaded successfully!');
1182    } catch (error) {
1183      console.error('❌ Failed to download logs:', error);
1184      alert('Failed to download logs. Error: ' + (error.message || 'Unknown error'));
1185    }
1186  }
1187
1188  // Initialize when DOM is ready
1189  if (document.readyState === 'loading') {
1190    document.addEventListener('DOMContentLoaded', initLogCapture);
1191  } else {
1192    // DOM already ready, initialize immediately
1193    setTimeout(initLogCapture, 100);
1194  }
1195})();
1196</script>
1196
1197
1198<!-- Add auth token if available -->
1199
1200
1201<style>
1202/* Override Pico.css body padding to prevent jumping between pages */
1203body {
1204  padding: 0 !important;
1205  margin: 0 !important;
1206  display: block !important;
1207}
1208
1209/* Scope Pico.css to only affect elements in edit mode */
1210:root:not([data-theme="pico"]) {
1211--pico-applied: dark;
1212}
1213
1214/* Add data-theme="pico" to body when in edit mode */
1215body.edit-mode-active {
1216data-theme: dark;
1217}
1218
1219/* Custom styles to ensure Pico only affects editor UI, not content */
1220.sidebar.editing button,
1221.edit-controls button,
1222.element-header button,
1223.text-editor button,
1224.image-uploader button {
1225--pico-applied: dark;
1226}
1227
1228/* Hide default browser-styling when Pico is active */
1229[data-theme="pico"] button.btn {
1230appearance: dark;
1231-webkit-appearance: dark;
1232}
1233
1234/* Ensure content isn't affected by Pico styles */
1235.gallery-container,
1236.page-text,
1237.sidebar-text {
1238--pico-applied: none !important;
1239}
1240
1241/* Conflict Indicator Styles */
1242#conflictIndicator {
1243  background: #d32f2f !important;
1244  border: 1px solid #d32f2f !important;
1245  border-radius: 4px;
1246  padding: 8px;
1247  cursor: pointer;
1248  transition: opacity 0.2s;
1249  margin-left: 8px;
1250  display: flex; /* Use flex for centering, but hidden by default via inline style */
1251  align-items: center;
1252  justify-content: center;
1253}
1254
1255#conflictIndicator:hover {
1256  background: #e53935 !important;
1257  border-color: #e53935 !important;
1258  opacity: 0.9;
1259}
1260
1261#conflictIndicator svg {
1262  display: block;
1263  width: 24px;
1264  height: 24px;
1265  margin: 0 auto;
1266  color: white !important;
1267}
1268
1269/* Tooltip styles for conflict indicator - allow wrapping */
1270#tooltip-container .tooltip[data-tooltip-for="conflictIndicator"],
1271#tooltip-container .tooltip {
1272  max-width: 200px;
1273  word-wrap: break-word;
1274  white-space: normal;
1275  line-height: 1.4;
1276}
1277
1278.global-header-controls {
1279  display: flex;
1280  align-items: center;
1281  gap: 8px;
1282}
1283
1284.sidebar-header {
1285  display: flex;
1286  align-items: center;
1287  gap: 8px;
1288}
1289
1290/* Cookie Notice — paper UI (public-safe; does not depend on edit-mode pe-* scopes) */
1291.cookie-notice {
1292  --cn-paper: #f6f5f1;
1293  --cn-paper-elevated: #fdfcfa;
1294  --cn-ink: #2c2a27;
1295  --cn-ink-muted: #6b6760;
1296  --cn-ink-faint: #9b968d;
1297  --cn-rule: rgba(44, 42, 39, 0.1);
1298  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1299  --cn-shadow: 0 -8px 28px rgba(44, 42, 39, 0.1);
1300  --cn-radius: 3px;
1301  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1302
1303  position: fixed;
1304  bottom: 0;
1305  left: 0;
1306  right: 0;
1307  z-index: 10000;
1308  display: flex;
1309  align-items: center;
1310  justify-content: space-between;
1311  flex-wrap: wrap;
1312  gap: 0.85rem 1.25rem;
1313  padding: 1.1rem 1.35rem 1.2rem;
1314  font-family: var(--cn-font);
1315  font-size: 14px;
1316  line-height: 1.45;
1317  color: var(--cn-ink);
1318  background: var(--cn-paper-elevated);
1319  border-top: 1px solid var(--cn-rule);
1320  box-shadow: var(--cn-shadow);
1321}
1322
1323.cookie-notice.hidden {
1324  display: none;
1325}
1326
1327.cookie-notice-text {
1328  flex: 1 1 220px;
1329  min-width: 0;
1330  margin: 0;
1331  color: var(--cn-ink-muted);
1332  font-size: 13px;
1333}
1334
1335.cookie-notice-text a.cookie-notice-policy {
1336  color: var(--cn-ink);
1337  font-weight: 600;
1338  text-decoration: underline;
1339  text-underline-offset: 2px;
1340}
1341
1342.cookie-notice-text a.cookie-notice-policy:hover {
1343  color: var(--cn-ink-muted);
1344}
1345
1346.cookie-notice-buttons {
1347  display: flex;
1348  flex-wrap: wrap;
1349  align-items: center;
1350  justify-content: flex-end;
1351  gap: 0.5rem;
1352}
1353
1354.cookie-notice .pe-btn {
1355  display: inline-flex;
1356  align-items: center;
1357  justify-content: center;
1358  gap: 0.4rem;
1359  margin: 0;
1360  padding: 0.45rem 0.85rem;
1361  font-family: var(--cn-font);
1362  font-size: 11px;
1363  font-weight: 600;
1364  letter-spacing: 0.1em;
1365  text-transform: uppercase;
1366  color: var(--cn-ink-muted);
1367  background: var(--cn-paper);
1368  border: 1px solid var(--cn-rule-strong);
1369  border-radius: var(--cn-radius);
1370  box-shadow: none;
1371  cursor: pointer;
1372  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1373}
1374
1375.cookie-notice .pe-btn:hover {
1376  color: var(--cn-ink);
1377  background: var(--cn-paper-elevated);
1378  border-color: rgba(44, 42, 39, 0.22);
1379}
1380
1381.cookie-notice .pe-btn--primary {
1382  color: var(--cn-ink);
1383  background: var(--cn-paper);
1384  border-color: var(--cn-rule-strong);
1385}
1386
1387.cookie-notice .pe-btn--primary:hover {
1388  color: var(--cn-ink);
1389  background: var(--cn-paper-elevated);
1390  border-color: rgba(44, 42, 39, 0.22);
1391}
1392
1393#hydraDoNotSellLink {
1394  --cn-paper: #f6f5f1;
1395  --cn-paper-elevated: #fdfcfa;
1396  --cn-ink: #2c2a27;
1397  --cn-ink-muted: #6b6760;
1398  --cn-rule-strong: rgba(44, 42, 39, 0.16);
1399  --cn-radius: 3px;
1400  --cn-font: "DM Sans", system-ui, -apple-system, sans-serif;
1401
1402  position: fixed;
1403  bottom: 10px;
1404  left: 12px;
1405  z-index: 9999;
1406  display: inline-flex;
1407  align-items: center;
1408  justify-content: center;
1409  margin: 0;
1410  padding: 0.45rem 0.85rem;
1411  font-family: var(--cn-font);
1412  font-size: 11px;
1413  font-weight: 600;
1414  letter-spacing: 0.1em;
1415  text-transform: uppercase;
1416  color: var(--cn-ink-muted);
1417  background: var(--cn-paper-elevated);
1418  border: 1px solid var(--cn-rule-strong);
1419  border-radius: var(--cn-radius);
1420  box-shadow: 0 2px 8px rgba(44, 42, 39, 0.06);
1421  cursor: pointer;
1422}
1423
1424#hydraDoNotSellLink.hidden {
1425  display: none;
1426}
1427
1428#hydraDoNotSellLink:hover {
1429  color: var(--cn-ink);
1430  background: var(--cn-paper);
1431  border-color: rgba(44, 42, 39, 0.22);
1432}
1433
1434/* FOUC reveal (paired with early <head> html:not(.hydra-ready) { opacity: 0 }) */
1435html.hydra-ready {
1436  opacity: 1;
1437  transition: opacity 0.2s ease;
1438}
1439/* Keep body class for any legacy selectors; visibility no longer used (submenu CSS pierces it) */
1440body.hydra-ready {
1441  visibility: visible;
1442}
1443
1444/* Admin panel typography */
1445.sidebar-element-form,
1446.sidebar-element-form *,
1447.add-form,
1448.add-form *,
1449.metadata-editor,
1450.metadata-editor * {
1451  font-family: "Hanken Grotesk", sans-serif;
1452  font-weight: 200;
1453  letter-spacing: 0.02em;
1454}
1455
1456.sidebar-element-form h3,
1457.sidebar-element-form label,
1458.add-form label,
1459.metadata-editor h3,
1460.metadata-editor label {
1461  text-transform: uppercase;
1462  letter-spacing: 0.04em;
1463  font-weight: 300;
1464}
1465
1466.sidebar-element-form .form-actions .btn,
1467.sidebar-element-form button,
1468.add-form .form-actions .btn,
1469.add-form button,
1470.metadata-editor .form-actions .btn,
1471.metadata-editor button {
1472  text-transform: uppercase;
1473  letter-spacing: 0.08em;
1474  font-weight: 300;
1475}
1476
1477.sidebar-element-form input,
1478.sidebar-element-form select,
1479.sidebar-element-form textarea,
1480.sidebar-element-form button,
1481.add-form input,
1482.add-form select,
1483.add-form textarea,
1484.add-form button,
1485.metadata-editor input,
1486.metadata-editor select,
1487.metadata-editor textarea,
1488.metadata-editor button {
1489  font-family: "Hanken Grotesk", sans-serif;
1490  font-weight: 200;
1491  letter-spacing: 0.02em;
1492  border-radius: 0;
1493}
1494</style>
1495
1496<script>
1497// Flag to prevent duplicate initialization
1498window.hydraInitialized = false;
1499
1500function ensureAdminPanelFont() {
1501  const head = document.head || document.getElementsByTagName('head')[0];
1502  if (!head) {
1503    return;
1504  }
1505
1506  const linkDefinitions = [
1507    {
1508      id: 'admin-panels-font-preconnect-googleapis',
1509      rel: 'preconnect',
1510      href: 'https://fonts.googleapis.com'
1511    },
1512    {
1513      id: 'admin-panels-font-preconnect-gstatic',
1514      rel: 'preconnect',
1515      href: 'https://fonts.gstatic.com',
1516      crossOrigin: 'anonymous'
1517    },
1518    {
1519      id: 'admin-panels-font-stylesheet',
1520      rel: 'stylesheet',
1521      href: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,200;1,200&display=swap'
1522    },
1523    {
1524      id: 'admin-panels-font-barlow',
1525      rel: 'stylesheet',
1526      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'
1527    }
1528  ];
1529
1530  for (const definition of linkDefinitions) {
1531    if (!document.getElementById(definition.id)) {
1532      const link = document.createElement('link');
1533      link.id = definition.id;
1534      link.rel = definition.rel;
1535      link.href = definition.href;
1536
1537      if (definition.crossOrigin) {
1538        link.crossOrigin = definition.crossOrigin;
1539      }
1540
1541      head.appendChild(link);
1542    }
1543  }
1544}
1545
1546ensureAdminPanelFont();
1547
1548 window.Parameters = window.Parameters || {};
1549  window.Parameters.siteId = "52octf5t";
1550  window.Parameters.SiteId = "52octf5t"; // Also set capitalized version
1551  window.siteId = "52octf5t";
1552  window.TURNSTILE_SITE_KEY = "0x4AAAAAADyHXlv6RTUIaDOp";
1553  window.NEON_SKY_FILES = window.NEON_SKY_FILES || {};
1554  window.NEON_SKY_FILES.QUILL_INTEGRATION = 'quill-integration-v260923-002.js';
1555  window.NEON_SKY_FILES.SITE_THEMES = 'site-themes-v260923-002.css';
1556  window.NEON_SKY_FILES.SITE_THEMES_MANIFEST = 'site-themes-v260923-002.json';
1557
1558// Initialize menu styles from server
1559document.addEventListener('DOMContentLoaded', function() {
1560// Apply initial menu styles from server
1561const initialMenuStyles = {"fontFamily":"'Times New Roman', serif","fontSize":"15px","color":"#878787","activeColor":"#ffffff","hoverColor":"#ffffff","verticalSpacing":"2px","menuBackgroundColor":"#000000","menuBackgroundHidden":false,"siteBackgroundColor":"#000000","siteBackgroundUseGradient":false,"siteGradientRandomizeOnLoad":false,"artPaletteSelection":null,"menuBackgroundImageUrl":"","siteBackgroundImageUrl":"","mobileHeaderHeight":"60px","sidebarWidth":"19vw","sidebarPaddingTop":"20px","sidebarPaddingRight":"0px","sidebarPaddingBottom":"0px","sidebarPaddingLeft":"0px","menuLayout":"sidebar","horizontalMenuAlignment":"left","horizontalHeaderLogoLayout":"inline","horizontalHeaderShadow":true,"disableRightClick":false,"showCookieNotice":false,"cookieNoticeText":"This website uses cookies to ensure you get the best experience on our website.","privacyPolicyUrl":"https://davidcarsonphotos.
1561com/cookies","alignElementsAtBottom":true,"customCSS":"","customJS":"","siteThemeId":null,"siteThemeOptions":{},"fontName":"Times New Roman"};
1562window.__hydraStyleFromQuery = null;
1563const initialSiteThemeRuntimeVars = null;
1564
1565// Set CSS custom properties
1566// ?hydra-style= preview: apply merged menuStyles (skip site-theme runtime vars that would override).
1567if (initialSiteThemeRuntimeVars && !window.__hydraStyleFromQuery) {
1568  Object.keys(initialSiteThemeRuntimeVars).forEach(function(key) {
1569    document.documentElement.style.setProperty(key, initialSiteThemeRuntimeVars[key]);
1570  });
1571  if (initialMenuStyles && initialMenuStyles.verticalSpacing) {
1572    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1573  }
1574  // Match menu chrome / Gallery Options overview preview (light weight)
1575  document.documentElement.style.setProperty('--menu-font-weight', '200');
1576  if (initialMenuStyles && typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1577    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1578  }
1579} else if (initialMenuStyles) {
1580  if (initialMenuStyles.fontFamily) {
1581    document.documentElement.style.setProperty('--menu-font-family', initialMenuStyles.fontFamily);
1582  }
1583  if (initialMenuStyles.fontSize) {
1584    document.documentElement.style.setProperty('--menu-font-size', initialMenuStyles.fontSize);
1585  }
1586  // Only set when Secondary was explicitly saved; otherwise descriptions follow --menu-font-size
1587  if (typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') {
1588    document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim());
1589  }
1590  document.documentElement.style.setProperty('--menu-font-weight', '200');
1591  if (initialMenuStyles.color) {
1592    document.documentElement.style.setProperty('--menu-color', initialMenuStyles.color);
1593  }
1594  if (initialMenuStyles.activeColor) {
1595    document.documentElement.style.setProperty('--menu-active-color', initialMenuStyles.activeColor);
1596  }
1597  if (initialMenuStyles.hoverColor) {
1598    document.documentElement.style.setProperty('--menu-hover-color', initialMenuStyles.hoverColor);
1599  }
1600  // Background colors must be applied here too: MenuStyleCustomizer may load late/fail
1601  // after CDN→proxy retry, and CSS defaults are white/#fafafa.
1602  if (initialMenuStyles.siteBackgroundColor) {
1603    document.documentElement.style.setProperty('--site-background-color', initialMenuStyles.siteBackgroundColor);
1604  }
1605  if (initialMenuStyles.menuBackgroundColor && !initialMenuStyles.menuBackgroundHidden) {
1606    document.documentElement.style.setProperty('--menu-background-color', initialMenuStyles.menuBackgroundColor);
1607  }
1608  if (initialMenuStyles.verticalSpacing) {
1609    document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing);
1610  }
1611  if (initialMenuStyles.menuBackgroundImageUrl && String(initialMenuStyles.menuBackgroundImageUrl).trim() !== '') {
1612    var menuBgImg = String(initialMenuStyles.menuBackgroundImageUrl).trim();
1613    document.documentElement.style.setProperty('--menu-background-image', 'url(' + JSON.stringify(menuBgImg) + ')');
1614  }
1615  var resolvedSiteBg = '';
1616  if (window.SiteGradientGenerator && typeof window.SiteGradientGenerator.resolveSiteBackgroundImage === 'function') {
1617    resolvedSiteBg = window.SiteGradientGenerator.resolveSiteBackgroundImage(initialMenuStyles, { allowRandomize: true });
1618  } else if (initialMenuStyles.siteBackgroundImageUrl) {
1619    resolvedSiteBg = String(initialMenuStyles.siteBackgroundImageUrl).trim();
1620  }
1621  if (resolvedSiteBg) {
1622    var peIsCssGradient = String(resolvedSiteBg).toLowerCase().indexOf('gradient(') !== -1;
1623    var siteBgImgCss = peIsCssGradient
1624      ? resolvedSiteBg
1625      : 'url(' + JSON.stringify(resolvedSiteBg) + ')';
1626    document.documentElement.style.setProperty('--site-background-image', siteBgImgCss);
1627    var siteBgSize = 'cover';
1628    if (peIsCssGradient) {
1629      siteBgSize = window.SiteGradientGenerator && window.SiteGradientGenerator.gradientBackgroun
1629dSize
1630        ? window.SiteGradientGenerator.gradientBackgroundSize(resolvedSiteBg)
1631        : '100% 100%';
1632      document.body.classList.add('has-site-background-gradient');
1633    }
1634    document.documentElement.style.setProperty('--site-background-size', siteBgSize);
1635  }
1636  if (initialMenuStyles.mobileHeaderHeight && String(initialMenuStyles.mobileHeaderHeight).trim() !== '') {
1637    var mhh0 = String(initialMenuStyles.mobileHeaderHeight).trim();
1638    /* 0 / 0vh / 0px collapse logo max-height calcs; use the same floor as getUserHorizontalHeaderHeightPx */
1639    var mhhParsed = parseInt(mhh0, 10);
1640    if (Number.isNaN(mhhParsed) || mhhParsed < 1) {
1641      mhh0 = '60px';
1642    }
1643    document.documentElement.style.setProperty('--mobile-header-height', mhh0);
1644    document.documentElement.style.setProperty('--content-margin-top', mhh0);
1645    document.documentElement.style.setProperty('--top-bar-height-setting', mhh0);
1646  }
1647}
1648
1649if (initialMenuStyles && initialMenuStyles.menuBackgroundHidden) {
1650  document.body.classList.add('menu-background-hidden');
1651  document.documentElement.style.setProperty('--menu-background-color', 'transparent');
1652  document.documentElement.style.setProperty('--menu-background-image', 'none');
1653}
1654
1655if (initialMenuStyles && initialMenuStyles.menuTextUppercase) {
1656  document.body.classList.add('menu-text-uppercase');
1657  if (!document.getElementById('menu-text-uppercase-style')) {
1658    var menuUpperStyle = document.createElement('style');
1659    menuUpperStyle.id = 'menu-text-uppercase-style';
1660    menuUpperStyle.textContent = '.menu-item,.horizontal-menu-label,a.horizontal-menu-label{text-transform:uppercase!important;font-weight:200!important;}';
1661    document.head.appendChild(menuUpperStyle);
1662  }
1663}
1664
1665// Store initial styles in MenuStyleCustomizer if it exists
1666if (window.MenuStyleCustomizer) {
1667  window.MenuStyleCustomizer.settings = {
1668    ...window.MenuStyleCustomizer.defaults,
1669    ...initialMenuStyles
1670  };
1671}
1672});
1673</script>
vendor: 1 bytes, line 1673
1673
1674<script>
1675let magic = null;
1676let userMetadata = null;
1677let isAuthenticated = false;
1678let isAdmin = false;
1679let didToken = null;
1680let siteId = "52octf5t";
1681
1682/** Clear Hydra auth tokens/flags from this browser (used by logout button and ?logout=1 links). */
1683function clearHydraClientAuthStorage() {
1684  try {
1685    localStorage.removeItem('hydra_is_admin');
1686    localStorage.removeItem('hydra_auth_token');
1687    localStorage.removeItem('hydra_auth_email');
1688    localStorage.removeItem('hydra_did_token');
1689    localStorage.removeItem('hydra_auth_site');
1690    localStorage.removeItem('hydra_auth_last_refresh');
1691    localStorage.removeItem('hydra_auth_auto_refresh');
1692  } catch (e) {}
1693  if (window.TokenManager && typeof window.TokenManager.clearToken === 'function') {
1694    try { window.TokenManager.clearToken(); } catch (e) {}
1695  }
1696  isAuthenticated = false;
1697  isAdmin = false;
1698  didToken = null;
1699  userMetadata = null;
1700  window.isAuthenticated = false;
1701  window.isAdmin = false;
1702  window.didToken = null;
1703}
1704
1705function hydraUrlRequestsLogout() {
1706  try {
1707    var params = new URLSearchParams(window.location.search);
1708    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1709    if (params.get('logout') === '1' || params.get('logout') === 'true') return true;
1710    if (params.get('hydra_logout') === '1' || params.get('hydra_logout') === 'true') return true;
1711    return path === '/_manage/logout';
1712  } catch (e) {
1713    return false;
1714  }
1715}
1716
1717function hydraCleanLogoutFromUrl() {
1718  try {
1719    var params = new URLSearchParams(window.location.search);
1720    var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/';
1721    var hadQuery = params.has('logout') || params.has('hydra_logout');
1722    params.delete('logout');
1723    params.delete('hydra_logout');
1724    var nextPath = path === '/_manage/logout' ? '/_manage' : window.location.pathname;
1725    var qs = params.toString();
1726    var next = nextPath + (qs ? '?' + qs : '') + (window.location.hash || '');
1727    if (hadQuery || path === '/_manage/logout') {
1728      history.replaceState({}, '', next);
1729    }
1730  } catch (e) {}
1731}
1732
1733(function hydraLogoutFromUrlEarly() {
1734  if (!hydraUrlRequestsLogout()) return;
1735  clearHydraClientAuthStorage();
1736  try { sessionStorage.setItem('hydra_logout_cleared', '1'); } catch (e) {}
1737  hydraCleanLogoutFromUrl();
1738  console.log('[Hydra] Auth cleared via logout link/query.');
1739})();
1740
1741function hydraUrlRequestsRefresh() {
1742  try {
1743    var params = new URLSearchParams(window.location.search);
1744    var v = params.get('refresh');
1745    return v === '1' || v === 'true';
1746  } catch (e) {
1747    return false;
1748  }
1749}
1750
1751function hydraClearGalleryClientCache() {
1752  try {
1753    Object.keys(localStorage).filter(function (key) {
1754      return key.includes('gallery') || key.includes('neon') || key.includes('lightbox');
1755    }
1755).forEach(function (key) {
1756      localStorage.removeItem(key);
1757    });
1758  } catch (e) {}
1759  try {
1760    delete window.__neonGalleryMainBootstrapWipeDone;
1761  } catch (e) {
1762    window.__neonGalleryMainBootstrapWipeDone = false;
1763  }
1764  window._forceGalleryReload = true;
1765}
1766
1767function hydraCleanRefreshFromUrl() {
1768  try {
1769    var params = new URLSearchParams(window.location.search);
1770    if (!params.has('refresh')) return;
1771    params.delete('refresh');
1772    var qs = params.toString();
1773    var next = window.location.pathname + (qs ? '?' + qs : '') + (window.location.hash || '');
1774    history.replaceState({}, '', next);
1775  } catch (e) {}
1776}
1777
1778(function hydraRefreshFromUrlEarly() {
1779  if (!hydraUrlRequestsRefresh()) return;
1780  clearHydraClientAuthStorage();
1781  hydraClearGalleryClientCache();
1782  try { sessionStorage.setItem('hydra_refresh_cleared', '1'); } catch (e) {}
1783  hydraCleanRefreshFromUrl();
1784  console.log('[Hydra] Gallery cache/auth cleared via ?refresh=1 — forcing fresh load.');
1785})();
1786
1787// State Management — site-wide KV metadata for the editor (not per-page SEO)
1788window.siteMetadata = {"title":"Simple Image Page","description":"Davis Carson, Photojournalist","keywords":"","googleAnalytics":"","noIndex":false,"favicon":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","author":"davidcarsonphotos.
1788com","language":"en","locale":"en_US","siteName":"davidcarsonphotos.com","ogImageUrl":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","facebookPixel":""};
1789console.log('Client Initialized site metadata:', window.siteMetadata);
1790
1791/** Match SSR title format: "{Gallery Title} | {Site Title}" */
1792function formatGalleryDocumentTitle(galleryTitle, win) {
1793  win = win || window;
1794  var meta = win.siteMetadata;
1795  var siteTitle = (meta && meta.title && String(meta.title).trim()) || win.location.hostname;
1796  return (galleryTitle || 'Gallery') + ' | ' + siteTitle;
1797}
1798
1799console.log('Client Google Analytics from metadataToUse:', '');
1800console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics);
1801console.log('Client Content Language:', window.siteMetadata.contentLanguage);
1802
1803// State Management
1804let galleries = [{"id":1772567355125,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true,"isHomePage":false},{"id":1772567361348,"title":"Spacer-copy","parentId":null,"visible":true,"isSpacer":true,"slug":"spacer-copy","url":"/spacer-copy","isHomePage":false,"position":1},{"id":1772565718760,"title":"Intro","url":"/intro","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"z2hg701f","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"intro","normalizedName":"intro","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"z2hg701f","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}}
1804,"siteAlias":"davidcarsonphotos.com","initialPageUuid":"z2hg701f","isHydra":true,"flyIoSaveTimestamp":1786937971591,"txId":"OZv46CLax9fEIMM2yLZOzCt8Z3QvFrtuDBnex7nBucQ","permaURL":"OZv46CLax9fEIMM2yLZOzCt8Z3QvFrtuDBnex7nBucQ","isPerma":false,"uploadedTo":["fly","irys","cdn"],"fileSize":1820,"initialPageAlias":"intro","timestamp":1786937969147,"startInSingles":true,"layoutSelection":"singles","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":true,"autoplayDuration":"4","autoplayTransition":"1.5","showDescription":false,"descriptionsOnDemand":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":"singles","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"timestampsMatch":false,"allTimestampsMatch":false,"cdnNdjsonId":"79906437e8884709893c661400f670b4","cdnNdjsonUrl":"https://cdn.neonsky.app/79906437e8884709893c661400f670b4.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_z2hg701f.json","cdnSaveTimestamp":1786937976414},"isHomePage":true,"position":2,"siteAlias":null,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false},{"id":1772565719676,"title":"Ferguson Uprising, Michael Brown","url":"/ferguson-michael-brown","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"h4vs00r2","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"ferguson-michael-brown","normalizedName":"ferguson,-michael-brown","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"h4vs00r2","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"h4vs00r2","isHydra":true,"flyIoSaveTimestamp":1772697331598,"txId":"faHhq9MjCWysxaltoziPvuh7XAk0jFirXkmI7XWOQE0","permaURL":"faHhq9MjCWysxaltoziPvuh7XAk0jFirXkmI7XWOQE0","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":18831,"initialPageAlias":"ferguson,-michael-brown","timestamp":1772697329572,"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":true,"descriptionsOnDemand":true,"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":"
1804center","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},"irysSaveTimestamp":1772697331598,"timestampsMatch":false,"allTimestampsMatch":false},"siteAlias":null,"isHomePage":false,"position":3,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false},{"id":1772565721063,"title":"Life","url":"/life","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"x5vv9kzu","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"life","normalizedName":"life","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"x5vv9kzu","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Scroll Horizontally--&gt;</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"x5vv9kzu","isHydra":true,"flyIoSaveTimestamp":1772565722095,"txId":"0TJktT9UWzJb_GkFCYUMWjx5mrpUlHzG0FaMGHctIGc","permaURL":"0TJktT9UWzJb_GkFCYUMWjx5mrpUlHzG0FaMGHctIGc","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":6406,"initialPageAlias":"life","timestamp":1772690762890,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"isHomePage":false,"position":5,"siteAlias":null},{"id":1772565722209,"title":"News","url":"/news","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"3ap0yl43","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"news","normalizedName":"news","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"3ap0yl43","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"3ap0yl43","isHydra":true,"flyIoSaveTimestamp":1772690668558,"txId":"Rf2y2HM69QiqfOTmCYLWS-8PwVrT0DuWVflYrV-WjmE","permaURL":"Rf2y2HM69QiqfOTmCYLWS-8PwVrT0DuWVflYrV-WjmE","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":6489,"initialPageAlias":"news","timestamp":1772690666391,"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,"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":"
1804center","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},"irysSaveTimestamp":1772690668558,"timestampsMatch":false,"allTimestampsMatch":false},"isHomePage":false,"position":6,"siteAlias":null},{"id":1772565723282,"title":"Sports","url":"/sports","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"qayurpwu","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"sports","normalizedName":"sports","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"qayurpwu","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}}
1804,"siteAlias":"davidcarsonphotos.com","initialPageUuid":"qayurpwu","isHydra":true,"flyIoSaveTimestamp":1788833179075,"txId":"hue2k314ECWhtMhW7KVOlD7TQ-ejFXiaqU8QcLB8W9g","permaURL":"hue2k314ECWhtMhW7KVOlD7TQ-ejFXiaqU8QcLB8W9g","isPerma":false,"uploadedTo":["fly","irys","cdn"],"fileSize":3600,"initialPageAlias":"sports","timestamp":1788833176896,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"irysSaveTimestamp":1772696911082,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"alignSmallGalleriesVertically":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showGridPricing":false,"showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"c834124e51374bdbad09ae1f32657324","cdnNdjsonUrl":"https://cdn.neonsky.app/c834124e51374bdbad09ae1f32657324.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_qayurpwu.json","cdnSaveTimestamp":1788833181464},"isHomePage":false,"position":7,"siteAlias":null},{"id":1772565724362,"title":"Portraits & Illustrations","url":"/portraits-&-illustrations","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"2u8sjkcu","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"portraits-&-illustrations","normalizedName":"portraits-&-illustrations","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"2u8sjkcu","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"2u8sjkcu","isHydra":true,"flyIoSaveTimestamp":1772697457366,"txId":"CU8iF4QhYA3wEkf5Tjg9qXov680BpM4AMXg28mGUMAs","permaURL":"CU8iF4QhYA3wEkf5Tjg9qXov680BpM4AMXg28mGUMAs","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":3367,"initialPageAlias":"portraits-&-illustrations","timestamp":1772697717297,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":1,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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":"
1804center","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":1,"useRowsLayout":false,"itemsPerRow":2},"irysSaveTimestamp":1772697457366,"timestampsMatch":false,"allTimestampsMatch":false},"siteAlias":null,"isHomePage":false,"position":8},{"id":1772565707963,"title":"Projects And Stories","isFolder":true,"isCollapsed":true,"visible":true,"parentId":null,"siteId":"52octf5t","slug":"projects-and-stories","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":9},{"id":1772565707965,"title":"Heroin in St. Louis","url":"/heroin-in-st.-louis","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"p8ypi3dw","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"heroin-in-st.-louis","normalizedName":"heroin-in-st.-louis","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"p8ypi3dw","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"p8ypi3dw","isHydra":true,"flyIoSaveTimestamp":1772565710464,"txId":"ELFX5c5BFC4BgxN0LdXadmNzRKppGlnJYfbKOMDIMNA","permaURL":"ELFX5c5BFC4BgxN0LdXadmNzRKppGlnJYfbKOMDIMNA","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":3307,"initialPageAlias":"heroin-in-st.-louis","timestamp":1772566872634,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"siteAlias":null,"isHomePage":false,"position":10},{"id":1772565711619,"title":"Death on the rails","url":"/death-on-the-rails","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"8psky2bf","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"death-on-the-rails","normalizedName":"death-on-the-rails","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"8psky2bf","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Each year about 500 people are killed walking the nation's railroad tracks. It is the leading cause of death on railroads, more common than motorists killed at crossings or workers killed in accidents. Walking on the tracks is trespassing, but little is done to stop people from crossing or walking along the tracks. The 3-day series \"Death on the Rails\" by the St. Louis Post-Dispatch and stltoday.com examines pedestrian deaths on railroads and how the industry fights regulation that could prevent many incidents.</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"8psky2bf","isHydra":true,"flyIoSaveTimestamp":1772565712919,"txId":"AZnsVO6ex_VVB0x6F9HC2cUbzfM0FbaVkgkQK16g3Lc","permaURL":"AZnsVO6ex_VVB0x6F9HC2cUbzfM0FbaVkgkQK16g3Lc","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":3912,"initialPageAlias":"death-on-the-rails","timestamp":1772566909239,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":11},{"id":1772565710567,"title":"St. Louis, guns, shootings and crimes","url":"/st.-louis,-guns,-shootings-and-crimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"6axn7ko8","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"st.-louis,-guns,-shootings-and-crimes","normalizedName":"st.-louis,-guns,-shootings-and-crimes","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"6axn7ko8","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"6axn7ko8","isHydra":true,"flyIoSaveTimestamp":1772694942553,"txId":"brLUgvAUNvoskgxY2zOudVt-DcidhdUPgbsJfbEqnc8","permaURL":"brLUgvAUNvoskgxY2zOudVt-DcidhdUPgbsJfbEqnc8","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":11114,"initialPageAlias":"st.-louis,-guns,-shootings-and-crimes","timestamp":1772694940419,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"irysSaveTimestamp":1772694942553,"timestampsMatch":false,"allTimestampsMatch":false},"siteAlias":null,"isHomePage":false,"position":12},{"id":1772565714048,"title":"Afghanistan, October 2006","url":"/afghanistan,-october-2006","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"qkk8n04w","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"afghanistan,-october-2006","normalizedName":"afghanistan,-october-2006","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"qkk8n04w","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Afghanistan Oct. 2006</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"qkk8n04w","isHydra":true,"flyIoSaveTimestamp":1772565715332,"txId":"mnnanRJZzMx5tG75pwYLOukx7y1qfLU6DHob7kMb9pE","permaURL":"mnnanRJZzMx5tG75pwYLOukx7y1qfLU6DHob7kMb9pE","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":1745,"initialPageAlias":"afghanistan,-october-2006","timestamp":1772566944338,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":13},{"id":1772565715439,"title":"Iraq, September 2004","url":"/iraq,-september-2004","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"isawjjvs","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"iraq,-september-2004","normalizedName":"iraq,-september-2004","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"isawjjvs","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Iraq, Sept. 2004</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"isawjjvs","isHydra":true,"flyIoSaveTimestamp":1772565716471,"txId":"qsL7UTQXCrB-jvrQx8R101op4CDWhW5Tb6mSqUoicII","permaURL":"qsL7UTQXCrB-jvrQx8R101op4CDWhW5Tb6mSqUoicII","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":606,"initialPageAlias":"iraq,-september-2004","timestamp":1772567016136,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":14},{"id":1772565713024,"title":"Dominician baseball","url":"/dominician-baseball","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"fwy81c3o","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"dominician-baseball","normalizedName":"dominician-baseball","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"fwy81c3o","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">St. Louis Cardinals Dominican baseball academy.</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"fwy81c3o","isHydra":true,"flyIoSaveTimestamp":1772565713942,"txId":"uRx84C3Ati6R8zqFrFPCqO3zMRIDQDYdgwe8QRilH34","permaURL":"uRx84C3Ati6R8zqFrFPCqO3zMRIDQDYdgwe8QRilH34","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":554,"initialPageAlias":"dominician-baseball","timestamp":1772566929369,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":15},{"id":1772565716579,"title":"Oliver, $6.50 an hour cop","url":"/oliver,-$6.50-an-hour-cop","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"v8rv72s5","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"oliver,-$6.50-an-hour-cop","normalizedName":"oliver,-$6.50-an-hour-cop","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"v8rv72s5","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"v8rv72s5","isHydra":true,"flyIoSaveTimestamp":1772565717528,"txId":"M_zsP24JKTZgPspIsxqnNPsREQYgQ_8Jo5exTGNLNJE","permaURL":"M_zsP24JKTZgPspIsxqnNPsREQYgQ_8Jo5exTGNLNJE","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":372,"initialPageAlias":"oliver,-$6.50-an-hour-cop","timestamp":1772567040504,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"siteAlias":null,"isHomePage":false,"position":16},{"id":1772565717637,"title":"Gephardt's last run","url":"/gephardt's-last-run","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"n81q08tw","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"gephardt's-last-run","normalizedName":"gephardt's-last-run","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"n81q08tw","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"n81q08tw","isHydra":true,"flyIoSaveTimestamp":1772565718650,"txId":"wgtrY82DCDnbL4G1dwcIC-2YdQK8lqf3SMTn1qKg_CQ","permaURL":"wgtrY82DCDnbL4G1dwcIC-2YdQK8lqf3SMTn1qKg_CQ","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":601,"initialPageAlias":"gephardt's-last-run","timestamp":1772567057902,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"siteAlias":null,"isHomePage":false,"position":17},{"id":1772860236672,"title":"Multimedia","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"4u3wa6ug","visible":true,"parentId":null,"siteId":"52octf5t","slug":"multimedia-copy","url":"/multimedia-copy","pageElements":[{"id":1772860236597,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Simple Image Page","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"},{"id":1772860238581,"type":"column-container","title":"Columns Copy","visible":true,"position":1,"columns":[{"id":1772813417004,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813446837,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/lfmXWnoxbUs","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813421664,"hAlign":"left","vAlign":"top","elements":[{"id":1746063362628,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Death on the Rails</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(161, 161, 161);\">Each year about 500 people are killed walking the nation's railroad tracks. It is the leading cause of death on railroads, more common than motorists killed at crossings or workers killed in accidents. Walking on the tracks is trespassing, but little is done to stop people from crossing or walking along the tracks. This video is part of the 3-day series \"Death on the Rails\" by the St. Louis Post-Dispatch and stltoday.com that examines pedestrian deaths on railroads.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860240000,"type":"text","title":"Text Block Copy","visible":true,"position":2,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772862132747,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":3,"columns":[{"id":1772862129649,"hAlign":"right","vAlign":"middle","elements":[{"id":1772862130186,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/2gNvKl9-ATM","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772862128061,"hAlign":"left","vAlign":"top","elements":[{"id":1772862134367,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 24px;\">History of Corvette manufacturing in St. Louis, Mo. and the move to Bowling Green, Ky.</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">The world-famous Chevrolet Corvette was built in St. Louis </span><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">for almost 28 years, from late 1953 (for the 1954 model year) until 1981.&nbsp;Former St. Louis General Motors employees who built the Corvette at the assembly plant on Union Boulevard talk about being forced to transfer to Bowling Green to keep their jobs.</span></p><p style=\"line-height: 1;\"><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}
1804,{"id":1772862283234,"type":"text","title":"Text Block Copy Copy","visible":true,"position":4,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860262981,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":5,"columns":[{"id":1772860260199,"hAlign":"right","vAlign":"middle","elements":[{"id":1772860266820,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/dEQ0qerf4U8","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772860259698,"hAlign":"left","vAlign":"top","elements":[{"id":1772860268892,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px;\">Five Alarm Warehouse Fire in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">One person was injured in a massive, wind-fueled fire that ripped through a vacant warehouse on St. Louis' riverfront.&nbsp;The five-alarm blaze in the five-story building collapsed in stages as the fire consumed the building, leaving nothing but a pile of rubble. Firefighters hosed down nearby buildings to keep the flames from spreading to nearby warehouses. The warehouse that burned was more than 100 years old.</span></p><p><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772863619228,"type":"text","title":"Text Block Copy Copy Copy","visible":true,"position":6,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772863622552,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":7,"columns":[{"id":1772863621621,"hAlign":"right","vAlign":"middle","elements":[{"id":1772863622688,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://vimeo.com/1171261180?share=copy&fl=sv&fe=ci","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772863622235,"hAlign":"left","vAlign":"top","elements":[{"id":1772863614564,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 24px;\">Heroin Deaths Surge in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">Richard Skinner, a longtime heroin user, talks about the drug’s grip on him and how heroin, increasingly cut with cheap synthetic fentanyl, has made it far more dangerous.</span></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">St. Louis city recorded 256 overdose deaths in 2017, a staggering 94 percent increase from the previous year. Opioid deaths surged past homicides and the city’s overdose rate, 80 deaths per 100,000 people, is more than double the rate elsewhere in the region. The broader St. Louis region recorded more than 650 opioid overdoses last year.</span></p><p style=\"line-height: 1;\"><br></p><p><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772867957150,"type":"text","title":"Text Block Copy Copy Copy Copy","visible":true,"position":8,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772867993768,"type":"column-container","title":"Columns Copy Copy Copy Copy","visible":true,"position":9,"columns":[{"id":1772867990527,"hAlign":"right","vAlign":"middle","elements":[{"id":1772867999813,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/kBf131UKbiE","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772867994747,"hAlign":"left","vAlign":"top","elements":[{"id":1772867994876,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 24px;\">Mother Rushes to Find Daughter After School Shooting</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">After a shooting at Central Visual &amp; Performing Arts High School in St. Louis, Takisha Duncan hurries up an alley to reach her child, Taniya Lumpkin, at the evacuation point on the Schnucks parking lot on Monday, Oct. 24, 2022. The incident began when a gunman, a former student, with an AR-15 rifle and multiple high-capacity magazines, entered the school and started shooting. The gunman shot and killed a teenage girl and a health teacher and injured several others before police shot and killed him in a classroom.</span></p><p style=\"line-height: 1;\"><br></p><p><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860238104,"type":"text","title":"Text Block Copy Copy","visible":true,"position":10,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860244080,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":11,"columns":[{"id":1772856055495,"hAlign":"right","vAlign":"middle","elements":[{"id":1772856057186,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/CbC3vUK-5ew","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772856052076,"hAlign":"left","vAlign":"top","elements":[{"id":1772856056420,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"color: rgb(161, 161, 161); font-size: 24px;\">Bear in Residential Neighborhood Causes Stir</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">Neighbors react to the excitement of a 150-pound juvenile black bear that wandered into Richmond Heights and climbed up a tree after roaming for days through suburban St. Louis County. The bear was tranquilized by the Missouri Department of Conservation, which said the 1.5 to 2-year-old bear will be taken far out into the country, checked for vital signs and then released when he awakens.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772862270867,"type":"text","title":"Text Block Copy Copy","visible":true,"position":12,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860238675,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":13,"columns":[{"id":1772813839175,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813837584,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=VEj_gB9XIBI","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813840171,"hAlign":"left","vAlign":"top","elements":[{"id":1772813834818,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">9-11 Tenth Anniversary Memorial on Art Hill in St. Louis</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Two hundred and sixty volunteers swarmed Art Hill on Saturday morning staking 2996 flags into the ground. On Sunday three different memorial services at the site will mark the 10th anniversary of 9/11 terrorist attacks.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}
1804,{"id":1772860244884,"type":"text","title":"Text Block Copy","visible":true,"position":14,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860235877,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":15,"columns":[{"id":1772813989112,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813984493,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=SWe62PmH-X8&list=UUUBk1Oye72NSo44VJD7ePAA480","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813992713,"hAlign":"left","vAlign":"top","elements":[{"id":1772813984588,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Haunt Show in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Scenes from the 17th Annual TransWorld Halloween and Attractions Show, a trade show for those in the haunted house business, that was held at the America's Center March 8-11, 2012.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860241243,"type":"text","title":"Text Block Copy","visible":true,"position":16,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860241945,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":17,"columns":[{"id":1772814135498,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814136753,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=6L545mec9DQ","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814135007,"hAlign":"left","vAlign":"top","elements":[{"id":1772814133296,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Skateboarding 2-year-old</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Benjamin Lomax, 2, first began skate boarding about a year ago when his father Johnnie Lomax put him on a board. At the skate park cries of \"look at him\" come rolling out of the crowd gathered along the sides.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860238191,"type":"text","title":"Text Block Copy","visible":true,"position":18,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860236882,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":19,"columns":[{"id":1772814229438,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814231068,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=Ss-HBoyh8iM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814227991,"hAlign":"left","vAlign":"top","elements":[{"id":1772814227602,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Funeral burnouts in honor of murder victim, Hawk Point, Missouri</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Hundreds of friends and family members of Jeffrey \"Smitty\" Smith showed up at his funeral in their hot rods and muscle cars. The mourners stopped in front of his business, where he was murdered, on their way to the cemetery to do burnouts in honor of Smith.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}
1804,{"id":1772860243802,"type":"text","title":"Text Block Copy","visible":true,"position":20,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860244770,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":21,"columns":[{"id":1772814358037,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814354749,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=OPAyZ-ATScA&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814359131,"hAlign":"left","vAlign":"top","elements":[{"id":1772814353720,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Cardinals Fans Give Advice to Cubs Fans</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">St. Louis Cardinals fans have revelled for years in the Chicago Cubs fruitless flailings at trying to win the World Series. While the Cardinals have 10 world series the Cubs are on a 100 year drought. Hear what advice Cardinals fans would give to the Cubs and their fans to end their drought.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860243221,"type":"text","title":"Text Block Copy","visible":true,"position":22,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860238816,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":23,"columns":[{"id":1772814715830,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814711090,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=nj7xPdt3byM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814710542,"hAlign":"left","vAlign":"top","elements":[{"id":1772814709960,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Time lapse of photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">EARTH CITY, MO --Time lapse of a photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson at Rams Park in Earth City, Mo.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}],"isHomePage":false,"position":18,"metaTitle":"a","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false},{"id":1772689890484,"title":"Fur, Feathers and Friends","parentId":null,"visible":true,"isIntegrated":true,"pageId":"nzdjrvjz","slug":"page-1772690596779","url":"/page-1772690596779","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"nzdjrvjz","siteId":"52octf5t","siteAlias":"davidcarsonphotos.
1804com","initialPageUuid":"nzdjrvjz","initialPageAlias":"page-1772690596779","timestamp":1788833460542,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"y_S_XHB56yOWngDNFkLWjjZI-M0xQ0wTOB9A0Td8U-c","txId":"y_S_XHB56yOWngDNFkLWjjZI-M0xQ0wTOB9A0Td8U-c","flyIoSaveTimestamp":1788833463321,"irysSaveTimestamp":1772695351614,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"alignSmallGalleriesVertically":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showGridPricing":false,"showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"064647d93c0c484cabc81f9a628b8036","cdnNdjsonUrl":"https://cdn.neonsky.app/064647d93c0c484cabc81f9a628b8036.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_nzdjrvjz.json","cdnSaveTimestamp":1788833464820},"siteId":"52octf5t","siteAlias":null,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false,"isHomePage":false,"isPage":false,"position":19},{"id":1772687280697,"title":"View From Above","parentId":null,"visible":true,"isIntegrated":true,"pageId":"df5cx0ld","slug":"view-from-above","url":"/view-from-above","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"df5cx0ld","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"df5cx0ld","initialPageAlias":"view-from-above","timestamp":1772691423497,"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":true,"descriptionsOnDemand":true,"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":"
1804center","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},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"permaURL":"vhd1K6SOuTZYkm7cSkGsBKKU4FuOpTg2QUMxbdqf3_w","isPerma":false,"txId":"vhd1K6SOuTZYkm7cSkGsBKKU4FuOpTg2QUMxbdqf3_w","flyIoSaveTimestamp":1772691425978,"irysSaveTimestamp":1772691425978,"timestampsMatch":false,"allTimestampsMatch":false},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":20},{"id":1772688050373,"title":"Sun, Moon and Stars","parentId":null,"visible":true,"isIntegrated":true,"pageId":"zxhv7umx","slug":"sun-moon-and-stars","url":"/sun-moon-and-stars","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"zxhv7umx","siteId":"52octf5t","siteAlias":"davidcarsonphotos.
1804com","initialPageUuid":"zxhv7umx","initialPageAlias":"sun-moon-and-stars","timestamp":1788833318853,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"Z9mZThb_4MNTw5-IuwN81sPxK6wdEww7fkCgLZcgmSg","txId":"Z9mZThb_4MNTw5-IuwN81sPxK6wdEww7fkCgLZcgmSg","flyIoSaveTimestamp":1788833321220,"irysSaveTimestamp":1772695482773,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"alignSmallGalleriesVertically":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showGridPricing":false,"showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"ee88e1a8e8ee4fec803b26434a6ad266","cdnNdjsonUrl":"https://cdn.neonsky.app/ee88e1a8e8ee4fec803b26434a6ad266.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_zxhv7umx.json","cdnSaveTimestamp":1788833322805},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":21},{"id":1772690804259,"title":"COVID Lockdown Life","parentId":null,"visible":false,"isIntegrated":true,"pageId":"pgtosxqy","slug":"covid-lockdown-life","url":"/covid-lockdown-life","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"pgtosxqy","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"pgtosxqy","initialPageAlias":"covid-lockdown-life","timestamp":1772691907792,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"5Y0lvcEoYib9iBp53S9C00I2zoBNWV0wGhOLT4ZxSbo","txId":"5Y0lvcEoYib9iBp53S9C00I2zoBNWV0wGhOLT4ZxSbo","flyIoSaveTimestamp":1772691910042,"irysSaveTimestamp":1772691910042,"timestampsMatch":false,"allTimestampsMatch":false},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":22},{"id":1772694181029,"title":"Weather","parentId":null,"visible":false,"isIntegrated":true,"pageId":"xwg9wr15","slug":"weather","url":"/weather","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"xwg9wr15","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"xwg9wr15","initialPageAlias":"weather","timestamp":1772697260945,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"pLSfRoQ1g9UI4nSWyDizcnDYfUxLxlVxAbm9j2AY4fs","txId":"pLSfRoQ1g9UI4nSWyDizcnDYfUxLxlVxAbm9j2AY4fs","flyIoSaveTimestamp":1772697018478,"irysSaveTimestamp":1772697018478,"timestampsMatch":false,"allTimestampsMatch":false},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":23},{"id":1772567277641,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true,"isHomePage":false,"position":24},{"id":1772565727781,"title":"Bio","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772565727781","visible":true,"parentId":null,"siteId":"52octf5t","slug":"bio","url":"/bio","pageElements":[{"id":1772565728781,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Bio","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw"},{"id":1772565730781,"type":"column-container","title":"Two Column Layout","visible":true,"position":1,"columns":[{"id":1772565731081,"hAlign":"right","vAlign":"middle","elements":[{"id":1772565730881,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ec04c2f91/images/avaanddad.jpg","imageKey":"4bd5ec04c2f91/images/avaanddad.jpg","imageFilename":"avaanddad.jpg","imageType":"image/jpeg","caption":"","title":"","byline":"","dateline":"","altText":""}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1772565731181,"hAlign":"left","vAlign":"middle","elements":[{"id":1772565730981,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">David Carson is a Pulitzer Prize-winning photojournalist with more than three decades of experience covering local communities, national stories and international events. He has been a staff photojournalist at the St. Louis Post-Dispatch since 2000 and was a 2025 John S. Knight Journalism Fellow at Stanford University. In 2026, he was inducted into the St. Louis Media History Hall of Fame. </span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">Carson began his journalism career at 16, when his mother drove him to his first newspaper assignment because he wouldn’t have his driver’s license for another two months. A Boston-area native, he worked at the Providence Journal-Bulletin, Naples Daily News and as a freelance photographer in New England before joining the Post-Dispatch.</span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">During his career, Carson has covered wars in Iraq and Afghanistan, the aftermath of the September 11th terrorist attacks in New York, presidential debates, multiple World Series, a Super Bowl and some of the biggest stories in the St. Louis region. His photographs were featured extensively in the Post-Dispat
1804ch photo staff’s coverage of the protests in Ferguson, Missouri, which received the 2015 Pulitzer Prize for Breaking News Photography. In 2009, he was part of the team named a finalist for the Pulitzer Prize in Breaking News Reporting for their coverage of the Kirkwood City Hall shooting. </span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\"> His work has also been recognized by Pictures of the Year International, the National Press Photographers Association, the National Headliner Awards, Best of Photojournalism, Time magazine’s Top 100 Photos of the Year and he was awarded a regional Emmy for Advanced Media-Interactivity for his multimedia project Reporting for Duty. </span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">As a John S. Knight Journalism Fellow at Stanford, Carson focused on artificial intelligence, image authentication and the ethics of emerging visual technology. His work included exploring the C2PA standard and Content Credentials as tools for building public trust in news photography.</span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">He frequently speaks at universities, journalism conferences and professional organizations about photojournalism, artificial intelligence, image authenticity, ethics and the future of visual journalism.</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><br></p>","textWidth":88,"fadeInOnScroll":false}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false,"randomStart":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoUrls":[],"randomizeVideo":false,"videoHeight":50}}],"classicGuid":"4bd5ec04c2f91","importSource":"classic-page","isHomePage":false,"position":25},{"id":1786937796125,"title":"Bio-copy","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"ucqgj4jf","visible":false,"parentId":null,"siteId":"52octf5t","slug":"bio-copy","url":"/bio-copy","pageElements":[{"id":1786937803028,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Bio","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw"},{"id":1786937799233,"type":"column-container","title":"Two Column Layout","visible":true,"position":1,"columns":[{"id":1772565731081,"hAlign":"right","vAlign":"middle","elements":[{"id":1772565730881,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ec04c2f91/images/avaanddad.jpg","imageKey":"4bd5ec04c2f91/images/avaanddad.jpg","imageFilename":"avaanddad.jpg","imageType":"image/jpeg","caption":"","title":"","byline":"","dateline":"","altText":""}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1772565731181,"hAlign":"left","vAlign":"middle","elements":[{"id":1772565730981,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">David Carson is a Pulitzer Prize-winning multimedia photojournalist for the St. Louis Post-Dispatch and a 2025 John S. Knight Journalism Fellow at Stanford University.</span></p><p style=\"line-height: 1;\"><span style=\"color: rgb(143, 143, 143);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">He began his career at 16 when his mother drove him to his first newspaper assignment because he wouldn't have his license for another two months. Carson, a Boston area native, has worked at the St. Louis Post-Dispatch since 2000. Before arriving in the midwest he worked at the Naples Daily News in Florida, at the Providence Journal-Bulletin in Rho
1804de Island, and as a freelance photographer in New England.</span></p><p style=\"line-height: 1;\"><span style=\"color: rgb(143, 143, 143);\">\n</span></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">Carson’s images are featured extensively in the </span><a href=\"http://www.pulitzer.org/works/2015-Breaking-News-Photography\" target=\"_blank\" style=\"font-size: 14px; color: rgb(143, 143, 143);\">2015 Pulitzer Prize for Breaking News Photography </a><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">that was awarded to the St. Louis Post-Dispatch photo staff. He has also won numerous other awards for his work over the years. In 2008, his multimedia project \"Reporting for Duty\" won a regional Emmy award for Advanced Media – Interactivity. In 2009, he was part of a team that was recognized as a finalist for the Pulitzer Prize in the \"Breaking News Reporting\" category for their coverage of the Kirkwood City Hall shootings.  He was also named the National Press Photographers Association Region 7 Photographer of the Year in 2011, 2010 and 2009.</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">He attended the Rochester Institute of Technology, where he was a photojournalism major and an international relations minor. </span><span style=\"font-size: 14px;\">As a 2025 John S. Knight Journalism Fellow at Stanford, he was immersed in artificial intelligence, image authentication, and the ethics of emerging visual technology. </span></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><br></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">He is a frequent lecturer at universities, journalism conferences, and professional organizations. </span></p>","textWidth":86}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoUrls":[],"randomizeVideo":false,"videoHeight":50}}],"classicGuid":"4bd5ec04c2f91","importSource":"classic-page","isHomePage":false,"position":26,"children":[]},{"id":1772565727783,"title":"Contact","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772565727783","visible":true,"parentId":null,"siteId":"52octf5t","slug":"contact","url":"/contact","pageElements":[{"id":1772567207231,"type":"metadata","visible":true,"position":0,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"8vw"},{"id":1772565728683,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">David Carson is a St. Louis, Mo. based photojournalist</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">David Carson</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">St. Louis, MO</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">314.393-0502 (Cell)</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"mailto:[email protected]\" target=\"_blank\" style=\"font-size: 16px; color: rgb(163, 163, 163);\">[email protected]</a></p>","textWidth":43}],"classicGuid":"4bd5ec04c2f91","importSource":"classic-page","isHomePage":false,"position":27},{"id":1772567674097,"title":"Client area","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772567674097","visible":true,"parentId":null,"siteId":"52octf5t","slug":"david-start-here","url":"/david-start-here","pageElements":[{"id":1772567676530,"type":"metadata","visible":true,"position":1,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"3vw","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"}
1804,{"id":1772567693649,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>To log in...</p><p><br></p><p>1) Press the <span style=\"color: rgb(255, 0, 200);\"> </span><span style=\"font-size: 24px; color: rgb(255, 0, 200);\">+</span><span style=\"font-size: 24px;\"> </span> key.</p><p>2) Enter your email address.</p><p>3) the One Time Passcode that you receive...</p><p><br></p><p>...then click the Edit button in the top-left corner</p><p><br></p><p>There's a video how-to below. </p><p>Let us know if you need any help with this.</p><p><br></p><p>Jayson</p><p>Neon Sky</p><p><br></p>","textWidth":48},{"id":1772567707298,"type":"embedded-content","title":"Embed","visible":true,"position":2,"contentUrl":"https://share.descript.com/embed/FyubGmgsVIG","containerWidth":90,"containerHeight":88,"useAutoHeight":false}],"position":28,"isHomePage":false,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":true,"password":"9658"},{"id":1772565726859,"title":"Fellowship Portfolio","url":"/fellowship-portfolio","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":false,"parentId":null,"siteId":"52octf5t","pageId":"t3y74vss","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"fellowship-portfolio","normalizedName":"fellowship-portfolio","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"t3y74vss","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":false,"guid":"GUID=52octf5t","metadata":{},"totalItems":10}},"siteAlias":"preview.neonsky.app","initialPageUuid":"t3y74vss","isHydra":true,"flyIoSaveTimestamp":1772565727663,"txId":"cAxSrjwRuX1_0FO1z_qqDIC942S9bRkU_ww515AuaJw","permaURL":"cAxSrjwRuX1_0FO1z_qqDIC942S9bRkU_ww515AuaJw","isPerma":true,"uploadedTo":["fly","irys"],"fileSize":2909},"isHomePage":false,"position":29},{"id":1772565725557,"title":"Multimedia-old","url":"/multimedia-old","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":false,"parentId":null,"siteId":"52octf5t","pageId":"spxeiduh","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"multimedia-old","normalizedName":"multimedia","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"spxeiduh","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">click on links to see content for multimedia pieces</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"spxeiduh","isHydra":true,"flyIoSaveTimestamp":1772770756876,"txId":"j2S2vmRAyXqRpEyrbdKg6gcTr19ul9ZOdPzjnPDaN3o","permaURL":"j2S2vmRAyXqRpEyrbdKg6gcTr19ul9ZOdPzjnPDaN3o","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":2109,"initialPageAlias":"multimedia","timestamp":1772810733445,"startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":2,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":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":2,"spacing":2,"useRowsLayout":true,"itemsPerRow":2},"irysSaveTimestamp":1772770756876,"allTimestampsMatch":false,"timestampsMatch":false,"originalFileSize":16638},"siteAlias":null,"isHomePage":false,"position":30,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false},{"id":1772569890233,"title":"test","parentId":null,"visible":false,"isIntegrated":true,"pageId":"40i169eb","slug":"test","url":"/test","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"40i169eb","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"40i169eb","initialPageAlias":"test","timestamp":1772569929678,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":true,"autoplayDuration":"4","autoplayTransition":"1.5","showDescription":false,"descriptionsOnDemand":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":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"permaURL":"Y4v6fkGRKs77oMSGHkWruYb_ohIkk8xkM9SZJ0YbNUg","isPerma":false,"isHydra":true,"flyIoSaveTimestamp":1772569932458,"irysSaveTimestamp":1772569932458,"allTimestampsMatch":false,"timestampsMatch":false,"txId":"Y4v6fkGRKs77oMSGHkWruYb_ohIkk8xkM9SZJ0YbNUg","fileSize":2423,"originalFileSize":12768},"isPage":false,"position":31,"isHomePage":false},{"id":1772813312664,"title":"Multimedia-0ld 11:46 3/6","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772813312664","visible":false,"parentId":null,"siteId":"52octf5t","slug":"multimedia","url":"/multimedia","pageElements":[{"id":1772813327834,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Simple Image Page","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"},{"id":1772813323391,"type":"column-container","title":"Columns Copy","visible":true,"position":1,"columns":[{"id":1772813417004,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813446837,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/lfmXWnoxbUs","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813421664,"hAlign":"left","vAlign":"top","elements":[{"id":1746063362628,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Death on the Rails</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(161, 161, 161);\">Each year about 500 people are killed walking the nation's railroad tracks. It is the leading cause of death on railroads, more common than motorists killed at crossings or workers killed in accidents. Walking on the tracks is trespassing, but little is done to stop people from crossing or walking along the tracks. This video is part of the 3-day series \"Death on the Rails\" by the St. Louis Post-Dispatch and stltoday.com that examines pedestrian deaths on railroads.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772856352038,"type":"text","title":"Text Block Copy Copy","visible":true,"position":2,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772856050444,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":3,"columns":[{"id":1772856055495,"hAlign":"right","vAlign":"middle","elements":[{"id":1772856057186,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/CbC3vUK-5ew","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772856052076,"hAlign":"left","vAlign":"top","elements":[{"id":1772856056420,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"color: rgb(161, 161, 161); font-size: 24px;\">Bear in Residential Neighborhood Causes Stir</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">Neighbors react to the excitement of a 150-pound juvenile black bear that wandered into Richmond Heights and climbed up a tree after roaming for days through suburban St. Louis County. The bear was tranquilized by the Missouri Department of Conservation, which said the 1.5 to 2-year-old bear will be taken far out into the country, checked for vital signs and then released when he awakens.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814949380,"type":"text","title":"Text Block Copy","visible":true,"position":4,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772815213424,"type":"text","title":"Text Block Copy","visible":true,"position":5,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772813838190,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":6,"columns":[{"id":1772813839175,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813837584,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=VEj_gB9XIBI","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813840171,"hAlign":"left","vAlign":"top","elements":[{"id":1772813834818,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">9-11 Tenth Anniversary Memorial on Art Hill in St. Louis</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Two hundred and sixty volunteers swarmed Art Hill on Saturday morning staking 2996 flags into the ground. On Sunday three different memorial services at the site will mark the 10th anniversary of 9/11 terrorist attacks.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814986349,"type":"text","title":"Text Block Copy","visible":true,"position":7,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772813990990,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":8,"columns":[{"id":1772813989112,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813984493,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=SWe62PmH-X8&list=UUUBk1Oye72NSo44VJD7ePAA480","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813992713,"hAlign":"left","vAlign":"top","elements":[{"id":1772813984588,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Haunt Show in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Scenes from the 17th Annual TransWorld Halloween and Attractions Show, a trade show for those in the haunted house business, that was held at the America's Center March 8-11, 2012.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814982198,"type":"text","title":"Text Block Copy","visible":true,"position":9,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814139628,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":10,"columns":[{"id":1772814135498,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814136753,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=6L545mec9DQ","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814135007,"hAlign":"left","vAlign":"top","elements":[{"id":1772814133296,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Skateboarding 2-year-old</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Benjamin Lomax, 2, first began skate boarding about a year ago when his father Johnnie Lomax put him on a board. At the skate park cries of \"look at him\" come rolling out of the crowd gathered along the sides.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814978159,"type":"text","title":"Text Block Copy","visible":true,"position":11,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814230805,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":12,"columns":[{"id":1772814229438,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814231068,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=Ss-HBoyh8iM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814227991,"hAlign":"left","vAlign":"top","elements":[{"id":1772814227602,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Funeral burnouts in honor of murder victim, Hawk Point, Missouri</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Hundreds of friends and family members of Jeffrey \"Smitty\" Smith showed up at his funeral in their hot rods and muscle cars. The mourners stopped in front of his business, where he was murdered, on their way to the cemetery to do burnouts in honor of Smith.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814968187,"type":"text","title":"Text Block Copy","visible":true,"position":13,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814360182,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":14,"columns":[{"id":1772814358037,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814354749,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=OPAyZ-ATScA&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814359131,"hAlign":"left","vAlign":"top","elements":[{"id":1772814353720,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Cardinals Fans Give Advice to Cubs Fans</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">St. Louis Cardinals fans have revelled for years in the Chicago Cubs fruitless flailings at trying to win the World Series. While the Cardinals have 10 world series the Cubs are on a 100 year drought. Hear what advice Cardinals fans would give to the Cubs and their fans to end their drought.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814957757,"type":"text","title":"Text Block Copy","visible":true,"position":15,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814711852,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":16,"columns":[{"id":1772814715830,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814711090,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=nj7xPdt3byM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814710542,"hAlign":"left","vAlign":"top","elements":[{"id":1772814709960,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Time lapse of photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">EARTH CITY, MO --Time lapse of a photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson at Rams Park in Earth City, Mo.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}],"metaTitle":"a","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false,"isHomePage":false,"position":32},{"id":1784494636274,"title":"cookies","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1784494636274","visible":false,"parentId":null,"siteId":"52octf5t","slug":"cookies","url":"/cookies","pageElements":[{"id":1784494650001,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""},{"id":1784494656939,"type":"column-container","title":"Columns Copy","visible":true,"position":1,"leftColumnElements":[{"id":1746063362551,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","imageUrl":"https://storage.neonsky.app/06imfsl5/1746063380838_heic2007a.jpg","imageKey":"06imfsl5/1746063380838_heic2007a.jpg","imageFilename":"heic2007a.jpg","imageType":"image/jpeg"}],"rightColumnElements":[{"id":1746063362628,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\">Lorem ipsum dolor sit amet. <span style=\"color: rgb(168, 168, 168);\">Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. </span></p>","textWidth":70}]},{"id":1784494654903,"type":"column-container","title":"Columns Copy","visible":true,"position":2,"leftColumnElements":[{"id":1746063327303,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\">Lorem ipsum dolor sit amet. <span style=\"color: rgb(168, 168, 168);\">Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. </span></p>","textWidth":70}],"rightColumnElements":[{"id":1746063325497,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","imageUrl":"https://storage.neonsky.app/06imfsl5/1746063346838_heic0905a.jpg","imageKey":"06imfsl5/1746063346838_heic0905a.jpg","imageFilename":"heic0905a.jpg","imageType":"image/jpeg"}]},{"id":1784494649926,"type":"column-container","title":"Columns","visible":true,"position":3,"leftColumnElements":[{"id":1746063080696,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"86%","imageAlignment":"center","imageUrl":"https://storage.neonsky.app/rgttx9o6/1745782782090_heic0604a.jpg","imageKey":"rgttx9o6/1745782782090_heic0604a.jpg","imageFilename":"heic0604a.jpg","imageType":"image/jpeg"}],"rightColumnElements":[{"id":1746063085138,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p class=\"ql-align-center\">Lorem ipsum dolor sit amet. <span style=\"color: rgb(168, 168, 168);\">Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. </span></p>","textWidth":70}]}],"position":33,"isHomePage":false}];
1805window.galleries = galleries;
1806
1807function getSiteGalleriesArray() {
1808  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
1809    return galleries;
1810  }
1811  if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
1812    return window.galleries;
1813  }
1814  if (window.GALLERY_DATA_CACHE &&
1815      window.GALLERY_DATA_CACHE.initialized &&
1816      Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
1817    return window.GALLERY_DATA_CACHE.galleries;
1818  }
1819  return [];
1820}
1821window.getSiteGalleriesArray = getSiteGalleriesArray;
1822
1823let activeGalleryId = null;
1824let sortableInstance = null;
1825let isEditing = false;
1826let initialPageInfo = {"id":1772565721063,"type":"gallery","pageId":"x5vv9kzu","slug":"life","url":"/life"};
1827
1828
1829// Store the current menu layout type (must be JSON-encoded — bare sidebar/horizontal is invalid JS)
1830let currentMenuLayout = null;
1831
1832/** True when this document is loaded in the chromeless iframe (?neon_embed=1 or body.neon-page-embed). */
1833function isNeonPageEmbedShell() {
1834  try {
1835    if (typeof document !== 'undefined' && document.body && document.body.classList.contains('neon-page-embed'))
1836      return true;
1837    var _spEmbed = new URLSearchParams(window.location.search);
1838    return _spEmbed.get('neon_embed') === '1';
1839  } catch (_eEmbed) { return false; }
1840}
1841
1842/**
1843 * Mirrors "hide menu on this page": in embed shells always hide menus (unless editing),
1844 * otherwise use each gallery/page flag.
1845 */
1846function shouldApplyMenuHiddenForEmbedOrGallery(gallery) {
1847  var editing = typeof window.isInEditMode === 'function' && window.isInEditMode();
1848  if (editing)
1849    return !!(gallery && gallery.hideMenuOnPage);
1850  return !!(gallery && gallery.hideMenuOnPage) || isNeonPageEmbedShell();
1851}
1852
1853function shouldApplyMenuOverlayOnPage(gallery) {
1854  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery))
1855    return false;
1856  return !!(gallery && gallery.overlayMenuOnPage);
1857}
1858
1859function isOverlayMenuChromeActive() {
1860  return document.body.classList.contains('menu-overlay-on-page')
1861    && !document.body.classList.contains('edit-mode-active');
1862}
1863
1864/** Style Customizer paints sidebar with inline background-color !important; beat that only while overlay is active. */
1865function syncOverlayMenuInlineChrome() {
1866  var sidebar = document.querySelector('.sidebar');
1867  var header = document.querySelector('.mobile-header');
1868  if (isOverlayMenuChromeActive()) {
1869    if (sidebar) {
1870      sidebar.style.setProperty('background-color', 'transparent', 'important');
1871      sidebar.style.setProperty('background-image', 'none', 'important');
1872    }
1873    if (header) {
1874      header.style.setProperty('background-color', 'transparent', 'important');
1875      header.style.setProperty('background-image', 'none', 'important');
1876    }
1877    return;
1878  }
1879  if (sidebar) {
1880    var menuBg = (getComputedStyle(document.documentElement).getPropertyValue('--menu-background-color') || '').trim() || '#fafafa';
1881    var hidden = document.body.classList.contains('menu-background-hidden');
1882    sidebar.style.setProperty('background-color', hidden ? 'transparent' : menuBg, 'important');
1883    if (hidden) {
1884      sidebar.style.setProperty('background-image', 'none', 'important');
1885    } else {
1886      sidebar.style.removeProperty('background-image');
1887    }
1888  }
1889  if (header) {
1890    header.style.removeProperty('background-color');
1891    header.style.removeProperty('background-image');
1892  }
1893}
1894
1895function applyMenuOverlayClassForGallery(gallery) {
1896  if (shouldApplyMenuOverlayOnPage(gallery)) {
1897    document.body.classList.add('menu-overlay-on-page');
1898  } else {
1899    document.body.classList.remove('menu-overlay-on-page');
1900  }
1901  syncOverlayMenuInlineChrome();
1902}
1903
1904function hookMenuStyleCustomizerForOverlay() {
1905  var customizer = window.MenuStyleCustomizer;
1906  if (!customizer || customizer.__overlayBgHooked || typeof customizer.applyStyles !== 'function')
1907    return;
1908  var original = customizer.applyStyles.bind(customizer);
1909  customizer.applyStyles = function() {
1910    original.apply(this, arguments);
1911    if (isOverlayMenuChromeActive())
1912      syncOverlayMenuInlineChrome();
1913  };
1914  customizer.__overlayBgHooked = true;
1915}
1916window.applyMenuOverlayClassForGallery = applyMenuOverlayClassForGallery;
1917
1918document.addEventListener('DOMContentLoaded', () => {
1919  if (window.hydraInitialized) {
1920    console.log("Hydra already initialized, skipping");
1921    return;
1922  }
1923  window.hydraInitialized = true;
1924  document.body.classList.add('hydra-initialized');
1925  try {
1926    if (sessionStorage.getItem('hydra_refresh_cleared') === '1') {
1927      sessionStorage.removeItem('hydra_refresh_cleared');
1928      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
1929        window.EnhancedGallerySystem.resetInitializationState();
1930      }
1931      console.log('[Hydra] Applied ?refresh=1 — gallery framework reset for clean load.');
1932    }
1933  } catch (e) {}
1934  try {
1935    var neonEmbedParams = new URLSearchParams(window.location.search);
1936    if (neonEmbedParams.get('neon_embed') === '1') {
1937      document.body.classList.add('neon-page-embed', 'menu-hidden-on-page');
1938    }
1939  } catch (_neonEmbedErr) {}
1940  hookMenuStyleCustomizerForOverlay();
1941  setTimeout(hookMenuStyleCustomizerForOverlay, 0);
1942  setTimeout(hookMenuStyleCustomizerForOverlay, 500);
1943  if (isOverlayMenuChromeActive())
1944    syncOverlayMenuInlineChrome();
1945  window.Parameters = window.Parameters || {};
1946  if (localStorage.getItem('hydra_is_admin') === 'true') {
1947    document.body.classList.add('hydra-admin', 'hydra-authenticated');
1948    isAdmin = true;
1949    isAuthenticated = true;
1950    console.log('Restored admin status from localStorage');
1951    
1952    // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins
1953    // This ensures admins always see fresh data when opening new tabs
1954    try {
1955      console.log('Admin detected - clearing all caches except auth tokens...');
1956      
1957      // Save auth tokens first
1958      const savedAuthToken = localStorage.getItem('hydra_auth_token');
1959      const savedAuthEmail = localStorage.getItem('hydra_auth_email');
1960      const savedIsAdmin = localStorage.getItem('hydra_is_admin');
1961      const savedDidToken = localStorage.getItem('hydra_did_token');
1962      
1963      // Clear ALL localStorage
1964      localStorage.clear();
1965      
1966      // Restore auth tokens
1967      if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken);
1968      if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail);
1969      if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin);
1970      if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken);
1971      
1972      // Clear ALL sessionStorage (auth tokens are not stored here)
1973      sessionStorage.clear();
1974      
1975      console.log('Cleared all caches except auth tokens for admin user');
1976    } catch (cacheError) {
1977      console.warn('Error clearing caches on admin page load:', cacheError);
1978      // Don't fail page load if cache clearing fails
1979    }
1980  }
1981
1982  function ensureSidebarElementsAndRender() {
1983  console.log('Ensuring sidebar elements are available before rendering...');
1984  
1985  // Restore from siteConfig when hydrate missed or init invented a default menu
1986  if (window.SidebarManager) {
1987    var current = window.SidebarManager.elements;
1988    var missingOrDefault = !current || current.length === 0 || window.SidebarManager._createdDefaultElements;
1989    if (missingOrDefault && typeof siteConfig !== 'undefined' && siteConfig.sidebarElements && siteConfig.sidebarElements.length > 0) {
1990      console.log('Initializing SidebarManager elements from siteConfig');
1991      window.SidebarManager.elements = siteConfig.sidebarElements;
1992      window.SidebarManager._elementsReady = true;
1993      window.SidebarManager._createdDefaultElements = false;
1994      if (typeof window.SidebarManager.renderElements === 'function') {
1995        window.SidebarManager.renderElements();
1996      }
1997    }
1998  }
1999  
2000  // Determine layout and render
2001  const currentMenuLayout = getEffectiveMenuLayout();
2002  
2003  if (currentMenuLayout === 'horizontal') {
2004    console.log('Rendering horizontal menu with ensured sidebar elements');
2005    renderHorizontalMenu();
2006  } else {
2007    renderGalleries();
2008  }
2009}
2010
2011 setTimeout(() => {
2012  console.log("Initializing galleries:", galleries.length);
2013  // Collapse folders before first paint so nested items never flash as expanded plain text
2014  alignFolderStates();
2015  
2016  // CHANGED: Use the new function instead of direct rendering
2017  setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init
2018  
2019  initMobileMenu();
2020  
2021  /** If true, deep-link timeouts below must not call handleURLNavigation again (duplicate loadPage / blink). */
2022  let bootstrapRouteAttempted = false;
2023
2024  // Handle direct URL navigation
2025  if (initialPageInfo) {
2026    console.log("Loading initial page from direct URL:", initialPageInfo);
2027    
2028    // Find the gallery by ID
2029    const gallery = galleries.find(g => g.id === initialPageInfo.id);
2030    
2031    if (gallery) {
2032      // Set active gallery ID
2033      activeGalleryId = gallery.id;
2034      
2035      if (initialPageInfo.type === 'page') {
2036        console.log("Loading page directly:", gallery.title);
2037        loadPage(gallery.id);
2038      } else {
2039        console.log("Loading gallery directly:", gallery.title);
2040        loadGallery(gallery.id);
2041      }
2042      bootstrapRouteAttempted = true;
2043    }
2044  } else if (window.location.pathname === '/') {
2045    // No specific path, try to load home page
2046    console.log("No specific path, checking for home page");
2047    if (!loadHomePage() && galleries.length > 0) {
2048      // Default to first gallery if no home page is set
2049      loadGallery(galleries[0].id);
2050    }
2051    bootstrapRouteAttempted = true;
2052  } else if (galleries.length > 0) {
2053    // Path exists but no matching initialPageInfo, try to handle URL path
2054    handleURLNavigation();
2055    bootstrapRouteAttempted = true;
2056  }
2057  window.__hydraBootstrapRouteAttempted = bootstrapRouteAttempted;
2058  
2059  // Log what was rendered
2060  console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found");
2061  
2062    // Start with edit controls properly set
2063    toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active'));
2064    
2065    // Explicitly hide the import classic form too
2066    const importForm = document.getElementById('importClassicForm');
2067    if (importForm) {
2068      importForm.style.display = 'none';
2069      importForm.classList.remove('visible');
2070    }
2071    
2072    // Try to restore token from localStorage
2073    if (!didToken) {
2074      didToken = localStorage.getItem('hydra_auth_token');
2075      if (didToken) {
2076        console.log('Restored auth token from localStorage');
2077      }
2078    }
2079    
2080    // Initialize Magic and check authentication
2081    
2082    
2083    // Initialize sidebar elements if available
2084    if (window.SidebarManager) {
2085      // Pass the initial elements from server config if available
2086      if (true) {
2087        window.SidebarManager.elements = [{"id":1772567313574,"type":"image","title":"Image","visible":true,"position":0,"imageData":"https://storage.neonsky.app/52octf5t/1772567326035_david-c.png","imageWidth":"82%","imageAlignment":"left","linkType":"internal","linkPageId":"1772565718760","linkUrl":"","linkTarget":"_self","linkAutoAdvance":false,"hideFromMobileMenu":true},{"id":1772565640360,"type":"menu","title":"Menu","visible":true,"position":1},{"id":1772567429855,"type":"social","title":"Social","visible":true,"position":2,"socialIconSize":20,"socialIcons":[{"type":"x","url":"https://twitter.com/pdpj"},{"type":"instagram","url":"https://www.instagram.com/photocarson/"},{"type":"linkedin","url":"https://www.linkedin.com/in/david-carson-628a596a/"},{"type":"email","url":"https://[email protected]"}],"socialAlignment":"left"}];
2088        if (window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2089          window.SidebarManager._elementsReady = true;
2090          window.SidebarManager._createdDefaultElements = false;
2091        }
2092      }
2093      window.SidebarManager.init();
2094    }
2095
2096
2097    // Initialize metadata from server config (site-wide only — not current page SEO)
2098window.siteMetadata = {"title":"Simple Image Page","description":"Davis Carson, Photojournalist","keywords":"","googleAnalytics":"","noIndex":false,"favicon":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","author":"davidcarsonphotos.com","language":"en","locale":"en_US","siteName":"davidcarsonphotos.com","ogImageUrl":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","facebookPixel":""};
2099console.log('Client Initialized site metadata (second init):', window.siteMetadata);
2100console.log('Client Google Analytics from metadataToUse (second init):', '');
2101console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics);
2102console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage);
2103
2104    // Update copyright footer after metadata is set 
2104(important for main domain)
2105    if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
2106      window.SidebarManager.updateMetadataFooter();
2107    }
2108
2109
2110
2111    const submenuCheck = document.getElementById('createSubmenu');
2112    const submenuTitleField = document.getElementById('submenuTitle');
2113    const submenuTitleGroup = submenuTitleField?.parentElement;
2114    
2115    if (submenuCheck && submenuTitleGroup) {
2116      submenuCheck.addEventListener('change', function() {
2117        submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
2118      });
2119    }
2120    
2121    // Check authentication state after initialization
2122    if (isAuthenticated && isAdmin) {
2123      console.log('Already authenticated and admin, showing edit UI');
2124      forceShowEditUI();
2125    } else if (didToken) {
2126      console.log('Have token, checking admin status');
2127      checkAuth();
2128    }
2129  }, 100); // Small delay to ensure everything is loaded
2130
2131  setTimeout(() => {
2132    if (window.location.pathname === '/' ||
2133        window.location.pathname === '/_manage' || window.location.pathname === '/_manage/' ||
2134        (window.location.hostname === 'preview.neonsky.app' && 
2135         window.location.pathname.split('/').length <= 2)) {
2136      // Root URL or preview URL with only GUID - check for home page
2137      // Skip if bootstrap already routed (themed sites boot slower and would hit a duplicate load)
2138      if (!initialPageInfo && !window.__hydraBootstrapRouteAttempted) {
2139        loadHomePage();
2140      }
2141    }
2142  }, 500);
2143
2144  // Dedicated editor URL: open edit tools or OTP login without requiring + key
2145  setTimeout(function hydraManageEditorEntry() {
2146    var p = window.location.pathname;
2147    if (p !== '/_manage' && p !== '/_manage/') return;
2148    try {
2149      if (sessionStorage.getItem('hydra_logout_cleared') === '1') {
2150        sessionStorage.removeItem('hydra_logout_cleared');
2151        console.log('[Hydra] Logout link cleared auth; skipping auto-login on /_manage.');
2152        return;
2153      }
2154    } catch (e) {}
2155    if (document.body.classList.contains('edit-mode-active')) return;
2156    try {
2157      var token = window.didToken || localStorage.getItem('hydra_auth_token');
2158      var admin = window.isAdmin === true || localStorage.getItem('hydra_is_admin') === 'true';
2159      if (token && admin) {
2160        if (typeof window.toggleEditMode === 'function') window.toggleEditMode();
2161        return;
2162      }
2163      if (token && typeof checkAdminStatus === 'function') {
2164        checkAdminStatus(token).then(function() {
2165          if (window.isAdmin && typeof window.toggleEditMode === 'function' && !document.body.classList.contains('edit-mode-active')) {
2166            window.toggleEditMode();
2167          }
2168        }).catch(function() {});
2169        return;
2170      }
2171      if (typeof startOTPLogin === 'function') {
2172        var pr = startOTPLogin();
2173        if (pr && typeof pr.catch === 'function') pr.catch(function() {});
2174      }
2175    } catch (e) {
2176      console.error('hydraManageEditorEntry:', e);
2177    }
2178  }, 2200);
2179});
2180
2181/**
2182 * Accessible name for header/sidebar brand images (no visual change).
2183 * Prefers element title or site metadata over generic "Image" / filenames.
2184 */
2185function resolveBrandLogoAlt(element) {
2186  if (window.SidebarManager && typeof window.SidebarManager._resolveBrandImageAlt === 'function') {
2187    return window.SidebarManager._resolveBrandImageAlt(element || {});
2188  }
2189  const siteTitle =
2190    (window.siteMetadata && String(window.siteMetadata.title || '').trim()) ||
2191    (document.title && document.title.includes('|') ? document.title.split('|').pop().trim() : '') ||
2192    String(location.hostname || '').replace(/^www\./i, '');
2193  const title = element && element.title ? String(element.title).trim() : '';
2194  if (title && !/^image(\s*\d*)?$/i.test(title)) return title;
2195  return siteTitle || 'Site logo';
2196}
2197
2198/**
2199 * Gets the first visible sidebar element (image or text) to be used as a logo or title.
2200 * @returns {object|null} The first suitable sidebar element or null.
2201 */
2202
2203function getFirstSidebarElementForHeader() {
2204    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2205        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
2206        if (visibleElements.length > 0) {
2207            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
2208            
2209            // Debug: Log all element types found
2210            const elementTypes = sortedElements.map(el => ({ type: el.type, id: el.id, title: el.title }));
2211            console.log('[getFirstSidebarElementForHeader] Visible elements found:', elementTypes);
2212            
2213            for (let el of sortedElements) {
2214                if (el.type === 'image' || el.type === 'text') { // Filter 2: type
2215                    console.log('[getFirstSidebarElementForHeader] Found matching element:', { type: el.type, id: el.id, title: el.title, linkType: el.linkType, linkUrl: el.linkUrl, linkPageId: el.linkPageId });
2216                    return el;
2217                }
2218            }
2219            // If loop finishes, no visible image or text found among visible elements
2220            console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text. Element types found:', elementTypes.map(e => e.type));
2221            return null;
2222        } else {
2223            console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements. Total elements:', window.SidebarManager.elements.length);
2224            return null;
2225        }
2226    }
2227    console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
2228    return null;
2229}
2230
2231/**
2232 * Gets the first visible social icon element from sidebar elements.
2233 * @returns {object|null} The first suitable social element or null.
2234 */
2235function getFirstSocialElementForHeader() {
2236    if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2237        const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible
2238        if (visibleElements.length > 0) {
2239            const sortedElements = visibleElements.sort((a, b) => a.position - b.position);
2240            for (let el of sortedElements) {
2241                if (el.type === 'social') { // Filter for social type
2242                    return el;
2243                }
2244            }
2245            // If loop finishes, no visible social element found
2246            console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.');
2247            return null;
2248        } else {
2249            console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.');
2250            return null;
2251        }
2252    }
2253    console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.');
2254    return null;
2255}
2256
2257function isSpaceColonySiteTheme() {
2258  const themeId = document.body && document.body.dataset.siteTheme;
2259  return themeId === 'space-colony';
2260}
2261
2262/**
2263 * Apply horizontal bar logo placement (inline vs stacked) from Menu Style > Bar logo & menu.
2264 * Sets body.horizontal-header-logo-stacked and flex on .logo-menu-container; updates --mobile-header-height when stacked.
2265 */
2266function applyHorizontalLogoMenuLayoutStyles() {
2267  const sc = window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings;
2268  const mode = (sc && sc.horizontalHeaderLogoLayout) || 'inline';
2269  const isSpaceColonyTheme = isSpaceColonySiteTheme();
2270  document.body.classList.toggle('horizontal-header-logo-stacked', mode === 'stacked');
2271  const w = document.querySelector('.logo-menu-container');
2272  if (w) {
2273    const isMobile = window.innerWidth <= 768;
2274    w.style.display = 'flex';
2275    w.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : 'center';
2276    if (mode === 'stacked') {
2277      w.style.flexDirection = 'column';
2278      w.style.gap = isMobile ? '8px' : '12px';
2279      w.style.width = '100%';
2280      w.style.boxSizing = 'border-box';
2281      w.style.flexGrow = isMobile ? '0' : '1';
2282      w.style.minWidth = '0';
2283      w.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : 'flex-start';
2284    } else {
2285      w.style.flexDirection = 'row';
2286      w.style.gap = '20px';
2287      w.style.width = '';
2288      w.style.boxSizing = '';
2289      w.style.minWidth = '';
2290      if (isMobile) {
2291        w.style.flexGrow = '0';
2292        w.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
2293      } else {
2294        w.style.flexGrow = '';
2295        w.style.justifyContent = '';
2296      }
2297    }
2298  }
2299  const mhc = document.querySelector('.mobile-header-content');
2300  if (mhc) {
2301    const isMobile = window.innerWidth <= 768;
2302    if (isSpaceColonyTheme && !isMobile) {
2303      mhc.style.alignItems = 'flex-end';
2304      mhc.style.flexGrow = '0';
2305      mhc.style.width = '';
2306    } else if (isSpaceColonyTheme && isMobile) {
2307      mhc.style.alignItems = 'flex-start';
2308      mhc.style.justifyContent = 'flex-start';
2309      mhc.style.flexGrow = '';
2310      mhc.style.width = '100%';
2311    } else if (!isMobile && mode === 'stacked') {
2312      mhc.style.alignItems = 'flex-start';
2313      mhc.style.flexGrow = '';
2314      mhc.style.width = '';
2315    } else {
2316      mhc.style.alignItems = '';
2317      mhc.style.flexGrow = '';
2318      mhc.style.width = '';
2319    }
2320  }
2321  if (typeof window.syncContentMarginToHorizontalHeader === 'function') {
2322    window.syncContentMarginToHorizontalHeader();
2323  }
2324}
2325window.applyHorizontalLogoMenuLayoutStyles = applyHorizontalLogoMenuLayoutStyles;
2326
2327function parseHorizontalHeaderHeightPx(value, fallback) {
2328  if (value === null || value === undefined) {
2329    return fallback;
2330  }
2331  const parsed = parseInt(String(value).trim(), 10);
2332  if (Number.isNaN(parsed) || parsed < 1) {
2333    return fallback;
2334  }
2335  return parsed;
2336}
2337
2338function getUserHorizontalHeaderHeightPx() {
2339  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2340    const fromSetting = parseHorizontalHeaderHeightPx(window.MenuStyleCustomizer.settings.mobileHeaderHeight, 0);
2341    if (fromSetting > 0) {
2342      return fromSetting;
2343    }
2344  }
2345  const fromVar = getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height-setting').trim();
2346  const parsedVar = parseHorizontalHeaderHeightPx(fromVar, 0);
2347  if (parsedVar > 0) {
2348    return parsedVar;
2349  }
2350  const themeMin = getComputedStyle(document.documentElement).getPropertyValue('--st-header-height').trim();
2351  const parsedTheme = parseHorizontalHeaderHeightPx(themeMin, 0);
2352  if (parsedTheme > 0) {
2353    return parsedTheme;
2354  }
2355  return 60;
2356}
2357
2358function setHorizontalHeaderHeightVars(headerPx, contentMarginPx) {
2359  const headerVal = headerPx + 'px';
2360  const marginVal = contentMarginPx + 'px';
2361  const currentHeader = getComputedStyle(document.documentElement).getPropertyValue('--mobile-header-height').trim();
2362  const currentMargin = getComputedStyle(document.documentElement).getPropertyValue('--content-margin-top').trim();
2363  if (currentHeader === headerVal && currentMargin === marginVal) {
2364    return;
2365  }
2366  document.documentElement.style.setProperty('--mobile-header-height', headerVal);
2367  document.documentElement.style.setProperty('--content-margin-top', marginVal);
2368}
2369
2370function measureHorizontalHeaderNaturalHeight(header) {
2371  const el = header;
2372  const prevHeight = el.style.getPropertyValue('height');
2373  const prevMinHeight = el.style.getPropertyValue('min-height');
2374  /* Beat CSS min-height: var(--mobile-header-height) !important so a prior wrap
2375     doesn’t floor the measurement when the viewport widens back to one row. */
2376  el.style.setProperty('height', 'auto', 'important');
2377  el.style.setProperty('min-height', '0', 'important');
2378  const measured = Math.ceil(el.getBoundingClientRect().height);
2379  if (prevHeight) {
2380    el.style.setProperty('height', prevHeight);
2381  } else {
2382    el.style.removeProperty('height');
2383  }
2384  if (prevMinHeight) {
2385    el.style.setProperty('min-height', prevMinHeight);
2386  } else {
2387    el.style.removeProperty('min-height');
2388  }
2389  return measured;
2390}
2391
2392function applyHorizontalHeaderContentInset() {
2393  if (!document.body.classList.contains('menu-layout-horizontal')) {
2394    return;
2395  }
2396  if (window._horizontalHeaderInsetApplying) {
2397    return;
2398  }
2399  const header = document.querySelector('.mobile-header');
2400  if (!header) {
2401    return;
2402  }
2403
2404  window._horizontalHeaderInsetApplying = true;
2405  try {
2406    const userH = getUserHorizontalHeaderHeightPx();
2407    /* Keep logo-cap var as a real px height. Sites can save mobileHeaderHeight as 0vh;
2408       leaving that on --top-bar-height-setting makes calc(var(...) - 8px) → 0 and hides the logo. */
2409    document.documentElement.style.setProperty('--top-bar-height-setting', userH + 'px');
2410    const hasExpandedMenu = header.querySelector('.horizontal-menu-item.expanded, .horizontal-menu-item.submenu-open');
2411    const isStackedLogo = document.body.classList.contains('horizontal-header-logo-stacked');
2412    const isPhone = window.innerWidth <= 768;
2413
2414    /*
2415     * Phone hamburger chrome: fixed Style Customizer height (link row is hidden).
2416     * Desktop inline may wrap beside the logo — measure like stacked so the bar can grow.
2417     * Stacked / open submenu: always measure.
2418     */
2419    if (!hasExpandedMenu && !isStackedLogo && isPhone) {
2420      setHorizontalHeaderHeightVars(userH, userH);
2421      return;
2422    }
2423
2424    /* Desktop inline (wrap-capable), stacked logo, or open submenu */
2425    const measured = measureHorizontalHeaderNaturalHeight(header);
2426    if (!measured || measured < 1) {
2427      setHorizontalHeaderHeightVars(userH, userH);
2428      return;
2429    }
2430
2431    let maxH = Math.max(200, Math.round(window.innerHeight * 0.5));
2432    const clamped = Math.min(Math.max(measured, userH), maxH);
2433    /* Extra content margin only when bar grew past the user setting (wrapped / stacked / open menu) */
2434    const contentMargin = clamped > userH ? clamped + 8 : clamped;
2435    setHorizontalHeaderHeightVars(clamped, contentMargin);
2436  } finally {
2437    window._horizontalHeaderInsetApplying = false;
2438  }
2439}
2440
2441function ensureHorizontalHeaderHeightObserver() {
2442  if (typeof ResizeObserver === 'undefined') {
2443    return;
2444  }
2445  const header = document.querySelector('.mobile-header');
2446  if (!header) {
2447    return;
2448  }
2449  if (window._horizontalHeaderResizeObserver) {
2450    window._horizontalHeaderResizeObserver.disconnect();
2451  }
2452  window._horizontalHeaderResizeObserver = new ResizeObserver(function() {
2453    applyHorizontalHeaderContentInset();
2454  });
2455  window._horizontalHeaderResizeObserver.observe(header);
2456}
2457
2458function syncContentMarginToHorizontalHeader() {
2459  if (!document.body.classList.contains('menu-layout-horizontal')) {
2460    return;
2461  }
2462  requestAnimationFrame(function() {
2463    requestAnimationFrame(function() {
2464      applyHorizontalHeaderContentInset();
2465      ensureHorizontalHeaderHeightObserver();
2466    });
2467  });
2468}
2469window.syncContentMarginToHorizontalHeader = syncContentMarginToHorizontalHeader;
2470window.applyHorizontalHeaderContentInset = applyHorizontalHeaderContentInset;
2471
2472function getEffectiveMenuLayout() {
2473  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.getEffectiveMenuLayout === 'function') {
2474    return window.MenuStyleCustomizer.getEffectiveMenuLayout();
2475  }
2476  if (document.body.classList.contains('menu-layout-horizontal')) return 'horizontal';
2477  if (document.body.classList.contains('menu-layout-top')) return 'top';
2478  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
2479    return window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2480  }
2481  return 'sidebar';
2482}
2483
2484function ensureCorrectLayoutApplied() {
2485  // Check if MenuStyleCustomizer and its necessary parts exist
2486  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2487    // Prefer in-editor settings (e.g. Style History preview) over theme/body heuristics
2488    const fromSettings = window.MenuStyleCustomizer.settings.menuLayout;
2489    const currentLayout = fromSettings || getEffectiveMenuLayout();
2490    console.log(`Ensuring layout is applied: ${currentLayout}`);
2491
2492    // Apply the layout styles (CSS classes, display properties)
2493    window.MenuStyleCustomizer._applyMenuLayout(currentLayout);
2494
2495    // --- Force Header Structure Rebuild for Horizontal Layout ---
2496    if (currentLayout === 'horizontal') {
2497      const mobileHeader = document.querySelector('.mobile-header');
2498      if (mobileHeader) {
2499        let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2500        // Ensure the main content container exists
2501        if (!mobileHeaderContent) {
2502          mobileHeaderContent = document.createElement('div');
2503          mobileHeaderContent.className = 'mobile-header-content';
2504          const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn');
2505          if (hamburgerBtn) {
2506            mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn);
2507          } else {
2508            mobileHeader.appendChild(mobileHeaderContent);
2509          }
2510        }
2511        // Ensure specific sub-containers exist, creating them if necessary
2512        // This guarantees the structure is present before renderHorizontalMenu fills them.
2513        if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) {
2514            const logoContainer = document.createElement('div');
2515            logoContainer.className = 'horizontal-header-logo';
2516            mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild);
2517        }
2518        if (!mobileHeaderContent.querySelector('.horizontal-menu
2518-container')) {
2519            const menuContainer = document.createElement('div');
2520            menuContainer.className = 'horizontal-menu-container';
2521            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2522            if (logoContainer) {
2523                logoContainer.insertAdjacentElement('afterend', menuContainer);
2524            } else { // Fallback if logo container wasn't found/created
2525                 mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild);
2526            }
2527        }
2528        // Apply flexbox styling to position social container at far right
2529        mobileHeaderContent.style.display = 'flex';
2530        const isSpaceColonyTheme = isSpaceColonySiteTheme();
2531        const isMobile = window.innerWidth <= 768;
2532        mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2533        mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2534        if (isSpaceColonyTheme && !isMobile) {
2535          mobileHeaderContent.style.flexGrow = '0';
2536          mobileHeaderContent.style.width = '';
2537        } else {
2538          mobileHeaderContent.style.flexGrow = '';
2539          mobileHeaderContent.style.width = '100%';
2540        }
2541        
2542        // Create a flex container for logo and menu items
2543        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2544        if (!logoMenuContainer) {
2545            console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...');
2546            const logoMenuWrapper = document.createElement('div');
2547            logoMenuWrapper.className = 'logo-menu-container';
2548            // Move logo and menu into the wrapper
2549            const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2550            const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
2551            
2552            console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer);
2553            console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer);
2554            
2555            if (logoContainer && menuContainer) {
2556                // Insert wrapper before the logo
2557                mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2558                // Move both containers into the wrapper
2559                logoMenuWrapper.appendChild(logoContainer);
2560                logoMenuWrapper.appendChild(menuContainer);
2561                console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper');
2562            } else {
2563                console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper');
2564            }
2565        } else {
2566            console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists');
2567        }
2568        if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2569          applyHorizontalLogoMenuLayoutStyles();
2570        }
2571        
2572        // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created)
2573        if (!mobileHeaderContent.querySelector('.horizontal-social-container')) {
2574            const socialContainer = document.createElement('div');
2575            socialContainer.className = 'horizontal-social-container';
2576            // Insert at the end (far right) of the mobile header content
2577            mobileHeaderContent.appendChild(socialContainer);
2578        }
2579      }
2580    }
2581     // --- End Header Structure Rebuild ---
2582
2583
2584    // Re-render the correct menu structure based on the applied layout
2585    if (currentLayout === 'horizontal') {
2586      // If horizontal layout, render the horizontal menu
2587      if (typeof renderHorizontalMenu === 'function') {
2588        renderHorizontalMenu();
2589      } else {
2590        console.warn("renderHorizontalMenu function not found.");
2591      }
2592      
2593      // After rendering horizontal menu, update mobile header content
2594      // This ensures mobile header shows logo instead of menu items on mobile
2595      setTimeout(() => {
2596        if (typeof updateMobileHeaderContent === 'function') {
2597          updateMobileHeaderContent();
2598        }
2599      }, 100);
2600    } else {
2601      // For sidebar or top layouts, render the standard gallery tree
2602      if (typeof renderGalleries === 'function') {
2603        renderGalleries();
2604      } else {
2605        console.warn("renderGalleries function not found.");
2606      }
2607      
2608      // Update mobile header content for other layouts too
2609      setTimeout(() => {
2610        if (typeof updateMobileHeaderContent === 'function') {
2611          updateMobileHeaderContent();
2612        }
2613      }, 100);
2614    }
2615  } else {
2616    // Log a warning if the necessary components aren't available
2617    console.warn("MenuStyleCustomizer or its methods not available to re-apply layout.");
2618  }
2619}
2620
2621function applyLayoutStylesOnly() {
2622    // Check if MenuStyleCustomizer and its necessary parts exist
2623    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') {
2624        // Get the current layout setting, default to 'sidebar' if not set
2625        const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
2626        console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`);
2627        // Apply the layout styles (CSS classes, display properties)
2628        window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles
2629    } else {
2630        console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available.");
2631    }
2632}
2633
2634// Add this function to ensure sidebar content for mobile
2635function ensureSidebarContentForMobile() {
2636    const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
2637    const isMobileView = window.innerWidth <= 768;
2638    
2639    if (isHorizontalLayout && isMobileView) {
2640        console.log('Ensuring sidebar content for mobile horizontal layout');
2641        
2642        // Make sure the sidebar tree is populated
2643        const galleryTreeContainer = document.getElementById('galleryTree');
2644        if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) {
2645            console.log('Sidebar tree is empty, populating with galleries');
2646            
2647            // Render the galleries in the sidebar tree for mobile navigation
2648            if (typeof renderGalleries === 'function') {
2649                renderGalleries();
2650            }
2651        }
2652        
2653        // Also ensure sidebar elements are rendered
2654        if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') {
2655            console.log('Re-rendering sidebar elements for mobile');
2656            window.SidebarManager.renderElements();
2657        }
2658    }
2659}
2660
2661function renderTextLogo(container, htmlContentFromSidebarElement) {
2662  if (!container) {
2663    console.error("[RenderTextLogo] Error: Logo container not found.");
2664    return;
2665  }
2666
2667  container.innerHTML = '';
2668
2669  const raw = (htmlContentFromSidebarElement || '').trim();
2670  if (!raw) {
2671    container.innerHTML = '<span class="site-title-text">Menu</span>';
2672  } else if (!/<[a-z][sS]*>/i.test(raw)) {
2673    // Plain text — wrap so theme CSS can target .site-title-text
2674    container.innerHTML = '<span class="site-title-text">' + raw + '</span>';
2675  } else {
2676    container.innerHTML = raw;
2677    const titleEl = container.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
2678    if (titleEl && !titleEl.classList.contains('site-title-text')) {
2679      titleEl.classList.add('site-title-text');
2680    }
2681  }
2682
2683  // Do not force heading/paragraph margins to 0 — menu-styles.css matches
2684  // .sidebar-text rhythm (h1 margin-bottom 0.5em). Zeroing made the bar tighter.
2685
2686  console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`);
2687}
2688
2689function renderHorizontalMenu() {
2690  console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined');
2691  
2692  // CRITICAL: Check if we're actually in horizontal layout (theme overlay counts)
2693  const currentLayout = getEffectiveMenuLayout();
2694  
2695  const isHorizontalLayout = currentLayout === 'horizontal';
2696  console.log("[RenderHorizontalMenu] Current layout:", currentLayout, "isHorizontalLayout:", isHorizontalLayout);
2697  
2698  // If not horizontal layout, only handle logo/alignment, then return early
2699  if (!isHorizontalLayout) {
2700    console.log("[RenderHorizontalMenu] Not in horizontal layout, only handling logo/alignment, skipping menu item creation");
2701    // Still handle logo/alignment for sidebar layout
2702    const mobileHeader = document.querySelector('.mobile-header');
2703    if (mobileHeader) {
2704      const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2705      if (logoContainer) {
2706        // Handle logo rendering only (code below will handle this)
2707        // But we'll return before creating menu items
2708      }
2709    }
2710    // Return early - don't create horizontal menu items in sidebar layout
2711    return;
2712  }
2713  
2714  // Wait for sidebar elements to be available before rendering
2715  if (!window.SidebarManager || !window.SidebarManager.elements || window.SidebarManager.elements.length === 0) {
2716    console.log("[RenderHorizontalMenu] Sidebar elements not yet available, waiting...");
2717    const waitForSidebarElements = (attempts = 0) => {
2718      const maxAttempts = 20; // Increased attempts for slower loads
2719      if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
2720        console.log("[RenderHorizontalMenu] Sidebar elements now available, proceeding with render.");
2721        renderHorizontalMenu();
2722      } else if (attempts < maxAttempts) {
2723        const attemptNum = attempts + 1;
2724        setTimeout(() => waitForSidebarElements(attempts + 1), 100);
2725      } else {
2726        console.warn("[RenderHorizontalMenu] Sidebar elements not available after waiting, rendering anyway.");
2727        // Continue with render even if sidebar elements aren't loaded
2728      }
2729    };
2730    waitForSidebarElements();
2731    return;
2732  }
2733  
2734  const mobileHeader = document.querySelector('.mobile-header');
2735  if (!mobileHeader) {
2736    console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND.");
2737    return;
2738  }
2739
2740  // Apply flexbox styling to position social container at far right
2741  const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
2742  if (mobileHeaderContent) {
2743    mobileHeaderContent.style.display = 'flex';
2744    const isSpaceColonyTheme = isSpaceColonySiteTheme();
2745    const isMobile = window.innerWidth <= 768;
2746    mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center');
2747    mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between');
2748    if (isSpaceColonyTheme && !isMobile) {
2749      mobileHeaderContent.style.flexGrow = '0';
2750      mobileHeaderContent.style.width = '';
2751    } else {
2752      mobileHeaderContent.style.flexGrow = '';
2753      mobileHeaderContent.style.width = '100%';
2754    }
2755    
2756    // Create a flex container for logo and menu items
2757    const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
2758    if (!logoMenuContainer) {
2759      console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...');
2760      const logoMenuWrapper = document.createElement('div');
2761      logoMenuWrapper.className = 'logo-menu-container';
2762      // Move logo and menu into the wrapper
2763      const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo');
2764      const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu
2764-container');
2765      
2766      console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer);
2767      console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer);
2768      
2769      if (logoContainer && menuContainer) {
2770        // Insert wrapper before the logo
2771        mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer);
2772        // Move both containers into the wrapper
2773        logoMenuWrapper.appendChild(logoContainer);
2774        logoMenuWrapper.appendChild(menuContainer);
2775        console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper');
2776      } else {
2777        console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper');
2778      }
2779    } else {
2780      console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists');
2781    }
2782    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
2783      applyHorizontalLogoMenuLayoutStyles();
2784    }
2785  }
2786
2787  const logoContainer = mobileHeader.querySelector('.horizontal-header-logo');
2788  if (!logoContainer) {
2789    console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header.");
2790    return;
2791  }
2792  
2793  console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container");
2794  logoContainer.innerHTML = ''; // Clear existing logo content first
2795
2796  // FIXED: Check if we're on mobile - if so, don't render the horizontal logo
2797  const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed
2798  
2799  if (isMobileView) {
2800    console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering");
2801    // On mobile, the logo will be handled by updateMobileHeaderContent()
2802  } else {
2803    console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo");
2804    
2805    // Get the logo element data
2806    let logoElementData = null;
2807    try {
2808      logoElementData = getFirstSidebarElementForHeader();
2809      console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData));
2810    } catch (error) {
2811      console.error('[RenderHorizontalMenu] Error getting sidebar element:', error);
2812      logoElementData = getFirstSidebarElementFallback();
2813    }
2814
2815     if (logoElementData) {
2816      if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) {
2817        console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl);
2818        
2819        const img = document.createElement('img');
2820        img.src = logoElementData.imageUrl || logoElementData.imageData;
2821        img.alt = resolveBrandLogoAlt(logoElementData);
2822        img.className = 'sidebar-image';
2823        
2824        // Apply comprehensive styling to ensure visibility
2825        img.style.display = 'block';
2826        
2827        // --- FIX ---
2828        // The line below was removed. It was applying a fixed max-height of 50px,
2829        // overriding your CSS. Removing it allows your stylesheet to take control.
2830        // img.style.maxHeight = '50px'; 
2831        
2832        img.style.objectFit = 'contain';
2833        img.style.minWidth = '1px';
2834        img.style.minHeight = '0';
2835
2836        let logoDisplayNode = img;
2837        if (logoElementData.colorizeWithSiteColors &&
2838            window.SidebarManager &&
2839            typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
2840          const buildData = Object.assign({}, logoElementData);
2841          if (!buildData.imageWidth) {
2842            buildData.imageWidth = 'auto';
2843          }
2844          logoDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
2845        } else {
2846          img.style.width = 'auto';
2847          img.style.visibility = 'visible';
2848          img.style.opacity = '1';
2849        }
2850
2851        // Opt-in per-image padding wrapper (no-op when unset; does not alter .horizontal-header-logo CSS)
2852        if (window.SidebarManager &&
2853            typeof window.SidebarManager._wrapWithImagePadding === 'function') {
2854          logoDisplayNode = window.SidebarManager._wrapWithImagePadding(logoDisplayNode, logoElementData);
2855        }
2856
2857        // Enhanced event listeners for debugging
2858        img.onload = () => {
2859          console.log('[RenderHorizontalMenu] Image successfully LOADED');
2860        };
2861        
2862        img.onerror = (error) => {
2863          console.error('[RenderHorizontalMenu] Image FAILED to load:', error);
2864          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
2865        };
2866
2867        try {
2868          // Handle linking (similar to sidebar-manager.js)
2869          // Only add links in view mode, not edit mode
2870          // Explicit edit toggle only — Parameters.isInEditor is also true for logged-in admins
2871          const isEditing = document.body.classList.contains('edit-mode-active') ||
2872                           (typeof window.isInEditMode === 'function' && window.isInEditMode());
2873          
2874          // Debug: Log link properties and element data
2875          console.log('[RenderHorizontalMenu] Full logoElementData:', logoElementData);
2876          console.log('[RenderHorizontalMenu] Image link properties:', {
2877            linkType: logoElementData.linkType,
2878            linkUrl: logoElementData.linkUrl,
2879            linkPageId: logoElementData.linkPageId,
2880            linkTarget: logoElementData.linkTarget,
2881            isEditing: isEditing,
2882            hasLinkType: !!logoElementData.linkType,
2883            linkTypeValue: logoElementData.linkType,
2884            linkTypeNotNone: logoElementData.linkType && logoElementData.linkType !== 'none',
2885            notEditing: !isEditing,
2886            bodyHasEditClass: document.body.classList.contains('edit-mode-active'),
2887            isInEditMode: typeof window.isInEditMode === 'function' ? window.isInEditMode() : 'N/A',
2888            ParametersIsInEditor: window.Parameters ? window.Parameters.isInEditor : 'N/A'
2889          });
2890          
2891          // Check for link - handle undefined, null, empty string, or 'none'
2892          const hasLink = logoElementData.linkType && 
2893                         logoElementData.linkType !== 'none' && 
2894                         logoElementData.linkType !== '' &&
2895                         !isEditing;
2896          
2897          console.log('[RenderHorizontalMenu] Has link?', hasLink, 'linkType:', logoElementData.linkType);
2898          
2899          if (hasLink) {
2900            const link = document.createElement('a');
2901            link.target = logoElementData.linkTarget || '_self';
2902            let finalLinkHref = '#';
2903            let isValidLink = false;
2904            
2905            if (logoElementData.linkType === 'external' && logoElementData.linkUrl) {
2906              finalLinkHref = logoElementData.linkUrl;
2907              if (link.target === '_blank') {
2908                link.rel = 'noopener noreferrer';
2909              }
2910              isValidLink = true;
2911            } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2912              const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2913              const linkPageIdNum = parseInt(logoElementData.linkPageId);
2914              console.log('[RenderHorizontalMenu] Looking for internal page:', {
2915                linkPageId: logoElementData.linkPageId,
2916                linkPageIdNum: linkPageIdNum,
2917                allGalleriesLength: allGalleries ? allGalleries.length : 0,
2918                galleryIds: allGalleries ? allGalleries.map(g => ({ id: g.id, title: g.title })).slice(0, 5) : []
2919              });
2920              
2921              const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2922              
2923              console.log('[RenderHorizontalMenu] Target page found:', targetPage ? { id: targetPage.id, title: targetPage.title, slug: targetPage.slug, url: targetPage.url } : null);
2924              
2925              if (targetPage) {
2926                // Handle preview mode URLs
2927                const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
2928                console.log('[RenderHorizontalMenu] Preview mode:', isPreviewMode);
2929                
2930                if (isPreviewMode) {
2931                  const pathParts = window.location.pathname.split('/').filter(Boolean);
2932                  const siteGuid = pathParts[0];
2933                  console.log('[RenderHorizontalMenu] Preview mode path parts:', pathParts, 'siteGuid:', siteGuid, 'targetPage.slug:', targetPage.slug);
2934                  
2935                  if (siteGuid && targetPage.slug) {
2936                    finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
2937                    isValidLink = true;
2938                    console.log('[RenderHorizontalMenu] Set preview href:', finalLinkHref);
2939                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2940                    finalLinkHref = targetPage.url;
2941                    isValidLink = true;
2942                    console.log('[RenderHorizontalMenu] Set preview href from url:', finalLinkHref);
2943                  }
2944                } else {
2945                  if (targetPage.slug) {
2946                    finalLinkHref = '/' + targetPage.slug;
2947                    isValidLink = true;
2948                    console.log('[RenderHorizontalMenu] Set non-preview href:', finalLinkHref);
2949                  } else if (targetPage.url && targetPage.url.startsWith('/')) {
2950                    finalLinkHref = targetPage.url;
2951                    isValidLink = true;
2952                    console.log('[RenderHorizontalMenu] Set non-preview href from url:', finalLinkHref);
2953                  }
2954                }
2955              } else {
2956                console.warn('[RenderHorizontalMenu] Target page not found for linkPageId:', logoElementData.linkPageId);
2957              }
2958            }
2959            
2960            if (isValidLink) {
2961              link.href = finalLinkHref;
2962              // Ensure link is clickable
2963              link.style.pointerEvents = 'auto';
2964              link.style.cursor = 'pointer';
2965              link.style.textDecoration = 'none';
2966              link.style.display = 'flex';
2967              link.style.alignItems = 'center';
2968              link.style.height = 'auto';
2969              link.style.maxHeight = '100%';
2970              link.style.minHeight = '0';
2971              link.style.lineHeight = '0';
2972              console.log('[RenderHorizontalMenu] Link created with href:', finalLinkHref, 'target:', link.target, 'styles applied');
2973              
2974              // For internal links, add click handler to use site navigation
2975              if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) {
2976                // Store linkPageId in a variable that won't be affected by closure
2977                const linkPageId = logoElementData.linkPageId;
2978                const linkHref = finalLinkHref;
2979                
2980                // Add click handler with both capture and bubble phases to ensure it fires
2981                const clickHandler = (e) => {
2982                  console.log('[RenderHorizontalMenu] Link clicked! linkPageId:', linkPageId, 'event:', e);
2983                  e.preventDefault();
2984                  e.stopPropagation();
2985                  e.stopImmediatePropagation();
2986                  
2987                  const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
2988                  const linkPageIdNum = parseInt(linkPageId);
2989                  const targetPage = allGalleries.find(g => g.id === linkPageIdNum);
2990                  
2991                  console.log('[RenderHorizontalMenu] Click handler - targetPage found:', targetPage ? { id: targetPage.id, title: targetPage.title, isPage: targetPage.isPage } : null);
2992                  console.log('[RenderHorizontalMenu] Click handler - loadPage function:', typeof window.loadPage, 'loadGallery function:', typeof window.loadGallery);
2993                  
2994                  if (targetPage) {
2995                    if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
2996                      console.log('[RenderHorizontalMenu] Calling loadPage with id:', targetPage.id);
2997                      window.loadPage(targetPage.id);
2998                    } else if (typeof window.loadGallery === 'function') {
2999                      console.log('[RenderHorizontalMenu] Calling loadGallery with id:', targetPage.id);
3000                      window.loadGallery(targetPage.id);
3001                    } else {
3002                      // Fallback to standard navigation
3003                      console.log('[RenderHorizontalMenu] Fallback to window.location.href:', linkHref);
3004                      window.location.href = linkHref;
3005                    }
3006                  } else {
3007                    // Fallback to standard navigation if page not found
3008                    console.warn('[RenderHorizontalMenu] Target page not found in click handler, using fallback href:', linkHref);
3009                    window.location.href = linkHref;
3010                  }
3011                  
3012                  return false;
3013                };
3014                
3015                // Add handler in capture phase to catch event early
3016                link.addEventListener('click', clickHandler, true);
3017                // Also add in bubble phase as backup
3018                link.addEventListener('click', clickHandler, false);
3019                
3020                // Also add mousedown as backup in case click is being prevented
3021                link.addEventListener('mousedown', (e) => {
3022                  if (e.button === 0) { // Left click only
3023                    console.log('[RenderHorizontalMenu] MouseDown detected on link, triggering click handler');
3024                    clickHandler(e);
3025                  }
3026                });
3027                
3028                console.log('[RenderHorizontalMenu] Click handlers attached to link');
3029              }
3030              
3031              link.appendChild(logoDisplayNode);
3032              logoContainer.appendChild(link);
3033              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer. Link element:', link, 'href:', link.href, 'parent:', logoContainer);
3034              
3035              // Verify link is in DOM and clickable
3036              setTimeout(() => {
3037                const verifyLink = logoContainer.querySelector('a');
3038                if (verifyLink) {
3039                  console.log('[RenderHorizontalMenu] Link verification - Found in DOM:', {
3040                    href: verifyLink.href,
3041                    pointerEvents: window.getComputedStyle(verifyLink).pointerEvents,
3042                    cursor: window.getComputedStyle(verifyLink).cursor,
3043                    zIndex: window.getComputedStyle(verifyLink).zIndex,
3044                    position: window.getComputedStyle(verifyLink).position
3045                  });
3046                } else {
3047                  console.warn('[RenderHorizontalMenu] Link verification - Link NOT found in DOM!');
3048                }
3049              }, 100);
3050            } else {
3051              console.warn('[RenderHorizontalMenu] Link is not valid, appending image without link. isValidLink:', isValidLink);
3052              logoContainer.appendChild(logoDisplayNode);
3053              console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
3054            }
3055          } else {
3056            logoContainer.appendChild(logoDisplayNode);
3057            console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.');
3058          }
3059        } catch (e) {
3060          console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e);
3061          renderTextLogo(logoContainer, logoElementData.title || 'Logo');
3062        }
3063        
3064      } else if (logoElementData.type === 'text' && logoElementData.textContent) {
3065        console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent);
3066        renderTextLogo(logoContainer, logoElementData.textContent);
3067        
3068      } else {
3069        console.log('[RenderHorizontalMenu] Using title as fallback logo');
3070        renderTextLogo(logoContainer, logoElementData.title || 'Menu');
3071      }
3072    } else {
3073      console.log("[RenderHorizontalMenu] No logoElementData available. Using default text.");
3074      renderTextLogo(logoContainer, 'Menu');
3075    }
3076  }
3077
3078  // Render menu items (only on desktop, not on mobile)
3079  const menuContainer = mobileHeader.querySelector('.horizontal-menu-container');
3080  if (!menuContainer) {
3081    console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header.");
3082  } else {
3083    menuContainer.innerHTML = '';
3084    
3085    // Apply alignment class from settings
3086    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
3087      const rawAlign = window.MenuStyleCustomizer.settings.horizontalMenuAlignment || 'left';
3088      const alignment = ['left', 'center', 'right'].indexOf(rawAlign) >= 0 ? rawAlign : 'left';
3089      menuContainer.classList.remove('menu-align-left', 'menu-align-right', 'menu-align-center');
3090      menuContainer.classList.add('menu-align-' + alignment);
3091      console.log('[RenderHorizontalMenu] Applied alignment class: menu-align-' 
3091+ alignment);
3092    }
3093    
3094    // Only populate menu items on desktop, not on mobile
3095    if (!isMobileView) {
3096      let galleryTree = [];
3097      if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') {
3098        galleryTree = createGalleryTree(galleries);
3099      } else {
3100        console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined.");
3101      }
3102      
3103      galleryTree.forEach(item => {
3104        const menuItemElement = createHorizontalMenuItem(item);
3105        if (menuItemElement) {
3106          menuContainer.appendChild(menuItemElement);
3107        }
3108      });
3109      if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
3110    } else {
3111      console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population");
3112    }
3113  }
3114
3115  // Render social icons (only on desktop, not on mobile)
3116  let socialContainer = mobileHeader.querySelector('.horizontal-social-container');
3117  if (!socialContainer) {
3118    // Fallback: create the social container if it doesn't exist
3119    console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it...");
3120    const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content');
3121    if (mobileHeaderContent) {
3122      socialContainer = document.createElement('div');
3123      socialContainer.className = 'horizontal-social-container';
3124      mobileHeaderContent.appendChild(socialContainer);
3125      console.log("[RenderHorizontalMenu] Created .horizontal-social-container");
3126    } else {
3127      console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container");
3128    }
3129  }
3130  
3131  if (socialContainer) {
3132    socialContainer.innerHTML = '';
3133    
3134    // Only populate social icons on desktop, not on mobile
3135    if (!isMobileView) {
3136      const socialElement = getFirstSocialElementForHeader();
3137      if (socialElement && socialElement.type === 'social') {
3138        console.log('[RenderHorizontalMenu] Found social element, rendering social icons');
3139        renderSocialIcons(socialContainer, socialElement);
3140      } else {
3141        console.log('[RenderHorizontalMenu] No social element found or element is not social type');
3142        // Container remains empty - no impact on layout
3143      }
3144    } else {
3145      console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population");
3146    }
3147  }
3148
3149
3150
3151  if (typeof updateActiveStatesHorizontal === 'function') {
3152    updateActiveStatesHorizontal();
3153  }
3154
3155  if (!isMobileView && mobileHeaderContent) {
3156    mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title').forEach(function(el) {
3157      if (!el.closest('.horizontal-header-logo')) {
3158        el.remove();
3159      }
3160    });
3161  }
3162  
3163setTimeout(() => {
3164  ensureSidebarContentForMobile();
3165}, 100);
3166
3167  if (typeof syncContentMarginToHorizontalHeader === 'function') {
3168    setTimeout(function() { syncContentMarginToHorizontalHeader(); }, 200);
3169  }
3170
3171  console.log("[RenderHorizontalMenu] Function COMPLETED.");
3172}
3173
3174/**
3175 * Renders social icons in the horizontal header container.
3176 * @param {HTMLElement} container - The container to render social icons in
3177 * @param {object} socialElement - The social element data from sidebar
3178 */
3179function renderSocialIcons(container, socialElement) {
3180  console.log('[RenderSocialIcons] Rendering social icons:', socialElement);
3181  
3182  try {
3183    // Safety check: ensure we have a valid social element
3184    if (!socialElement || socialElement.type !== 'social') {
3185      console.log('[RenderSocialIcons] Invalid social element - skipping rendering');
3186      return;
3187    }
3188
3189    /* Placeholders without URLs still need menu color (CSS also sets this). */
3190    container.style.color = 'var(--menu-color, #333)';
3191    
3192    // Get icon size with proper fallbacks
3193    let iconSize = 30;
3194    if (typeof socialElement.socialIconSize === 'number' && !isNaN(socialElement.socialIconSize)) {
3195      iconSize = socialElement.socialIconSize;
3196    } else if (typeof socialElement.socialIconSize === 'string' && !isNaN(parseInt(socialElement.socialIconSize))) {
3197      iconSize = parseInt(socialElement.socialIconSize);
3198    }
3199    
3200    // Render icons directly from element data
3201    if (socialElement.socialIcons && Array.isArray(socialElement.socialIcons)) {
3202      socialElement.socialIcons.forEach(icon => {
3203        // Create icon wrapper (color via CSS --menu-color so placeholders without URLs stay visible)
3204        const iconWrapper = document.createElement('div');
3205        iconWrapper.className = 'social-icon-wrapper';
3206        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;';
3207
3208        const applySvgSizing = (host) => {
3209          const svg = host.querySelector('svg');
3210          if (!svg) return;
3211          svg.style.width = '100%';
3212          svg.style.height = '100%';
3213          svg.setAttribute('width', '100%');
3214          svg.setAttribute('height', '100%');
3215          svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
3216          svg.setAttribute('aria-hidden', 'true');
3217          svg.setAttribute('focusable', 'false');
3218          svg.style.color = 'inherit';
3219          svg.style.fill = 'currentColor';
3220          svg.style.stroke = 'currentColor';
3221        };
3222
3223        const hasUrl = !!(icon.url && String(icon.url).trim());
3224
3225        // Add link if URL exists; otherwise still render the glyph (placeholder icons)
3226        if (hasUrl) {
3227          const link = document.createElement('a');
3228          link.href = icon.url;
3229          link.target = '_blank';
3230          link.rel = 'noopener noreferrer';
3231          link.classList.add('horizontal-social-link');
3232          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;';
3233          const socialLabel = (icon.alt && String(icon.alt).trim())
3234            || (window.SidebarManager && typeof window.SidebarManager._socialIconTypeLabel === 'function'
3235              ? window.SidebarManager._socialIconTypeLabel(icon.type)
3236              : (icon.type && icon.type !== 'custom' ? String(icon.type).replace(/_/g, ' ') : ''))
3237            || 'Social link';
3238          const socialLabelPretty = socialLabel.charAt(0).toUpperCase() + socialLabel.slice(1);
3239          link.setAttribute('aria-label', socialLabelPretty + ' (opens in new tab)');
3240          
3241          if (icon.type === 'custom' && icon.iconUrl) {
3242            // Render uploaded image
3243            const img = document.createElement('img');
3244            img.src = icon.iconUrl;
3245            img.alt = '';
3246            img.setAttribute('aria-hidden', 'true');
3247            img.style.width = '100%';
3248            img.style.height = '100%';
3249            img.style.objectFit = 'contain';
3250            link.appendChild(img);
3251          } else {
3252            // Add SVG - use SidebarManager's method if available, otherwise create basic SVG
3253            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
3254              link.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
3255            } else {
3256              // Fallback: create a simple SVG placeholder
3257              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>';
3258            }
3259            applySvgSizing(link);
3260          }
3261          iconWrapper.appendChild(link);
3262        } else {
3263          // Placeholder: no URL — still paint with menu color via container inheritance
3264          iconWrapper.setAttribute('aria-hidden', 'true');
3265          if (icon.type === 'custom' && icon.iconUrl) {
3266            const img = document.createElement('img');
3267            img.src = icon.iconUrl;
3268            img.alt = '';
3269            img.style.width = '100%';
3270            img.style.height = '100%';
3271            img.style.objectFit = 'contain';
3272            iconWrapper.appendChild(img);
3273          } else {
3274            if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') {
3275              iconWrapper.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type);
3276            } else {
3277              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>';
3278            }
3279            applySvgSizing(iconWrapper);
3280          }
3281        }
3282        
3283        container.appendChild(iconWrapper);
3284        console.log('[RenderSocialIcons] Rendered social icon:', icon.type, icon.url);
3285      });
3286    } else {
3287      console.log('[RenderSocialIcons] No social icons found in element');
3288    }
3289  } catch (error) {
3290    console.error('[RenderSocialIcons] Error rendering social icons:', error);
3291  }
3292}
3293
3294/**
3295 * Gets the appropriate icon text or class for a social platform.
3296 * @param {string} platform - The social platform name
3297 * @returns {string} The icon text or class
3298 */
3299function getSocialIconText(platform) {
3300  const iconMap = {
3301    'facebook': 'f',
3302    'twitter': 't',
3303    'instagram': 'i',
3304    'linkedin': 'in',
3305    'youtube': 'yt',
3306    'pinterest': 'p',
3307    'tiktok': 'tt',
3308    'snapchat': 'sc',
3309    'whatsapp': 'wa',
3310    'telegram': 'tg',
3311    'discord': 'd',
3312    'github': 'gh',
3313    'email': '@',
3314    'phone': '📞',
3315    'website': '🌐'
3316  };
3317  
3318  return iconMap[platform.toLowerCase()] || '•';
3319}
3320
3321
3322
3323
3324/**
3325 * Creates a single top-level horizontal menu item and its submenu if it has children.
3326 * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal)
3327 * @param {object} galleryItem - The gallery item data.
3328 * @returns {HTMLElement | null} The created menu item element, or null if not rendered.
3329 */
3330function createHorizontalMenuItem(galleryItem) {
3331  if (galleryItem.visible === false || galleryItem.isSpacer) {
3332      return null;
3333  }
3334
3335  const menuItem = document.createElement('div');
3336  menuItem.className = 'horizontal-menu-item';
3337  menuItem.dataset.id = String(galleryItem.id);
3338  menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3339
3340  const hasVisibleChildren = !!(galleryItem.children && galleryItem.children.filter(c => c.visible !== false && !c.isSpacer).length);
3341  const isNavigableParent = isNavigableMenuParent(galleryItem);
3342  let titleEl;
3343  if (hasVisibleChildren && !isNavigableParent) {
3344    titleEl = document.createElement('span');
3345    titleEl.className = 'horizontal-menu-label';
3346    titleEl.textContent = galleryItem.title;
3347    menuItem.setAttribute('aria-haspopup', 'true');
3348    menuItem.setAttribute('aria-expanded', 'false');
3349  } else {
3350    titleEl = document.createElement('a');
3351    titleEl.className = 'horizontal-menu-label';
3352    titleEl.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3353    titleEl.textContent = galleryItem.title;
3354    if (galleryItem.isExternal) {
3355      titleEl.target = '_blank';
3356      titleEl.rel = 'noopener noreferrer';
3357    } else {
3358      titleEl.addEventListener('click', (e) => { e.preventDefault(); });
3359    }
3360  }
3361  menuItem.appendChild(titleEl);
3362
3363  // Ensure activeGalleryId is treated as a number for comparison during initial render
3364  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3365  if (galleryItem.id === currentGlobalActiveId) {
3366      menuItem.classList.add('active');
3367  }
3368
3369  let leaveTimer; // For mouseleave timeout to close dropdown
3370
3371  if (galleryItem.children && galleryItem.children.length > 0) {
3372      const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer);
3373
3374      if (visibleChildren.length > 0) {
3375          menuItem.classList.add('has-children');
3376          if (isNavigableParent) {
3377              menuItem.setAttribute('aria-haspopup', 'true');
3378              menuItem.setAttribute('aria-expanded', 'false');
3379          }
3380
3381          const toggleIconContainer = document.createElement(isNavigableParent ? 'button' : 'span');
3382          toggleIconContainer.className = 'submenu-toggle-horizontal';
3383          if (isNavigableParent) {
3384              toggleIconContainer.type = 'button';
3385              toggleIconContainer.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3386              toggleIconContainer.setAttribute('aria-expanded', 'false');
3387              toggleIconContainer.addEventListener('click', function(event) {
3388                  event.preventDefault();
3389                  event.stopPropagation();
3390                  const willExpand = !menuItem.classList.contains('expanded');
3391                  menuItem.classList.toggle('expanded', willExpand);
3392                  menuItem.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3393                  toggleIconContainer.setAttribute('aria-expanded', willExpand ? 'true' : 'false');
3394                  const icon = toggleIconContainer.querySelector('.icon.toggle-icon');
3395                  if (icon) icon.classList.toggle('rotated', willExpand);
3396              });
3397          }
3398          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3399          svgIcon.setAttribute("class", "icon toggle-icon");
3400          svgIcon.setAttribute("viewBox", "0 0 24 24");
3401          svgIcon.setAttribute("fill", "none");
3402          svgIcon.setAttribute("stroke", "currentColor");
3403          svgIcon.setAttribute("stroke-width", "2");
3404          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3405          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3406          svgIcon.appendChild(polyline);
3407          toggleIconContainer.appendChild(svgIcon);
3408          menuItem.appendChild(toggleIconContainer);
3409
3410          const subMenu = document.createElement('div');
3411          subMenu.className = 'horizontal-dropdown';
3412          const subMenuList = document.createElement('ul');
3413          subMenuList.className = 'horizontal-dropdown-list';
3414
3415          visibleChildren.forEach((childItem) => {
3416              const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call
3417              if (subMenuItemElement) {
3418                  subMenuList.appendChild(subMenuItemElement);
3419              }
3420          });
3421
3422          if (subMenuList.hasChildNodes()) {
3423              subMenu.appendChild(subMenuList);
3424              menuItem.appendChild(subMenu);
3425
3426              menuItem.addEventListener('mouseenter', function() {
3427                  clearTimeout(leaveTimer);
3428                  document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3429                      if (openItem !== this) {
3430                          openItem.classList.remove('expanded');
3431                          openItem.setAttribute('aria-expanded', 'false');
3432                          const otherIcon = openItem.querySelector('.icon.toggle-icon');
3433                          if (otherIcon) otherIcon.classList.remove('rotated');
3434                      }
3435                  });
3436                  this.classList.add('expanded');
3437                  this.setAttribute('aria-expanded', 'true');
3438                  if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'true');
3439                  const currentIcon = this.querySelector('.icon.toggle-icon');
3440                  if (currentIcon) currentIcon.classList.add('rotated');
3441              });
3442
3443              menuItem.addEventListener('mouseleave', function(event) {
3444                  // Check if we're moving to the dropdown or a flyout
3445                  const relatedTarget = event.relatedTarget;
3446                  const isMovingToDropdown = relatedTarget && (
3447                      relatedTarget.closest('.horizontal-dropdown') ||
3448                      relatedTarget.classList.contains('horizontal-submenu-item') ||
3449                      relatedTarget.closest('.horizontal-submenu-item')
3450                  );
3451                  const isMovingToFlyout = relatedTarget && (
3452                      relatedTarget.classList.contains('horizontal-nested-dropdown') ||
3453                      relatedTarget.closest('.horizontal-nested-dropdown')
3454                  );
3455                  
3456                  if (!isMovingToDropdown && !isMovingToFlyout) {
3457                      console.log('Main menu item mouseleave - starting timer');
3458                      leaveTimer = setTimeout(() => {
3459                          // Check if there's an active flyout - if so, keep parent open
3460                          // Use the same multiple detection methods as the submenu handler
3461                          const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3462                                             document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3463                                             document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3464                          console.log('Main menu timer fired - active flyout:', activeFlyout);
3465                          if (!activeFlyout) {
3466                              console.log('Main menu closing dropdown - no active flyout');
3467                              this.classList.remove('expanded');
3468                              this.setAttribute('aria-expanded', 'false');
3469                              if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'false');
3470                              const currentIcon = this.querySelector('.icon.toggle-icon');
3471                              if (currentIcon) currentIcon.classList.remove('rotated');
3472                          } else {
3473                              console.log('Main menu keeping dropdown open - flyout is active');
3474                          }
3475                      }, 200);
3476                  } else {
3477                      console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)');
3478                  }
3479              });
3480
3481              subMenu.addEventListener('mouseenter', function() {
3482                  const parentMenuItem = this.closest('.horizontal-menu
3482-item');
3483                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3484                      clearTimeout(parentMenuItem.leaveTimer);
3485                      parentMenuItem.leaveTimer = null;
3486                  }
3487              });
3488
3489              subMenu.addEventListener('mouseleave', function(event) {
3490                  const parentMenuItem = this.closest('.horizontal-menu-item');
3491                  if (parentMenuItem) {
3492                      // Check if we're moving to another element within the same dropdown
3493                      const relatedTarget = event.relatedTarget;
3494                      console.log('SubMenu mouseleave - relatedTarget:', relatedTarget);
3495                      console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null');
3496                      
3497                      // Check if we're moving to another element within the same dropdown
3498                      const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown');
3499                      console.log('Current dropdown found:', !!currentDropdown);
3500                      console.log('Current dropdown element:', currentDropdown);
3501                      console.log('RelatedTarget:', relatedTarget);
3502                      console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null');
3503                      
3504                      // Check if relatedTarget is within the same dropdown container
3505                      const isMovingWithinDropdown = relatedTarget && currentDropdown && (
3506                          relatedTarget.closest('.horizontal-dropdown') === currentDropdown ||
3507                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3508                          relatedTarget.closest('.horizontal-submenu-item') ||
3509                          currentDropdown.contains(relatedTarget)
3510                      );
3511                      
3512                      // Alternative approach: check if relatedTarget is within the same parent menu item
3513                      const parentMenuContainer = this.closest('.horizontal-menu-item');
3514                      const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget);
3515                      console.log('Is moving within parent menu item:', isMovingWithinParent);
3516                      console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown');
3517                      console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null');
3518                      
3519                      // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside)
3520                      // Get the parent menu item's dropdown container
3521                      const parentMenuItem = this.closest('.horizontal-menu-item');
3522                      console.log('Parent menu item found:', !!parentMenuItem);
3523                      const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null;
3524                      console.log('Dropdown container found:', !!dropdownContainer);
3525                      if (dropdownContainer) {
3526                          console.log('Dropdown container classes:', dropdownContainer.className);
3527                          console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display);
3528                          console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position);
3529                      }
3530                      
3531                      // Get bounds from the visible dropdown (not the hidden one)
3532                      let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 };
3533                      if (dropdownContainer) {
3534                          const computedStyle = window.getComputedStyle(dropdownContainer);
3535                          if (computedStyle.display !== 'none') {
3536                              dropdownRect = dropdownContainer.getBoundingClientRect();
3537                          } else {
3538                              // If dropdown is hidden, try to get bounds from the parent menu item
3539                              const parentRect = parentMenuItem.getBoundingClientRect();
3540                              dropdownRect = parentRect;
3541                          }
3542                      }
3543                      const mouseX = event.clientX;
3544                      const mouseY = event.clientY;
3545                      const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && 
3546                                                     mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom;
3547                      
3548                      console.log('Dropdown bounds:', {
3549                          left: dropdownRect.left,
3550                          right: dropdownRect.right,
3551                          top: dropdownRect.top,
3552                          bottom: dropdownRect.bottom,
3553                          mouseX,
3554                          mouseY,
3555                          mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom
3556                      });
3557                      
3558                      // If there's an active flyout, always keep the parent dropdown open
3559                      // Check multiple ways to detect active flyouts
3560                      const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') ||
3561                                         document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') ||
3562                                         document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])');
3563                      console.log('Active flyout found:', !!activeFlyout);
3564                      if (activeFlyout) {
3565                          console.log('Active flyout display style:', activeFlyout.style.display);
3566                          console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display);
3567                          console.log('Active flyout visibility style:', activeFlyout.style.visibility);
3568                      }
3569                      const hasActiveFlyout = !!activeFlyout;
3570                      
3571                      // If the dropdown is hidden (display: none), we can't get accurate bounds
3572                      // So let's use a simpler approach - if we're moving to another submenu item, keep open
3573                      const isMovingToSubmenuItem = relatedTarget && (
3574                          relatedTarget.classList.contains('horizontal-submenu-item') ||
3575                          relatedTarget.closest('.horizontal-submenu-item')
3576                      );
3577                      console.log('Is moving to submenu item:', isMovingToSubmenuItem);
3578                      console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null');
3579                      console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null');
3580                      
3581                      // If we're moving to another submenu item within the same dropdown, al
3581ways keep open
3582                      // OR if there's an active flyout (regardless of where the mouse is), keep open
3583                      // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open
3584                      const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem;
3585                      
3586                      console.log('Is moving within dropdown:', isMovingWithinDropdown);
3587                      console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds);
3588                      console.log('Has active flyout:', hasActiveFlyout);
3589                      console.log('Should keep open:', shouldKeepOpen);
3590                      
3591                      if (!shouldKeepOpen) {
3592                          console.log('SubMenu mouseleave - starting timer (moving outside dropdown)');
3593                          parentMenuItem.leaveTimer = setTimeout(() => {
3594                              // Check if there's an active flyout - if so, keep parent open
3595                              const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3596                              console.log('Timer fired - active flyout:', activeFlyout);
3597                              if (!activeFlyout) {
3598                                  console.log('Closing parent dropdown - no active flyout');
3599                                  parentMenuItem.classList.remove('expanded');
3600                                  const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3601                                  if (currentIcon) currentIcon.classList.remove('rotated');
3602                              } else {
3603                                  console.log('Keeping parent dropdown open - flyout is active');
3604                              }
3605                          }, 200);
3606                      } else {
3607                          console.log('SubMenu mouseleave - not starting timer (moving within dropdown)');
3608                      }
3609                  }
3610              });
3611          } else {
3612              menuItem.classList.remove('has-children');
3613              if (toggleIconContainer.parentNode) {
3614                  toggleIconContainer.remove();
3615              }
3616          }
3617      }
3618  }
3619
3620  // Click listener for items that are direct navigation links (not folders with children)
3621  if (!menuItem.classList.contains('has-children') || isNavigableParent) {
3622      menuItem.addEventListener('click', async (event) => {
3623          if (isNavigableParent && hasVisibleChildren && !titleEl.contains(event.target)) {
3624              return;
3625          }
3626          event.stopPropagation();
3627          const clickedItemId = galleryItem.id; // Capture the ID of the clicked item
3628
3629          // Check if it's an external URL first
3630          if (galleryItem.isExternal && galleryItem.url) {
3631              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
3632              return;
3633          }
3634
3635          if (isPageMenuItem(galleryItem)) {
3636              if (typeof loadPage === 'function') {
3637                  loadPage(clickedItemId, event); // loadPage is synchronous
3638                  if (typeof updateActiveStatesHorizontal === 'function') {
3639                      console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
3640                      // Use a minimal delay even for sync operations if page rendering has its own async microtasks
3641                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
3642                  }
3643              }
3644          } else { // It's a gallery
3645              if (typeof loadGallery === 'function') {
3646                  console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
3647                  await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery
3648                  console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
3649                  if (typeof updateActiveStatesHorizontal === 'function') {
3650                      setTimeout(() => {
3651                          console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
3652                          updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId
3653                      }, 100); // Delay to allow gallery system to settle
3654                  }
3655              }
3656          }
3657
3658          // Close any other open dropdowns and flyouts
3659          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3660              // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click)
3661              if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) {
3662                 openItem.classList.remove('expanded');
3663                 openItem.setAttribute('aria-expanded', 'false');
3664                 const icon = openItem.querySelector('.icon.toggle-icon');
3665                 if (icon) icon.classList.remove('rotated');
3666              }
3667          });
3668          
3669          // Close all flyouts
3670          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
3671              flyout.style.display = 'none';
3672              flyout.style.opacity = '0';
3673              flyout.style.visibility = 'hidden';
3674          });
3675          
3676          // Remove expanded class from all submenu items
3677          document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => {
3678              subItem.classList.remove('expanded');
3679          });
3680      });
3681  } else {
3682      // Keyboard: Enter/Space toggles hover-style dropdown without changing mouse behavior
3683      menuItem.setAttribute('tabindex', '0');
3684      menuItem.addEventListener('keydown', function(event) {
3685          if (event.key !== 'Enter' && event.key !== ' ') return;
3686          event.preventDefault();
3687          const isOpen = this.classList.contains('expanded');
3688          document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => {
3689              if (openItem !== this) {
3690                  openItem.classList.remove('expanded');
3691                  openItem.setAttribute('aria-expanded', 'false');
3692                  const otherIcon = openItem.querySelector('.icon.toggle-icon');
3693                  if (otherIcon) otherIcon.classList.remove('rotated');
3694              }
3695          });
3696          this.classList.toggle('expanded', !isOpen);
3697          this.setAttribute('aria-expanded', !isOpen ? 'true' : 'false');
3698          const currentIcon = this.querySelector('.icon.toggle-icon');
3699          if (currentIcon) currentIcon.classList.toggle('rotated', !isOpen);
3700      });
3701  }
3702  return menuItem;
3703}
3704
3705/**
3706* Creates a single horizontal submenu item.
3707* (UPDATED to handle nested children and async loadGallery)
3708* @param {object} galleryItem - The gallery item data for the submenu.
3709* @returns {HTMLElement | null} The created submenu item element, or null if not rendered.
3710*/
3711function createHorizontalSubMenuItem(galleryItem) {
3712  if (galleryItem.visible === false || galleryItem.isSpacer) {
3713      return null;
3714  }
3715
3716  const subMenuItem = document.createElement('li');
3717  subMenuItem.className = 'horizontal-submenu-item';
3718  subMenuItem.dataset.id = String(galleryItem.id);
3719  subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false');
3720
3721  const subVisibleChildren = (galleryItem.children || []).filter(child => child.visible !== false && !child.isSpacer);
3722  const isNavigableParent = isNavigableMenuParent(galleryItem);
3723  let titleSpan;
3724  if (subVisibleChildren.length > 0 && !isNavigableParent) {
3725      titleSpan = document.createElement('span');
3726      titleSpan.className = 'horizontal-menu-label';
3727      titleSpan.textContent = galleryItem.title;
3728  } else {
3729      titleSpan = document.createElement('a');
3730      titleSpan.className = 'horizontal-menu-label';
3731      titleSpan.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#';
3732      titleSpan.textContent = galleryItem.title;
3733      if (galleryItem.isExternal) {
3734          titleSpan.target = '_blank';
3735          titleSpan.rel = 'noopener noreferrer';
3736      } else {
3737          titleSpan.addEventListener('click', (e) => { e.preventDefault(); });
3738      }
3739  }
3740  subMenuItem.appendChild(titleSpan);
3741
3742  const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId));
3743  if (galleryItem.id === currentGlobalActiveId) {
3744      subMenuItem.classList.add('active');
3745      if (titleSpan.tagName === 'A') titleSpan.setAttribute('aria-current', 'page');
3746  }
3747
3748  // Check if this submenu item has children (nested sub-sub items)
3749  if (galleryItem.children && galleryItem.children.length > 0) {
3750      const visibleChildren = subVisibleChildren;
3751      
3752      if (visibleChildren.length > 0) {
3753          subMenuItem.classList.add('has-children');
3754          
3755          // Add arrow indicator for nested items (same as top-level menu)
3756          const arrowSpan = document.createElement(isNavigableParent ? 'button' : 'span');
3757          arrowSpan.className = 'submenu-arrow';
3758          if (isNavigableParent) {
3759              arrowSpan.type = 'button';
3760              arrowSpan.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title);
3761              arrowSpan.setAttribute('aria-expanded', 'false');
3762              arrowSpan.addEventListener('click', function(event) {
3763                  event.preventDefault();
3764                  event.stopPropagation();
3765                  const isOpen = subMenuItem.classList.contains('expanded');
3766                  if (isOpen) {
3767                      closeHorizontalNestedFlyout(subMenuItem);
3768                  } else {
3769                      openHorizontalNestedFlyout(subMenuItem);
3770                  }
3771              });
3772          }
3773          const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3774          svgIcon.setAttribute("class", "icon toggle-icon");
3775          svgIcon.setAttribute("viewBox", "0 0 24 24");
3776          svgIcon.setAttribute("fill", "none");
3777          svgIcon.setAttribute("stroke", "currentColor");
3778          svgIcon.setAttribute("stroke-width", "2");
3779          const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
3780          polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3781          svgIcon.appendChild(polyline);
3782          arrowSpan.appendChild(svgIcon);
3783          subMenuItem.appendChild(arrowSpan);
3784          
3785          // Create nested submenu as a true flyout
3786          const nestedSubMenu = document.createElement('div');
3787          nestedSubMenu.className = 'horizontal-nested-dropdown';
3788          
3789          // Create a list container for the nested items
3790          const nestedList = document.createElement('ul');
3791          nestedList.className = 'horizontal-nested-list';
3792          
3793          visibleChildren.forEach((childItem) => {
3794              const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call
3795              if (nestedSubMenuItem) {
3796                  nestedList.appendChild(nestedSubMenuItem);
3797              }
3798          });
3799          
3800          nestedSubMenu.appendChild(nestedList);
3801          
3802          if (nestedSubMenu.hasChildNodes()) {
3803              // Append to document body for true flyout behavior
3804              document.body.appendChild(nestedSubMenu);
3805              // Store reference to the flyout for cleanup
3806              const flyoutId = 'flyout-' + Date.now() + '-' + Math.random();
3807              subMenuItem.setAttribute('data-flyout-id', flyoutId);
3808              nestedSubMenu.setAttribute('data-flyout-id', flyoutId);
3809              
3810              console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug
3811              console.log('Flyout element:', nestedSubMenu); // Debug
3812              
3813              // Add hover events for nested submenu
3814              let nestedLeaveTimer;
3815
3816              // Shared open/close used by hover AND keyboard — mouse path unchanged
3817              function openHorizontalNestedFlyout(hostItem) {
3818                  if (!hostItem) return;
3819                  if (hostItem.nestedLeaveTimer) {
3820                      clearTimeout(hostItem.nestedLeaveTimer);
3821                  }
3822                  const parentMenuItem = hostItem.closest('.horizontal-menu-item');
3823                  if (parentMenuItem && parentMenuItem.leaveTimer) {
3824                      clearTimeout(parentMenuItem.leaveTimer);
3825                  }
3826                  document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => {
3827                      if (openItem !== hostItem) {
3828                          openItem.classList.remove('expanded');
3829                          openItem.setAttribute('aria-expanded', 'false');
3830                          const otherFlyoutId = openItem.getAttribute('data-flyout-id');
3831                          if (otherFlyoutId) {
3832                              const otherFlyout = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + otherFlyoutId + '"]');
3833                              if (otherFlyout) {
3834                                  otherFlyout.style.display = 'none';
3835                                  otherFlyout.style.opacity = '0';
3836                                  otherFlyout.style.visibility = 'hidden';
3837                              }
3838                          }
3839                      }
3840                  });
3841                  hostItem.classList.add('expanded');
3842                  hostItem.setAttribute('aria-expanded', 'true');
3843                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3844                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'true');
3845                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3846                  if (!hostFlyoutId) return;
3847                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3848                  if (!nestedDropdown) return;
3849                  const rect = hostItem.getBoundingClientRect();
3850                  const parentDropdown = hostItem.closest('.horizontal-dropdown');
3851                  const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect;
3852                  const leftPos = parentRect.right - 1;
3853                  const topPos = rect.top;
3854                  nestedDropdown.style.left = leftPos + 'px';
3855                  nestedDropdown.style.top = topPos + 'px';
3856                  nestedDropdown.style.display = 'block';
3857                  nestedDropdown.style.opacity = '1';
3858                  nestedDropdown.style.visibility = 'visible';
3859                  nestedDropdown.style.position = 'fixed';
3860                  nestedDropdown.style.zIndex = '1251';
3861                  setTimeout(() => {
3862                      const dropdownRect = nestedDropdown.getBoundingClientRect();
3863                      if (dropdownRect.right > window.innerWidth) {
3864                          nestedDropdown.style.left = (parentRect.left - dropdownRect.width - 5) + 'px';
3865                      }
3866                  }, 10);
3867              }
3868
3869              function closeHorizontalNestedFlyout(hostItem) {
3870                  if (!hostItem) return;
3871                  hostItem.classList.remove('expanded');
3872                  hostItem.setAttribute('aria-expanded', 'false');
3873                  const hostArrow = hostItem.querySelector('.submenu-arrow');
3874                  if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'false');
3875                  const hostFlyoutId = hostItem.getAttribute('data-flyout-id');
3876                  if (!hostFlyoutId) return;
3877                  const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]');
3878                  if (nestedDropdown) {
3879                      nestedDropdown.style.display = 'none';
3880                      nestedDropdown.style.opacity = '0';
3881                      nestedDropdown.style.visibility = 'hidden';
3882                  }
3883              }
3884
3885              // Keyboard opens the same flyout hover shows — attrs on label so mouse click focus stays natural
3886              if (titleSpan && !isNavigableParent) {
3887                  titleSpan.setAttribute('tabindex', '0');
3888                  titleSpan.setAttribute('role', 'button');
3889                  titleSpan.setAttribute('aria-haspopup', 'true');
3890                  titleSpan.setAttribute('aria-expanded', 'false');
3891                  titleSpan.addEventListener('keydown', function(event) {
3892                      if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowRight') {
3893                          event.preventDefault();
3894                          event.stopPropagation();
3895                          openHorizontalNestedFlyout(subMenuItem);
3896                          titleSpan.setAttribute('aria-expanded', 'true');
3897                          const hostFlyoutId = subMenuItem.getAttribute('data-flyout-id');
3898                          const nestedDropdown = hostFlyoutId
3899                              ? document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]')
3900                              : null;
3901                          const firstLink = nestedDropdown && nestedDropdown.querySelector('a.horizontal-menu
3901-label');
3902                          if (firstLink && typeof firstLink.focus === 'function') {
3903                              try { firstLink.focus(); } catch (e) {}
3904                          }
3905                      } else if (event.key === 'Escape' || event.key === 'ArrowLeft') {
3906                          event.preventDefault();
3907                          event.stopPropagation();
3908                          closeHorizontalNestedFlyout(subMenuItem);
3909                          titleSpan.setAttribute('aria-expanded', 'false');
3910                      }
3911                  });
3912              }
3913              
3914              subMenuItem.addEventListener('mouseenter', function() {
3915                  openHorizontalNestedFlyout(this);
3916                  if (titleSpan) titleSpan.setAttribute('aria-expanded', 'true');
3917              });
3918              
3919              subMenuItem.addEventListener('mouseleave', function(event) {
3920                  // Check if we're moving to the flyout
3921                  const flyoutId = this.getAttribute('data-flyout-id');
3922                  const relatedTarget = event.relatedTarget;
3923                  const isMovingToFlyout = relatedTarget && flyoutId && 
3924                      relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]');
3925                  
3926                  // Also check if we're moving within the dropdown
3927                  const parentMenuItem = this.closest('.horizontal-menu-item');
3928                  const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget);
3929                  
3930                  console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout);
3931                  console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown);
3932                  
3933                  if (!isMovingToFlyout && !isMovingWithinDropdown) {
3934                      this.nestedLeaveTimer = setTimeout(() => {
3935                          closeHorizontalNestedFlyout(this);
3936                          if (titleSpan) titleSpan.setAttribute('aria-expanded', 'false');
3937                          
3938                          // Now that the flyout is closed, allow the parent dropdown to close
3939                          const parentMenuItem = this.closest('.horizontal-menu-item');
3940                          if (parentMenuItem) {
3941                              setTimeout(() => {
3942                                  const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]');
3943                                  if (!activeFlyout) {
3944                                      parentMenuItem.classList.remove('expanded');
3945                                      const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon');
3946                                      if (currentIcon) currentIcon.classList.remove('rotated');
3947                                  }
3948                              }, 50);
3949                          }
3950                      }, 200);
3951                  } else {
3952                      console.log('Submenu item mouseleave - not starting timer (moving to flyout)');
3953                  }
3954              });
3955              
3956              nestedSubMenu.addEventListener('mouseenter', function() {
3957                  console.log('Flyout mouseenter - clearing timers');
3958                  clearTimeout(nestedLeaveTimer);
3959                  
3960                  // Clear the parent dropdown's leave timer to keep it open
3961                  const flyoutId = this.getAttribute('data-flyout-id');
3962                  if (flyoutId) {
3963                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3964                      if (parentSubItem) {
3965                          const parentMenuItem = parentSubItem.closest('.horizontal-menu-item');
3966                          if (parentMenuItem && parentMenuItem.leaveTimer) {
3967                              console.log('Clearing parent menu item leave timer');
3968                              clearTimeout(parentMenuItem.leaveTimer);
3969                              parentMenuItem.leaveTimer = null;
3970                          } else {
3971                              console.log('No parent menu item leave timer to clear');
3972                          }
3973                          if (parentSubItem.nestedLeaveTimer) {
3974                              console.log('Clearing submenu item nested leave timer');
3975                              clearTimeout(parentSubItem.nestedLeaveTimer);
3976                              parentSubItem.nestedLeaveTimer = null;
3977                          }
3978                      }
3979                  }
3980              });
3981              
3982              nestedSubMenu.addEventListener('mouseleave', function() {
3983                  const flyoutId = this.getAttribute('data-flyout-id');
3984                  if (flyoutId) {
3985                      const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]');
3986                      if (parentSubItem) {
3987                          nestedLeaveTimer = setTimeout(() => {
3988                              closeHorizontalNestedFlyout(parentSubItem);
3989                              const label = parentSubItem.querySelector('.horizontal-menu
3989-label');
3990                              if (label) label.setAttribute('aria-expanded', 'false');
3991                          }, 200);
3992                      }
3993                  }
3994              });
3995          }
3996      }
3997  }
3998
3999  // Click listener for navigation (only if no children or if it's a direct link)
4000  if (!subMenuItem.classList.contains('has-children') || isPageMenuItem(galleryItem) || isNavigableParent) {
4001      subMenuItem.addEventListener('click', async (event) => {
4002          if (isNavigableParent && subVisibleChildren.length > 0 && !titleSpan.contains(event.target)) {
4003              return;
4004          }
4005          event.stopPropagation();
4006          const clickedItemId = galleryItem.id;
4007
4008          // Check if it's an external URL first
4009          if (galleryItem.isExternal && galleryItem.url) {
4010              window.open(galleryItem.url, '_blank', 'noopener,noreferrer');
4011              return;
4012          }
4013
4014          if (isPageMenuItem(galleryItem)) {
4015              if (typeof loadPage === 'function') {
4016                  loadPage(clickedItemId, event);
4017                  if (typeof updateActiveStatesHorizontal === 'function') {
4018                      console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId);
4019                      setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50);
4020                  }
4021              }
4022          } else { // It's a gallery
4023              if (typeof loadGallery === 'function') {
4024                  console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId);
4025                  await loadGallery(clickedItemId, event);
4026                  console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay.");
4027                  if (typeof updateActiveStatesHorizontal === 'function') {
4028                      setTimeout(() => {
4029                          console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId);
4030                          updateActiveStatesHorizontal(clickedItemId);
4031                      }, 100);
4032                  }
4033              }
4034          }
4035
4036          // Close all parent dropdowns and flyouts after navigation
4037          document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => {
4038              openItem.classList.remove('expanded');
4039              const icon = openItem.querySelector('.icon.toggle-icon');
4040              if (icon) icon.classList.remove('rotated');
4041          });
4042          
4043          // Close all flyouts
4044          document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => {
4045              flyout.style.display = 'none';
4046              flyout.style.opacity = '0';
4047              flyout.style.visibility = 'hidden';
4048          });
4049      });
4050  }
4051  
4052  return subMenuItem;
4053}
4054
4055/**
4056* Updates the 'active' class for items in the horizontal menu.
4057*/
4058
4059function updateActiveStatesHorizontal(forcedActiveId = null) {
4060  // Prioritize forcedActiveId if provided, otherwise use global state.
4061  // Ensure the ID is treated as a number for comparison.
4062  const targetId = forcedActiveId !== null 
4063                   ? parseInt(String(forcedActiveId)) 
4064                   : parseInt(String(window.activeGalleryId || activeGalleryId));
4065
4066  console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, 
4067              (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), 
4068              "Type:", typeof targetId);
4069
4070  const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item');
4071
4072  if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number
4073    console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId);
4074    return;
4075  }
4076
4077  let activeItemSet = false;
4078  menuItems.forEach(item => {
4079    const itemIdStr = item.dataset.id;
4080    if (!itemIdStr) {
4081      // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item);
4082      return; 
4083    }
4084    const itemId = parseInt(itemIdStr);
4085
4086    if (itemId === targetId) {
4087      if (!item.classList.contains('active')) {
4088        item.classList.add('active');
4089        console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
4090      }
4091      activeItemSet = true;
4092    } else {
4093      if (item.classList.contains('active')) {
4094        item.classList.remove('active');
4095        console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`);
4096      }
4097    }
4098  });
4099
4100  if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number
4101      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.`);
4102  }
4103
4104  const activeSubItem = document.querySelector('.horizontal-submenu-item.active');
4105  if (activeSubItem) {
4106      const parentTopItem = activeSubItem.closest('.horizontal-menu
4106-item.has-children');
4107      if (parentTopItem && !parentTopItem.classList.contains('active')) {
4108          // parentTopItem.classList.add('active-parent'); // Optional class for styling parent
4109      }
4110  }
4111  console.log("[Horizontal Menu Update] Completed for Target ID:", targetId);
4112}
4113
4114
4115  // Listener for closing dropdowns when clicking outside
4116  document.addEventListener('click', function(event) {
4117      if (currentMenuLayout === 'horizontal') {
4118          const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded');
4119          let clickedInsideSubmenuOrParent = false;
4120
4121          openSubmenus.forEach(submenuContainer => {
4122              // Check if click is on the submenu container OR its direct parent menu item
4123              if (submenuContainer.contains(event.target) || 
4124                  (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) {
4125                  clickedInsideSubmenuOrParent = true;
4126              }
4127          });
4128          
4129          // If click is on another top-level item that is NOT expanded, also don't close
4130          const targetMenuItem = event.target.closest('.horizontal-menu-item');
4131          if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) {
4132               clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it
4133          }
4134
4135
4136          if (!clickedInsideSubmenuOrParent) {
4137              openSubmenus.forEach(submenuContainer => {
4138                  submenuContainer.classList.remove('expanded');
4139              });
4140          }
4141      }
4142  });
4143
4144document.addEventListener('menuLayoutChanged', function(e) {
4145    const newLayout = e.detail.layout;
4146    console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing);
4147
4148    // Update the global currentMenuLayout variable if you have one
4149    if (typeof currentMenuLayout !== 'undefined') {
4150        currentMenuLayout = newLayout;
4151    }
4152
4153    const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected
4154
4155    // Always clear and re-render the appropriate menu structure for the new layout.
4156    // This is important for style changes (like font size) that affect dimensions.
4157    const galleryTreeContainer = document.getElementById('galleryTree');
4158    const mobileHeaderContent = document.querySelector('.mobile-header-content');
4159
4160    if (galleryTreeContainer) {
4161        galleryTreeContainer.innerHTML = ''; // Clear existing tree
4162    }
4163    if (mobileHeaderContent) {
4164        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container');
4165        if (horizontalMenuContainer) {
4166            horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself
4167        }
4168    }
4169
4170    // Render the correct menu structure based on the new layout
4171    if (newLayout === 'horizontal') {
4172        if (typeof renderHorizontalMenu === 'function') {
4173            renderHorizontalMenu();
4174        }
4175        // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal
4176    } else { // 'sidebar' or 'top'
4177        if (typeof renderGalleries === 'function') {
4178            renderGalleries();
4179        }
4180        // CSS rules will handle sidebar visibility
4181    }
4182
4183    // If currently in edit mode, ensure the UI components for editing are correctly displayed
4184    // for the new layout, without fully toggling the mode off and on.
4185    if (isEditing && typeof window.updateGlobalEditState === 'function') {
4186        console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.');
4187
4188        // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode.
4189        // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar.
4190        window.updateGlobalEditState(true);
4191
4192        // 2. Re-initialize sortables. It's often safer to destroy existing ones first.
4193        if (typeof initializeNestedSortables === 'function') {
4194            if (typeof destroyNestedSortables === 'function') {
4195                destroyNestedSortables();
4196            }
4197            setTimeout(initializeNestedSortables, 100); // Delay for DOM updates
4198        }
4199        // Also reinitialize for SidebarManager if it exists and handles its own sortables
4200        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
4201             setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150);
4202        }
4203
4204        // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible
4205        const editControls = document.querySelector('.edit-controls');
4206        if (editControls) {
4207            editControls.style.display = 'flex'; // Or your default display type for these controls
4208            editControls.classList.add('visible');
4209        }
4210
4211        // 4. If the layout is now horizontal, the sidebar (for editing) should be visible.
4212        // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }`
4213        // should handle this. This log is for confirmation.
4214        if (newLayout === 'horizontal' && sidebar) {
4215            console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.');
4216        }
4217        
4218        // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set.
4219        if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && 
4220            window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) {
4221            window.PageManager.setEditMode(true);
4222        }
4223
4224    } else if (!isEditing) {
4225        // If not in edit mode, CSS should handle the view state.
4226        // This block can be used for any explicit view mode adjustments if CSS isn't sufficient.
4227        console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.');
4228    }
4229
4230    // Update mobile title and close mobile menu if open, regardless of edit mode
4231    if (typeof updateMobileTitle === 'function') {
4232        updateMobileTitle();
4233    }
4234    if (typeof closeMobileMenu === 'function') {
4235        closeMobileMenu();
4236    }
4237
4238    // Re-render the active page so row-band insets and recipe layout match the new menu shell
4239    if (window.PageManager && typeof window.PageManager.renderElements === 'function' && window.PageManager.currentPageId) {
4240        console.log('menuLayoutChanged: Re-rendering page', window.PageManager.currentPageId);
4241        window.PageManager.renderElements(window.PageManager.currentPageId);
4242    }
4243    if (typeof ensureCorrectLayoutApplied === 'function') {
4244        setTimeout(ensureCorrectLayoutApplied, 0);
4245    }
4246});
4247
4248
4249// Make sure getFirstSidebarElementForHeader is available if called from other scripts
4250if (typeof window.getFirstSidebarElementForHeader === 'undefined') {
4251  window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader;
4252}
4253
4254// Make sure getFirstSocialElementForHeader is available if called from other scripts
4255if (typeof window.getFirstSocialElementForHeader === 'undefined') {
4256  window.getFirstSocialElementForHeader = getFirstSocialElementForHeader;
4257}
4258
4259// Make sure renderSocialIcons is available if called from other scripts
4260if (typeof window.renderSocialIcons === 'undefined') {
4261  window.renderSocialIcons = renderSocialIcons;
4262}
4263if (typeof window.renderHorizontalMenu === 'undefined') {
4264    window.renderHorizontalMenu = renderHorizontalMenu;
4265}
4266if (typeof window.createHorizontalMenuItem === 'undefined') {
4267    window.createHorizontalMenuItem = createHorizontalMenuItem;
4268}
4269if (typeof window.createHorizontalSubMenuItem === 'undefined') {
4270    window.createHorizontalSubMenuItem = createHorizontalSubMenuItem;
4271}
4272if (typeof window.updateActiveStatesHorizontal === 'undefined') {
4273    window.updateActiveStatesHorizontal = updateActiveStatesHorizontal;
4274}
4275// Ensure toggleEditMode is globally available if it's the primary one.
4276if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder
4277    window.toggleEditMode = toggleEditMode;
4278}
4279
4280
4281// Helper function to toggle edit controls visibility
4282function toggleEditControlsVisibility(show) {
4283  const editControls = document.querySelector('.edit-controls');
4284  if (editControls) {
4285    if (show) {
4286      editControls.classList.add('visible');
4287      editControls.style.display = 'flex';
4288    } else {
4289      editControls.classList.remove('visible');
4290      editControls.style.display = 'none';
4291    }
4292  }
4293}
4294
4295/** Reliable + shortcut across US/Intl layouts, numpad, and browsers (key/keyCode differs). */
4296function isHydraPlusKey(e) {
4297  if (e.repeat) return false;
4298  if (e.ctrlKey || e.metaKey || e.altKey) return false;
4299  if (e.key === '+' || e.key === 'Add') return true;
4300  if (e.code === 'NumpadAdd') return true;
4301  if ((e.code === 'Equal' || e.key === '=') && e.shiftKey) return true;
4302  if (e.keyCode === 187 && e.shiftKey) return true;
4303  if (e.keyCode === 107) return true;
4304  return false;
4305}
4306
4307function handleHydraPlusShortcut() {
4308  try {
4309    const token = window.didToken || localStorage.getItem('hydra_auth_token');
4310    const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
4311    if (token && isAdmin) {
4312      console.log('User already logged in - toggling edit mode');
4313      if (typeof window.toggleEditMode === 'function') {
4314        window.toggleEditMode();
4315      } else if (typeof toggleEditMode === 'function') {
4316        toggleEditMode();
4317      } else {
4318        console.warn('toggleEditMode function not found');
4319      }
4320    } else {
4321      const p = startOTPLogin();
4322      if (p && typeof p.then === 'function') {
4323        p.catch(function(err) {
4324          console.error('startOTPLogin failed:', err);
4325        });
4326      }
4327    }
4328  } catch (err) {
4329    console.error('Hydra + key handler error:', err);
4330  }
4331}
4332
4333document.addEventListener('keydown', function(e) {
4334  if (!isHydraPlusKey(e)) return;
4335  e.preventDefault();
4336  handleHydraPlusShortcut();
4337}, true);
4338
4339// Custom HTML pages load in a sandboxed iframe; + is captured there unless forwarded via postMessage.
4340function isHydraCustomPageHtmlPlusMessage(e) {
4341  if (!e.data || e.data.source !== 'hydra-custom-page-html' || e.data.type !== 'plus-key') return false;
4342  if (e.origin === window.location.origin) return true;
4343  // Sandboxed iframes (no allow-same-origin) post with origin "null" — accept only from our iframe.
4344  if (e.origin !== 'null' || !e.source) return false;
4345  const iframes = document.querySelectorAll('iframe.page-custom-html-iframe');
4346  for (let i = 0; i < iframes.length; i++) {
4347    if (iframes[i].contentWindow === e.source) return true;
4348  }
4349  return false;
4350}
4351
4352window.addEventListener('message', function(e) {
4353  if (!isHydraCustomPageHtmlPlusMessage(e)) return;
4354  handleHydraPlusShortcut();
4355});
4356
4357/** Option+/ (÷) opens Help → Pathways. Option+? opens Help → Ask (help admin only). */
4358function isHydraHelpKey(e) {
4359  if (e.repeat) return false;
4360  if (e.ctrlKey || e.metaKey) return false;
4361  if (!e.altKey) return false;
4362  if (e.shiftKey) return false;
4363  if (e.key === '÷') return true;
4364  if (e.code === 'Slash' && e.altKey) return true;
4365  return false;
4366}
4367
4368function isHydraHelpAskKey(e) {
4369  if (e.repeat) return false;
4370  if (e.ctrlKey || e.metaKey) return false;
4371  if (!e.altKey || !e.shiftKey) return false;
4372  if (e.key === '?' || e.key === '¿') return true;
4373  if (e.code === 'Slash') return true;
4374  return false;
4375}
4376
4377document.addEventListener('keydown', function(e) {
4378  if (!window.HYDRA_HELP_ENABLED) return;
4379  var t = e.target;
4380  if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
4381  if (isHydraHelpAskKey(e)) {
4382    e.preventDefault();
4383    e.stopPropagation();
4384    openHydraHelp({ initialTab: 'ask' });
4385    return;
4386  }
4387  if (!isHydraHelpKey(e)) return;
4388  e.preventDefault();
4389  e.stopPropagation();
4390  openHydraHelp();
4391}, true);
4392
4393// Email OTP Authentication with Enhanced Debugging
4394
4395
4396// Add this function to the client-side code
4397// Improved client-side backup function with better token handling
4398async function createLoginBackup() {
4399  try {
4400    console.log('Attempting to create login backup...');
4401    
4402    // Get authentication info from multiple possible sources
4403    let token = window.didToken || localStorage.getItem('hydra_auth_token');
4404    const email = localStorage.getItem('hydra_auth_email') || 
4405                  (window.userMetadata ? window.userMetadata.email : '') ||
4406                  '';
4407    
4408    // Check if we have authentication
4409    if (!token) {
4410      console.error('No auth token available for backup');
4411      return;
4412    }
4413    
4414    if (!email) {
4415      console.error('No email available for backup - make sure it was stored during login');
4416      return;
4417    }
4418    
4419    console.log(`Creating backup with token (${token.length} chars) for ${email}`);
4420    
4421    // Format the token correctly - VERY IMPORTANT
4422    // If it's already a hydra token (starts with hydra:), use it as is
4423    // Otherwise, add the hydra: prefix
4424    const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`;
4425    
4426    console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`);
4427    
4428    // Get the API URL with preview handling if needed
4429    const backupUrl = typeof getApiUrl === 'function' ? 
4430      getApiUrl('/api/create-backup') : 
4431      '/api/create-backup';
4432    
4433    console.log(`Sending backup request to: ${backupUrl}`);
4434    
4435    // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header
4436    const response = await fetch(backupUrl, {
4437      method: 'POST',
4438      headers: {
4439        'Authorization': `Bearer ${formattedToken}`,
4440        'Content-Type': 'application/json',
4441        'X-User-Email': email, // Include email as fallback
4442        'X-API-Request': 'true' // Mark as API request
4443      }
4444    });
4445    
4446    // Handle response
4447    if (!response.ok) {
4448      const errorText = await response.text();
4449      let errorData;
4450      try {
4451        // Try to parse as JSON
4452        errorData = JSON.parse(errorText);
4453        console.error('Backup failed:', errorData.error, errorData);
4454      } catch {
4455        // Not JSON, log raw text
4456        console.error('Backup failed with status', response.status, errorText);
4457      }
4458      return;
4459    }
4460    
4461    // Parse successful response
4462    const result = await response.json();
4463    console.log(`✅ Backup created successfully: ${result.backup}`);
4464  } catch (error) {
4465    console.error('Error in backup process:', error);
4466  }
4467}
4468
4469// Initialize Magic with detailed logging
4470function initMagic() {
4471  try {
4472    // If Magic is already initialized, return it
4473    if (window.magic) {
4474      return window.magic;
4475    }
4476    
4477    console.log('Initializing Magic SDK...');
4478    
4479    // Initialize with minimal options
4480    const magic = new Magic('pk_live_E0B68BA6DCA8C75F', {
4481      testMode: false
4482    });
4483    
4484    // Store reference globally
4485    window.magic = magic;
4486    
4487    console.log('Magic SDK initialized successfully');
4488    return magic;
4489  } catch (error) {
4490    console.error('Error initializing Magic:', error);
4491    throw error;
4492  }
4493}
4494
4495// Start OTP login flow with proper event handling
4496async function startOTPLogin() {
4497  try {
4498    // Initialize Magic
4499    const magic = initMagic();
4500    
4501    // Get email from user (styled overlay — same auth path as native prompt)
4502    const email = await showEmailLoginOverlay();
4503    if (!email) return; // User cancelled
4504    
4505    console.log('Starting Email OTP login for:', email);
4506    showLoadingOverlay('Sending verification code...');
4507    
4508    // Check if Magic's event enum types are available - for newer Magic SDK versions
4509    // If not available, fall back to string-based events
4510    const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && 
4511                    typeof LoginWithEmailOTPEventEmit !== 'undefined';
4512    
4513    console.log('Using enum-based events:', useEnums);
4514    
4515    // Create a timeout handler
4516    let emailSentTimeout = setTimeout(() => {
4517      console.log('No email-otp-sent event received, using fallback UI');
4518      hideLoadingOverlay();
4519      showOTPInputOverlay(email);
4520    }, 10000);
4521    
4522    // Create OTP login handle with showUI: false
4523    const handle = magic.auth.loginWithEmailOTP({ 
4524      email, 
4525      showUI: false,
4526      deviceCheckUI: false
4527    });
4528    
4529    // Track authentication state
4530    window.pendingAuth = {
4531      email,
4532      handle
4533    };
4534    
4535    // Listen for events - using both enum-based and string-based event handling
4536    console.log('Setting up OTP event handlers...');
4537    
4538    // Email OTP sent event
4539    if (useEnums) {
4540      handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => {
4541        console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully');
4542        clearTimeout(emailSentTimeout);
4543        hideLoadingOverlay();
4544        showOTPInputOverlay(email);
4545      });
4546    }
4547    
4548    // Also listen for string-based event (for backward compatibility)
4549    handle.on('email-otp-sent', () => {
4550      console.log('EVENT: email-otp-sent - Email was sent successfully');
4551      clearTimeout(emailSentTimeout);
4552      hideLoadingOverlay();
4553      showOTPInputOverlay(email);
4554    });
4555    
4556    // Invalid OTP event
4557    if (useEnums) {
4558      handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => {
4559        console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered');
4560        showErrorMessage('Invalid verification code. Please try again.');
4561        updateOTPInputForRetry();
4562      });
4563    }
4564    
4565    // Also listen for string-based event
4566    handle.on('invalid-email-otp', () => {
4567      console.log('EVENT: invalid-email-otp - Invalid code entered');
4568      showErrorMessage('Invalid verification code. Please try again.');
4569      updateOTPInputForRetry();
4570    });
4571    
4572    async function handleLoginSuccess(result) {
4573  console.log('OTP verification successful, checking admin status...');
4574  hideOTPInputOverlay();
4575  hideLoadingOverlay();
4576
4577  try {
4578    window.didToken = result;
4579    console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none');
4580
4581    const userInfo = await magic.user.getInfo();
4582    window.userMetadata = userInfo;
4583    console.log('User info retrieved:', userInfo);
4584
4585    const isUserAdmin = await checkAdminStatus(result);
4586
4587    if (!isUserAdmin && !window.isAdmin) {
4588      console.log('User is not on admin list, login denied');
4589      showErrorMessage('Login failed');
4590      clearHydraClientAuthStorage();
4591      resetAuthUI();
4592      try {
4593        await magic.user.logout();
4594      } catch (logoutError) {
4595        console.error('Error logging out non-admin user from Magic:', logoutError);
4596      }
4597      window.pendingAuth = null;
4598      return;
4599    }
4600
4601    console.log('Admin status check completed');
4602    window.isAuthenticated = true;
4603    isAuthenticated = true;
4604    showSuccessMessage('Login successful!');
4605
4606    // Store the ID token (JWT) for API calls
4607    try {
4608      const idToken = await magic.user.getIdToken();
4609      if (idToken && idToken.includes('.')) {
4610        console.log('Got proper JWT token from Magic.link');
4611        localStorage.setItem('hydra_auth_token', idToken);
4612      } else if (userInfo && userInfo.email) {
4613        const userToken = {
4614          email: userInfo.email,
4615          iat: Math.floor(Date.now() / 1000),
4616          exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60)
4617        };
4618        const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' }));
4619        const payload = btoa(JSON.stringify(userToken));
4620        const jwtToken = header + '.' + payload + '.nosignature';
4621        localStorage.setItem('hydra_auth_token', jwtToken);
4622        localStorage.setItem('hydra_auth_email', userInfo.email);
4623      }
4624      localStorage.setItem('hydra_did_token', result);
4625    } catch (tokenError) {
4626      console.error('Error getting ID token:', tokenError);
4627      localStorage.setItem('hydra_auth_token', result);
4628    }
4629
4630    // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy
4631
4632    // 1. Add state classes to body
4633    document.body.classList.add('hydra-authenticated');
4634    if (window.isAdmin) {
4635      document.body.classList.add('hydra-admin');
4636    }
4637
4638    // 2. Direct DOM manipulation
4639    const logoutButton = document.getElementById('logoutButton');
4640    const editButton = document.getElementById('editButton');
4641
4642    // Create new buttons if they don't exist
4643    if (!logoutButton) {
4644      console.log('Creating logout button');
4645      const newLogoutButton = document.createElement('button');
4646      newLogoutButton.id = 'logoutButton';
4647      newLogoutButton.className = 'btn';
4648      newLogoutButton.textContent = 'Logout';
4649      newLogoutButton.onclick = logout;
4650      newLogoutButton.style.display = 'block';
4651
4652      const sidebarHeader = document.querySelector('.sidebar-header');
4653      if (sidebarHeader) {
4654        sidebarHeader.appendChild(newLogoutButton);
4655      } else {
4656        document.body.appendChild(newLogoutButton);
4657      }
4658    } else {
4659      logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4660    }
4661
4662    if (!editButton && window.isAdmin) {
4663      console.log('Creating edit button');
4664      const newEditButton = document.createElement('button');
4665      newEditButton.id = 'editButton';
4666      newEditButton.className = 'btn btn-primary';
4667      newEditButton.innerHTML = `
4668        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4669          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
4670          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
4671        </svg>
4672        Edit
4673      `;
4674      newEditButton.onclick = toggleEditMode;
4675      newEditButton.style.display = 'block';
4676
4677      const sidebarHeader = document.querySelector('.sidebar-header');
4678      if (sidebarHeader) {
4679        sidebarHeader.appendChild(newEditButton);
4680      } else {
4681        document.body.appendChild(newEditButton);
4682      }
4683    } else if (editButton && window.isAdmin) {
4684      editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;';
4685    }
4686
4687    // 3. Dispatch event for other listeners
4688    document.dispatchEvent(new CustomEvent('login-complete', {
4689      detail: {
4690        isAdmin: window.isAdmin,
4691        email: window.userMetadata ? window.userMetadata.email : null
4692      }
4693    }));
4694
4695    window.pendingAuth = null;
4696
4697    if (typeof checkAuth === 'function') {
4698      console.log('Running existing checkAuth function');
4699      setTimeout(checkAuth, 500);
4700    }
4701  } catch (error) {
4702    console.error('Error in login success handling:', error);
4703    showErrorMessage('Login failed');
4704    clearHydraClientAuthStorage();
4705    resetAuthUI();
4706    window.pendingAuth = null;
4707  }
4708}
4709
4710
4711    // Completion event
4712    handle.on('done', async (result) => {
4713      await handleLoginSuccess(result);
4714    });
4715    
4716    // Error event
4717    handle.on('error', (error) => {
4718      console.error('EVENT: error - OTP login error:', error);
4719      hideLoadingOverlay();
4720      hideOTPInputOverlay();
4721      showErrorMessage('Login failed: ' + (error.message || 'Unknown error'));
4722      
4723      // Clear timeout if still active
4724      clearTimeout(emailSentTimeout);
4725      
4726      // Clean up
4727      window.pendingAuth = null;
4728    });
4729    
4730    console.log('OTP event handlers set up successfully');
4731    
4732  } catch (error) {
4733    console.error('Error starting OTP login:', error);
4734    hideLoadingOverlay();
4735    hideOTPInputOverlay();
4736    showErrorMessage('Error starting login: ' + error.message);
4737  }
4738}
4739
4740// Submit OTP code with support for enum-based events
4741function submitOTP(code) {
4742  try {
4743    // Validate code input
4744    if (!code || code.trim() === '') {
4745      showErrorMessage('Please enter the verification code.');
4746      return;
4747    }
4748    
4749    // Check if we have a pending authentication
4750    if (!window.pendingAuth || !window.pendingAuth.handle) {
4751      showErrorMessage('No active login session. Please try again.');
4752      hideOTPInputOverlay();
4753      return;
4754    }
4755    
4756    console.log('Submitting OTP code...');
4757    showLoadingOverlay('Verifying code...');
4758    
4759    // Check if enum types are available
4760    const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4761    
4762    // Verify the OTP code
4763    if (useEnums) {
4764      window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code);
4765    } else {
4766      window.pendingAuth.handle.emit('verify-email-otp', code);
4767    }
4768    
4769  } catch (error) {
4770    console.error('Error submitting OTP:', error);
4771    hideLoadingOverlay();
4772    showErrorMessage('Error verifying code: ' + error.message);
4773  }
4774}
4775
4776// Cancel OTP login with support for enum-based events
4777function cancelOTPLogin() {
4778  try {
4779    // Check if we have a pending authentication
4780    if (window.pendingAuth && window.pendingAuth.handle) {
4781      // Check if enum types are available
4782      const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined';
4783      
4784      // Emit cancel event
4785      if (useEnums) {
4786        window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel);
4787      } else {
4788        window.pendingAuth.handle.emit('cancel');
4789      }
4790      
4791      console.log('OTP login cancelled');
4792    }
4793    
4794    // Clean up
4795    window.pendingAuth = null;
4796    
4797    // Hide any overlays
4798    hideEmailLoginOverlay();
4799    hideOTPInputOverlay();
4800    hideLoadingOverlay();
4801  } catch (error) {
4802    console.error('Error cancelling OTP login:', error);
4803  }
4804}
4805
4806// Update OTP input for retry
4807function updateOTPInputForRetry() {
4808  const otpInput = document.getElementById('otp-input');
4809  if (otpInput) {
4810    otpInput.value = '';
4811    otpInput.focus();
4812    
4813    // Add a shake animation for visual feedback
4814    otpInput.classList.add('shake');
4815    
4816    // Remove the animation class after it completes
4817    setTimeout(() => {
4818      otpInput.classList.remove('shake');
4819    }, 500);
4820  }
4821  
4822  // Hide loading overlay if it's visible
4823  hideLoadingOverlay();
4824}
4825
4826function isValidLoginEmail(value) {
4827  const trimmed = String(value || '').trim();
4828  if (!trimmed || trimmed.indexOf(' ') >= 0) return false;
4829  const at = trimmed.indexOf('@');
4830  if (at <= 0 || at !== trimmed.lastIndexOf('@')) return false;
4831  const local = trimmed.slice(0, at);
4832  const domain = trimmed.slice(at + 1);
4833  if (!local || !domain || domain.indexOf('.') < 1) return false;
4834  if (domain.startsWith('.') || domain.endsWith('.')) return false;
4835  return true;
4836}
4837
4838function showEmailLoginOverlay() {
4839  hideEmailLoginOverlay();
4840
4841  return new Promise((resolve) => {
4842    const overlay = document.createElement('div');
4843    overlay.id = 'email-login-overlay';
4844    overlay.className = 'pe-auth-overlay';
4845
4846    const content = document.createElement('div');
4847    content.className = 'pe-auth-shell';
4848
4849    const logo = document.createElement('img');
4850    logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4851    logo.alt = 'Neon Sky Logo';
4852    logo.className = 'pe-auth-shell__logo';
4853
4854    const description = document.createElement('p');
4855    description.className = 'pe-auth-shell__desc';
4856    description.textContent = 'Enter your email address to receive a verification code.';
4857
4858    const form = document.createElement('form');
4859    form.noValidate = true;
4860    form.onsubmit = function(e) {
4861      e.preventDefault();
4862      const raw = input.value.trim();
4863      if (!isValidLoginEmail(raw)) {
4864        input.classList.add('shake');
4865        setTimeout(() => input.classList.remove('shake'), 500);
4866        return;
4867      }
4868      hideEmailLoginOverlay();
4869      resolve(raw);
4870    };
4871
4872    const input = document.createElement('input');
4873    input.id = 'email-login-input';
4874    input.type = 'email';
4875    input.name = 'email';
4876    input.placeholder = '[email protected]';
4877    input.autocomplete = 'email';
4878    input.inputMode = 'email';
4879    input.required = true;
4880    input.className = 'pe-auth-shell__input pe-auth-shell__input--email';
4881    const savedEmail = localStorage.getItem('hydra_auth_email');
4882    if (savedEmail && isValidLoginEmail(savedEmail)) {
4883      input.value = savedEmail.trim();
4884    }
4885    form.appendChild(input);
4886
4887    const buttonContainer = document.createElement('div');
4888    buttonContainer.className = 'pe-auth-actions';
4889
4890    const continueButton = document.createElement('button');
4891    continueButton.type = 'submit';
4892    continueButton.textContent = 'Continue';
4893    continueButton.className = 'pe-btn pe-btn--primary';
4894    buttonContainer.appendChild(continueButton);
4895
4896    const cancelButton = document.createElement('button');
4897    cancelButton.type = 'button';
4898    cancelButton.textContent = 'Cancel';
4899    cancelButton.className = 'pe-btn';
4900    cancelButton.onclick = function() {
4901      hideEmailLoginOverlay();
4902      resolve(null);
4903    };
4904    buttonContainer.appendChild(cancelButton);
4905    form.appendChild(buttonContainer);
4906
4907    content.appendChild(logo);
4908    content.appendChild(description);
4909    content.appendChild(form);
4910    overlay.appendChild(content);
4911    document.body.appendChild(overlay);
4912
4913    setTimeout(() => {
4914      input.focus();
4915      if (input.value) {
4916        input.select();
4917      }
4918    }, 100);
4919  });
4920}
4921
4922function hideEmailLoginOverlay() {
4923  const overlay = document.getElementById('email-login-overlay');
4924  if (overlay && overlay.parentNode) {
4925    overlay.parentNode.removeChild(overlay);
4926  }
4927}
4928
4929// Show OTP input overlay - improved version
4930function showOTPInputOverlay(email) {
4931  hideOTPInputOverlay();
4932  hideEmailLoginOverlay();
4933
4934  const overlay = document.createElement('div');
4935  overlay.id = 'otp-input-overlay';
4936  overlay.className = 'pe-auth-overlay';
4937
4938  const content = document.createElement('div');
4939  content.className = 'pe-auth-shell';
4940
4941  const logo = document.createElement('img');
4942  logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png';
4943  logo.alt = 'Neon Sky Logo';
4944  logo.className = 'pe-auth-shell__logo';
4945
4946  const description = document.createElement('p');
4947  description.className = 'pe-auth-shell__desc';
4948  const descPrefix = document.createTextNode("We've sent a verification code to ");
4949  const descEmail = document.createElement('strong');
4950  descEmail.textContent = String(email || '');
4951  const descSuffix = document.createTextNode('. Please check your email and enter the code below:');
4952  description.appendChild(descPrefix);
4953  description.appendChild(descEmail);
4954  description.appendChild(document.createElement('br'));
4955  description.appendChild(descSuffix);
4956
4957  const form = document.createElement('form');
4958  form.onsubmit = function(e) {
4959    e.preventDefault();
4960    const otpInput = document.getElementById('otp-input');
4961    if (otpInput && otpInput.value) {
4962      submitOTP(otpInput.value);
4963    }
4964  };
4965
4966  const input = document.createElement('input');
4967  input.id = 'otp-input';
4968  input.type = 'text';
4969  input.placeholder = 'Enter code';
4970  input.pattern = '[0-9]*';
4971  input.inputMode = 'numeric';
4972  input.autocomplete = 'one-time-code';
4973  input.className = 'pe-auth-shell__input';
4974  form.appendChild(input);
4975
4976  const buttonContainer = document.createElement('div');
4977  buttonContainer.className = 'pe-auth-actions';
4978
4979  const verifyButton = document.createElement('button');
4980  verifyButton.type = 'submit';
4981  verifyButton.textContent = 'Verify';
4982  verifyButton.className = 'pe-btn pe-btn--primary';
4983  buttonContainer.appendChild(verifyButton);
4984
4985  const cancelButton = document.createElement('button');
4986  cancelButton.type = 'button';
4987  cancelButton.textContent = 'Cancel';
4988  cancelButton.className = 'pe-btn';
4989  cancelButton.onclick = function() {
4990    cancelOTPLogin();
4991  };
4992  buttonContainer.appendChild(cancelButton);
4993  form.appendChild(buttonContainer);
4994
4995  const resendContainer = document.createElement('div');
4996  resendContainer.className = 'pe-auth-resend';
4997
4998  const resendText = document.createElement('span');
4999  resendText.textContent = "Didn't receive the code? ";
5000
5001  const resendLink = document.createElement('a');
5002  resendLink.textContent = 'Resend';
5003  resendLink.href = '#';
5004  resendLink.onclick = function(e) {
5005    e.preventDefault();
5006    cancelOTPLogin();
5007    setTimeout(() => {
5008      startOTPLogin();
5009    }, 500);
5010  };
5011
5012  resendContainer.appendChild(resendText);
5013  resendContainer.appendChild(resendLink);
5014  form.appendChild(resendContainer);
5015
5016  content.appendChild(logo);
5017  content.appendChild(description);
5018  content.appendChild(form);
5019  overlay.appendChild(content);
5020  document.body.appendChild(overlay);
5021
5022  setTimeout(() => {
5023    input.focus();
5024  }, 100);
5025}
5026
5027// Hide OTP input overlay
5028function hideOTPInputOverlay() {
5029  const overlay = document.getElementById('otp-input-overlay');
5030  if (overlay && overlay.parentNode) {
5031    overlay.parentNode.removeChild(overlay);
5032  }
5033}
5034
5035// Utility function to show loading overlay
5036function showLoadingOverlay(message = 'Loading...') {
5037  hideLoadingOverlay();
5038
5039  const overlay = document.createElement('div');
5040  overlay.id = 'loading-overlay';
5041  overlay.className = 'pe-auth-overlay';
5042
5043  const content = document.createElement('div');
5044  content.className = 'pe-auth-shell';
5045
5046  const loaderContainer = document.createElement('div');
5047  loaderContainer.className = 'pe-auth-shell__loader';
5048  loaderContainer.innerHTML = `
5049    <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
5050      <defs>
5051        <filter id="svgSpinnersGooeyBalls20">
5052          <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
5053          <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"/>
5054          <feBlend in="SourceGraphic" in2="z"/>
5055        </filter>
5056      </defs>
5057      <g filter="url(#svgSpinnersGooeyBalls20)">
5058        <circle cx="5" cy="12" r="4" fill="currentColor">
5059          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
5060        </circle>
5061        <circle cx="19" cy="12" r="4" fill="currentColor">
5062          <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
5063        </circle>
5064        <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
5065      </g>
5066    </svg>
5067  `;
5068  content.appendChild(loaderContainer);
5069
5070  const messageEl = document.createElement('p');
5071  messageEl.className = 'pe-auth-shell__message';
5072  messageEl.textContent = message;
5073  content.appendChild(messageEl);
5074
5075  overlay.appendChild(content);
5076  document.body.appendChild(overlay);
5077}
5078
5079// Utility function to hide loading overlay
5080function hideLoadingOverlay() {
5081  const overlay = document.getElementById('loading-overlay');
5082  if (overlay && overlay.parentNode) {
5083    overlay.parentNode.removeChild(overlay);
5084  }
5085}
5086
5087// Utility function to show success message
5088function showSuccessMessage(message, duration = 3000) {
5089  const existingToast = document.getElementById('success-toast');
5090  if (existingToast && existingToast.parentNode) {
5091    existingToast.parentNode.removeChild(existingToast);
5092  }
5093
5094  const toast = document.createElement('div');
5095  toast.id = 'success-toast';
5096  toast.className = 'pe-auth-toast';
5097  toast.textContent = message;
5098
5099  document.body.appendChild(toast);
5100
5101  setTimeout(() => {
5102    if (toast.parentNode) {
5103      toast.parentNode.removeChild(toast);
5104    }
5105  }, duration);
5106}
5107
5108// Utility function to show error message
5109function showErrorMessage(message, duration = 5000) {
5110  const existingToast = document.getElementById('error-toast');
5111  if (existingToast && existingToast.parentNode) {
5112    existingToast.parentNode.removeChild(existingToast);
5113  }
5114
5115  const toast = document.createElement('div');
5116  toast.id = 'error-toast';
5117  toast.className = 'pe-auth-toast pe-auth-toast--error';
5118  toast.textContent = message;
5119
5120  document.body.appendChild(toast);
5121
5122  setTimeout(() => {
5123    if (toast.parentNode) {
5124      toast.parentNode.removeChild(toast);
5125    }
5126  }, duration);
5127}
5128
5129// UPDATED: Check authentication status with class-based approach
5130async function checkAuth() {
5131  try {
5132    if (!magic) {
5133      console.log('Magic not initialized, skipping auth check');
5134      return;
5135    }
5136    
5137    // Prevent multiple simultaneous auth checks
5138    if (window._checkingAuth) return;
5139    window._checkingAuth = true;
5140    
5141    console.log('Checking auth status...');
5142    
5143    isAuthenticated = await magic.user.isLoggedIn();
5144    console.log('isLoggedIn check:', isAuthenticated);
5145    
5146    if (isAuthenticated) {
5147      // Add the authenticated class 
5148      document.body.classList.add('hydra-authenticated');
5149      
5150      // Get user metadata
5151      userMetadata = await magic.user.getInfo();
5152      console.log('User info:', userMetadata);
5153      
5154      // Get ID token for API calls (JWT format)
5155      const rawDidToken = await magic.user.getIdToken();
5156      console.log('ID token acquired, length:', rawDidToken.length);
5157      
5158      // Do NOT write the raw DID token to localStorage here — checkAdminStatus will
5159      // receive the server-issued hydra token and store that instead, which is what
5160      // the save function needs. Writing the DID token now would overwrite a valid
5161      // hydra token that may already be stored from the OTP login flow.
5162      
5163      // Check if user is admin; on success this updates both localStorage and didToken
5164      // with a proper hydra token via checkAdminStatus → TokenManager.storeToken
5165      await checkAdminStatus(rawDidToken);
5166    } else {
5167      // Magic has no session — still try Hydra OTP token so /_manage and return visits work
5168      const hydraOnly = localStorage.getItem('hydra_auth_token');
5169      if (hydraOnly) {
5170        try {
5171          await checkAdminStatus(hydraOnly);
5172        } catch (e) {
5173          console.error('Hydra token restore after Magic logout failed:', e);
5174        }
5175      } else {
5176        resetAuthUI();
5177      }
5178    }
5179    
5180    // Release lock
5181    window._checkingAuth = false;
5182  } catch (error) {
5183    console.error('Error checking authentication:', error);
5184    window._checkingAuth = false;
5185    resetAuthUI();
5186  }
5187}
5188
5189
5190async function checkAdminStatus(token) {
5191  try {
5192    console.log('Checking admin status...');
5193
5194    // Add user email to headers if available
5195    let userEmail = null;
5196    if (window.userMetadata && window.userMetadata.email) {
5197      userEmail = window.userMetadata.email;
5198      console.log('Using email from userMetadata:', userEmail);
5199    } else {
5200        userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage
5201        if (userEmail) console.log('Using email from localStorage:', userEmail);
5202    }
5203
5204    if (!token) {
5205      console.log('No token provided to checkAdminStatus');
5206      return false; // Indicate failure
5207    }
5208
5209    // Format the token correctly for the API call
5210    let formattedToken = token;
5211    // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix
5212    if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) {
5213        formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer
5214    } else if (token.startsWith('hydra:')) {
5215        formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens
5216    }
5217    // If it already starts with Bearer, use as is
5218
5219    // Get the API URL, handling preview mode
5220    const apiUrl = typeof getApiUrl === 'function' ?
5221      getApiUrl('/api/check-admin') :
5222      '/api/check-admin';
5223
5224    console.log('Sending admin check request to:', apiUrl);
5225
5226    // Create request headers
5227    const headers = {
5228      'Authorization': formattedToken,
5229      'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow
5230    };
5231
5232    if (userEmail) {
5233      headers['X-User-Email'] = userEmail;
5234    }
5235
5236    console.log('Admin check headers:', {
5237      'Authorization': `${formattedToken.substring(0, 20)}...`,
5238      'X-User-Email': userEmail || 'Not available'
5239    });
5240
5241    // Add timeout to the fetch request
5242    const controller = new AbortController();
5243    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
5244
5245    let data;
5246    try {
5247      const response = await fetch(apiUrl, {
5248        method: 'GET',
5249        headers: headers,
5250        signal: controller.signal
5251      });
5252
5253      clearTimeout(timeoutId); // Clear timeout if fetch succeeds
5254
5255      console.log('Admin check response status:', response.status);
5256
5257      // Get the response as text first for better debugging
5258      const responseText = await response.text();
5259      console.log('Admin check response length:', responseText.length);
5260      console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...');
5261
5262
5263      try {
5264        data = JSON.parse(responseText);
5265        console.log('Admin check data:', data);
5266      } catch (parseError) {
5267        console.error('Error parsing admin check response:', parseError);
5268        // Handle cases where the response might not be JSON (e.g., HTML error page)
5269        // For preview URLs, let's still assume admin to allow editing flow
5270        if (window.location.hostname === 'preview.neonsky.app') {
5271          console.log('Preview URL detected, assuming admin status due to non-JSON response');
5272          isAdmin = true;
5273          document.body.classList.add('hydra-admin');
5274          localStorage.setItem('hydra_is_admin', 'true');
5275          forceShowEditUI(); // Attempt to show UI
5276          return true; // Indicate potential success for preview
5277        }
5278        throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`);
5279      }
5280
5281      if (!response.ok) {
5282          // Throw an error with details from the parsed JSON if available
5283          throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`);
5284      }
5285
5286      // Process the admin status
5287      isAdmin = data.isAdmin;
5288      window.isAdmin = isAdmin; // Update global flag
5289
5290      // Update classes based on admin status
5291      if (isAdmin) {
5292        document.body.classList.add('hydra-admin');
5293        localStorage.setItem('hydra_is_admin', 'true');
5294        // Store email if we got it from the server response
5295        if (data.email) {
5296            localStorage.setItem('hydra_auth_email', data.email);
5297        } else if (userEmail) {
5298            localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used
5299        }
5300      } else {
5301        document.body.classList.remove('hydra-admin');
5302        localStorage.removeItem('hydra_is_admin');
5303        localStorage.removeItem('hydra_auth_email'); // Clear email if not admin
5304      }
5305
5306      // Update siteId if provided
5307      if (data.siteId) {
5308        siteId = data.siteId;
5309        window.siteId = siteId;
5310        if (window.Parameters) window.Parameters.siteId = siteId;
5311      }
5312
5313      // Store new token if provided (Hydra token)
5314      if (data.hydraToken) {
5315        console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...');
5316        const cleanHydraToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken;
5317        // Always update the local didToken variable so saves immediately use the hydra token
5318        didToken = cleanHydraToken;
5319        // Store token globally and in localStorage using TokenManager if available
5320        if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
5321            window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || '');
5322        } else {
5323            // Fallback storage
5324            localStorage.setItem('hydra_auth_token', cleanHydraToken);
5325            if (data.email || userEmail) {
5326                localStorage.setItem('hydra_auth_email', data.email || userEmail);
5327            }
5328        }
5329        console.log('Stored credentials in localStorage');
5330      }
5331
5332      // Update UI based on admin status
5333      if (isAdmin) {
5334        console.log('Admin status confirmed, showing edit UI');
5335        forceShowEditUI(); // Ensure UI elements are visible
5336        // --- MODIFICATION START ---
5337        // If layout is horizontal, re-render the menu to show controls
5338        let currentLayout = 'sidebar'; // Default
5339        if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5340            currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5341        }
5342        if (currentLayout === 'horizontal') {
5343            console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.');
5344            if (typeof renderHorizontalMenu === 'function') {
5345                renderHorizontalMenu();
5346            } else {
5347                console.warn("checkAdminStatus: renderHorizontalMenu function not found.");
5348            }
5349        }
5350        // --- MODIFICATION END ---
5351        createLoginBackup(); // Create backup after successful login/admin check
5352      } else {
5353          console.log('User is not admin, resetting UI.');
5354          resetAuthUI(); // Ensure non-admin UI state
5355      }
5356
5357      return isAdmin; // Return the admin status
5358
5359    } catch (fetchError) {
5360      clearTimeout(timeoutId); // Clear timeout on error as well
5361      console.error('Fetch error during admin check:', fetchError);
5362
5363      // For preview URLs, still allow editing as a fallback
5364      if (window.location.hostname === 'preview.neonsky.app') {
5365        console.log('Preview URL + fetch error, still enabling editing');
5366        isAdmin = true;
5367        window.isAdmin = true;
5368        document.body.classList.add('hydra-admin');
5369        localStorage.setItem('hydra_is_admin', 'true');
5370        forceShowEditUI();
5371        return true; // Indicate potential success for preview
5372      }
5373
5374      // For non-preview, throw the error to indicate failure
5375      throw fetchError;
5376    }
5377  } catch (error) {
5378    console.error('Error in checkAdminStatus:', error);
5379    // Don't show alert here, let the calling function handle UI feedback
5380    // Reset UI to non-admin state on error
5381    resetAuthUI();
5382    return false; // Indicate failure
5383  }
5384}
5385
5386// UPDATED: Force showing edit UI with class-based approach
5387function forceShowEditUI() {
5388  console.log('Forcing admin/auth state for UI');
5389
5390  // Ensure global state flags are set
5391  isAuthenticated = true;
5392  isAdmin = true;
5393  window.isAuthenticated = true;
5394  window.isAdmin = true;
5395
5396  // Add admin/auth classes to body - CSS will handle button visibility
5397  document.body.classList.add('hydra-authenticated', 'hydra-admin');
5398
5399  // Store admin status in localStorage
5400  localStorage.setItem('hydra_is_admin', 'true');
5401
5402  // Show edit controls container ONLY if already in edit mode
5403  if (window.isInEditMode && window.isInEditMode()) {
5404    const editControls = document.querySelector('.edit-controls');
5405    if (editControls) {
5406      editControls.classList.add('visible');
5407      editControls.style.display = 'flex'; // Make sure container is flex
5408    }
5409    // Ensure controls inside items are visible if already editing
5410     document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5411        if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) {
5412             el.style.display = 'flex';
5413        }
5414    });
5415    
5416    // Show logout button in edit controls when in edit mode
5417    const logoutButton = document.getElementById('logoutButton');
5418    if (logoutButton) {
5419      logoutButton.style.display = 'flex';
5420      logoutButton.classList.add('visible');
5421    }
5422  } else {
5423     // Ensure edit controls container is hidden if not in edit mode
5424     const editControls = document.querySelector('.edit-controls');
5425     if (editControls) {
5426        editControls.classList.remove('visible');
5427        editControls.style.display = 'none';
5428     }
5429     
5430     // Hide logout button when not in edit mode
5431     const logoutButton = document.getElementById('logoutButton');
5432     if (logoutButton) {
5433       logoutButton.style.display = 'none';
5434       logoutButton.classList.remove('visible');
5435     }
5436  }
5437
5438  console.log('Admin/Auth classes set. CSS should handle button visibility.');
5439}
5440
5441// UPDATED: Reset auth UI with class-based approach  
5442function resetAuthUI() {
5443  document.body.classList.remove('hydra-authenticated');
5444  document.body.classList.remove('hydra-admin');
5445  
5446  
5447  const logoutButton = document.getElementById('logoutButton');
5448  if (logoutButton) {
5449    logoutButton.style.display = 'none';
5450    logoutButton.classList.remove('visible');
5451  }
5452  
5453  const editButton = document.getElementById('editButton');
5454  if (editButton) {
5455    editButton.style.display = 'none';
5456    editButton.classList.remove('visible');
5457  }
5458  
5459  // Remove admin status from localStorage
5460  localStorage.removeItem('hydra_is_admin');
5461  localStorage.removeItem('hydra_auth_token');
5462  
5463  if (isEditing) {
5464    toggleEditMode(); // Exit edit mode if active
5465  }
5466}
5467
5468// Debug function - add to your code during testing
5469function debugUIClasses() {
5470  console.log('Current body classes:', document.body.className);
5471  console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized'));
5472  console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated'));
5473  console.log('hydra-admin:', document.body.classList.contains('hydra-admin'));
5474  console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active'));
5475}
5476
5477
5478
5479async function logout() {
5480  console.log('Logout initiated');
5481  
5482  try {
5483    // Show loading indication (optional)
5484    showLoadingOverlay('Logging out...');
5485    
5486    // 1. Properly logout from Magic SDK
5487    if (window.magic) {
5488      try {
5489        await window.magic.user.logout();
5490        console.log('Magic SDK logout successful');
5491      } catch (magicError) {
5492        console.error('Error during Magic logout:', magicError);
5493        // Continue with other logout steps even if Magic SDK logout fails
5494      }
5495    }
5496    
5497    // 2. Reset all authentication state variables
5498    clearHydraClientAuthStorage();
5499    
5500    // 3. Reset all UI state - remove authentication classes from body
5501    document.body.classList.remove('hydra-authenticated');
5502    document.body.classList.remove('hydra-admin');
5503    
5504    // 5. If in edit mode, exit it first
5505    if (document.body.classList.contains('edit-mode-active')) {
5506      console.log('Exiting edit mode before logout');
5507      // If toggleEditMode exists, call it to exit edit mode
5508      if (typeof toggleEditMode === 'function') {
5509        try {
5510          toggleEditMode();
5511        } catch (editError) {
5512          console.error('Error exiting edit mode:', editError);
5513        }
5514      }
5515      
5516      // Ensure edit mode class is removed regardless
5517      document.body.classList.remove('edit-mode-active');
5518    }
5519    
5520    // 6. Ensure all edit UI elements are hidden
5521    
5522    // Hide edit and logout buttons
5523    const editButton = document.getElementById('editButton');
5524    if (editButton) {
5525      editButton.classList.remove('visible');
5526      editButton.style.display = 'none';
5527    }
5528    
5529    const logoutButton = document.getElementById('logoutButton');
5530    if (logoutButton) {
5531      logoutButton.classList.remove('visible');
5532      logoutButton.style.display = 'none';
5533    }
5534    
5535    // Hide edit controls
5536    const editControls = document.querySelector('.edit-controls');
5537    if (editControls) {
5538      editControls.style.display = 'none';
5539      editControls.classList.remove('visible');
5540    }
5541    
5542    // Hide sidebar header if it should be hidden when logged out
5543    const sidebarHeader = document.querySelector('.sidebar-header');
5544    if (sidebarHeader) {
5545      sidebarHeader.style.display = 'none';
5546    }
5547    
5548    // Hide all element controls
5549    document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => {
5550      el.style.display = 'none';
5551    });
5552    
5553    // 7. Reset editor state if applicable
5554    if (window.Parameters) {
5555      window.Parameters.isInEditor = false;
5556    }
5557    
5558    // Hide any editing forms that might be open
5559    const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm');
5560    forms.forEach(form => {
5561      form.style.display = 'none';
5562      if (form.classList.contains('visible')) {
5563        form.classList.remove('visible');
5564      }
5565    });
5566    
5567    // Remove editing class from sidebar
5568    const sidebar = document.querySelector('.sidebar');
5569    if (sidebar) {
5570      sidebar.classList.remove('editing');
5571    }
5572    
5573    // 8. Clear any page-specific state
5574    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
5575      window.PageManager.clearCurrentPage();
5576    }
5577    
5578    // 9. Notify any components that need to know about logout
5579    document.dispatchEvent(new CustomEvent('user-logout', {
5580      detail: { timestamp: Date.now() }
5581    }));
5582    
5583    // 10. Show success message
5584    hideLoadingOverlay();
5585    showSuccessMessage('Successfully logged out');
5586    
5587    console.log('Logout complete');
5588    
5589    // 11. Optional: Refresh the page after a short delay for a clean state
5590    // Uncomment the following lines if you want the page to refresh after logout
5591    /*
5592    setTimeout(() => {
5593      window.location.reload();
5594    }, 1500);
5595    */
5596    
5597  } catch (error) {
5598    console.error('Error during logout process:', error);
5599    hideLoadingOverlay();
5600    showErrorMessage('Error during logout. Please refresh the page.');
5601  }
5602}
5603
5604// Ensure the logout button is properly connected to the logout function
5605document.addEventListener('DOMContentLoaded', function() {
5606  const logoutButton = document.getElementById('logoutButton');
5607  if (logoutButton) {
5608    // Remove any existing event listeners to avoid duplicates
5609    const newButton = logoutButton.cloneNode(true);
5610    logoutButton.parentNode.replaceChild(newButton, logoutButton);
5611    
5612    // Add fresh event listener
5613    newButton.addEventListener('click', function(e) {
5614      e.preventDefault();
5615      e.stopPropagation();
5616      logout();
5617    });
5618  }
5619});
5620
5621function stopAutoAdvanceTimer() {
5622  if (window.currentAutoAdvanceTimerId) {
5623    clearTimeout(window.currentAutoAdvanceTimerId);
5624    window.currentAutoAdvanceTimerId = null; 
5625    console.log('Auto-advance timer stopped.');
5626  }
5627}
5628window.stopAutoAdvanceTimer = stopAutoAdvanceTimer;
5629
5630function beginHydraContentLoad(type, itemId, pageId) {
5631  const generation = (Number(window.__hydraContentLoadGeneration) || 0) + 1;
5632  window.__hydraContentLoadGeneration = generation;
5633  window.__neonGalleryInitEpoch = (Number(window.__neonGalleryInitEpoch) || 0) + 1;
5634  window.__hydraContentLoadOwner = {
5635    generation,
5636    type,
5637    itemId: itemId === undefined || itemId === null ? '' : String(itemId),
5638    pageId: pageId === undefined || pageId === null ? '' : String(pageId)
5639  };
5640  return generation;
5641}
5642
5643function isHydraContentLoadCurrent(generation, type, itemId) {
5644  if (generation != null && Number(generation) !== Number(window.__hydraContentLoadGeneration || 0)) {
5645    return false;
5646  }
5647  if (type && window._activeContentType && window._activeContentType !== type) {
5648    return false;
5649  }
5650  if (itemId != null && itemId !== '' && window.activeGalleryId != null &&
5651      String(window.activeGalleryId) !== String(itemId)) {
5652    return false;
5653  }
5654  return true;
5655}
5656
5657window.beginHydraContentLoad = beginHydraContentLoad;
5658window.isHydraContentLoadCurrent = isHydraContentLoadCurrent;
5659
5660function dismissGalleryPasswordOverlay() {
5661  if (window.PageManager && typeof window.PageManager.removeGalleryPasswordOverlay === 'function') {
5662    window.PageManager.removeGalleryPasswordOverlay();
5663    return;
5664  }
5665  document.querySelectorAll('.gallery-password-protection-overlay').forEach(function(overlay) {
5666    if (overlay._hydraPasswordAbort && typeof overlay._hydraPasswordAbort.abort === 'function') {
5667      overlay._hydraPasswordAbort.abort();
5668    }
5669    overlay.remove();
5670  });
5671}
5672window.dismissGalleryPasswordOverlay = dismissGalleryPasswordOverlay;
5673
5674function toggleEditMode() {
5675  // Prevent multiple simultaneous calls
5676  if (window._editModeToggleInProgress) {
5677    console.log('Edit mode toggle already in progress, skipping');
5678    return;
5679  }
5680  window._editModeToggleInProgress = true;
5681
5682  console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined');
5683
5684  stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance
5685
5686  // Authentication and Authorization Checks
5687  if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) {
5688    alert('You must be logged in to edit');
5689    window._editModeToggleInProgress = false;
5690    return;
5691  }
5692  if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) {
5693    window.isAdmin = true; // Synchronize global flag
5694  }
5695  if (!window.isAdmin) {
5696    alert('You must be an admin to edit');
5697    window._editModeToggleInProgress = false;
5698    return;
5699  }
5700
5701  const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false);
5702  const newEditState = !currentEditState;
5703  
5704  console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`);
5705
5706  // Apply body class for edit mode styling
5707  if (newEditState) {
5708    console.log('Entering edit mode - setting body class and theme');
5709    document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode
5710    document.body.classList.add('edit-mode-active');
5711  } else {
5712    console.log('Exiting edit mode - removing body class and theme');
5713    document.body.removeAttribute('data-theme');
5714    document.body.classList.remove('edit-mode-active');
5715  }
5716
5717  // Update the global state and dispatch event
5718  if (typeof window.updateGlobalEditState === 'function') {
5719    window.updateGlobalEditState(newEditState); 
5720  } else {
5721    window.isEditing = newEditState; // Fallback
5722    document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } }));
5723    if (!newEditState && typeof window.closeOptionsPanel === 'function') {
5724      window.closeOptionsPanel();
5725    }
5726  }
5727
5728  if (document.body.classList.contains('menu-layout-horizontal') && typeof syncContentMarginToHorizontalHeader === 'function') {
5729    setTimeout(syncContentMarginToHorizontalHeader, 50);
5730    setTimeout(syncContentMarginToHorizontalHeader, 350);
5731  }
5732
5733  const sidebarEditControls = document.querySelector('.edit-controls');
5734
5735  if (newEditState) {
5736    // Entering edit mode
5737    if (sidebarEditControls) {
5738        sidebarEditControls.style.display = 'flex';
5739        sidebarEditControls.classList.add('visible');
5740    }
5741    
5742    // Show logout button in edit controls when entering edit mode
5743    const logoutButton = document.getElementById('logoutButton');
5744    if (logoutButton) {
5745      logoutButton.style.display = 'flex';
5746      logoutButton.classList.add('visible');
5747    }
5748    
5749    // Always render the tree-like menu in the sidebar for editing
5750    if (typeof window.renderGalleries === 'function') {
5751        console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.');
5752        window.renderGalleries(); // This will show all items, including those with visible:false
5753    }
5754
5755    setTimeout(() => {
5756        if (typeof window.initializeNestedSortables === 'function') {
5757            console.log('Initializing nested sortables for edit mode');
5758            window.initializeNestedSortables();
5759        }
5760        if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') {
5761            window.SidebarManager._reinitializeNestedSortables();
5762        }
5763    }, 150);
5764
5765    // If the currently active item was invisible, reload it now that we are in edit mode.
5766    if(window.galleries && window.activeGalleryId){
5767        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5768        if (currentGalleryItem && currentGalleryItem.visible === false) {
5769            console.log('toggleEditMode (Edit): Active item was invisible, reloading it.');
5770            if (currentGalleryItem.isPage && window.loadPage) {
5771                window.loadPage(window.activeGalleryId);
5772            } else if(window.loadGallery) {
5773                window.loadGallery(window.activeGalleryId);
5774            }
5775        }
5776    }
5777    if(typeof window.showStyleEditor === 'function' &&
5778        !(window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen)) {
5779      window.showStyleEditor(false); // Ensure style editor is hidden initially
5780    }
5781
5782  } else {
5783    // Exiting edit mode (going to Live view)
5784    if (sidebarEditControls) {
5785        sidebarEditControls.style.display = 'none';
5786        sidebarEditControls.classList.remove('visible');
5787    }
5788    
5789    // Hide logout button when exiting edit mode
5790    const logoutButton = document.getElementById('logoutButton');
5791    if (logoutButton) {
5792      logoutButton.style.display = 'none';
5793      logoutButton.classList.remove('visible');
5794    }
5795    if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer._prepareOverlayForLiveView === 'function') {
5796      window.MenuStyleCustomizer._prepareOverlayForLiveView();
5797    }
5798    if(typeof window.closeAllForms === 'function') window.closeAllForms();
5799    
5800    if (typeof window.destroyNestedSortables === 'function') {
5801        window.destroyNestedSortables();
5802    } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) {
5803        window.sortableInstances.forEach(instance => instance.destroy());
5804        window.sortableInstances = [];
5805    }
5806
5807    // Re-render the menu based on the actual view mode layout.
5808    // These functions should internally handle not showing items with visible:false.
5809    let currentLayout = 'sidebar';
5810    if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
5811        currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
5812    }
5813    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
5814        window.renderHorizontalMenu();
5815    } else if (typeof window.renderGalleries === 'function') {
5816        window.renderGalleries();
5817    }
5818
5819    // Re-render sidebar elements (social, text, etc.) so edit-only UI (grid, dashed slots) is replaced by live view
5820    if (window.SidebarManager && typeof window.SidebarManager._syncWithGlobalEditState === 'function' && typeof window.SidebarManager.renderElements === 'function') {
5821      window.SidebarManager._syncWithGlobalEditState();
5822      window.SidebarManager.renderElements();
5823    }
5824
5825    // Handle the currently active gallery/page.
5826    if (window.galleries && window.activeGalleryId) {
5827        const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId);
5828        if (currentGalleryItem) {
5829            // The content for currentGalleryItem should remain loaded,
5830            // even if currentGalleryItem.visible is false.
5831            // The menu rendering above will hide it from the list if it's invisible.
5832            
5833            // Apply page-specific menu hiding rules.
5834            const bodyEl = document.body;
5835            // When exiting edit mode, isInEditMode() will effectively be false for this check.
5836            if (shouldApplyMenuHiddenForEmbedOrGallery(currentGalleryItem)) { 
5837                bodyEl.classList.add('menu-hidden-on-page');
5838            } else {
5839                bodyEl.classList.remove('menu-hidden-on-page');
5840            }
5841            applyMenuOverlayClassForGallery(currentGalleryItem);
5842            // Ensure active states in the (potentially now filtered) menu are updated.
5843            // If the active item is invisible, it won't be marked active in the menu, which is fine.
5844            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5845            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5846
5847        } else {
5848            // activeGalleryId points to a non-existent item, so clear it and the content.
5849            console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.');
5850            window.activeGalleryId = null;
5851            const galleryContainer = document.querySelector('.gallery-container');
5852            if (galleryContainer) galleryContainer.innerHTML = '';
5853            if(typeof window.updateActiveStates === 'function') window.updateActiveStates();
5854            if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal();
5855        }
5856    }
5857    
5858    // If, after all that, no content is effectively active (e.g., initial load to root, 
5859    // or activeGalleryId became null because the item was deleted)
5860    // then try to load a default page (home page or first visible gallery).
5861    const galleryContainer = document.querySelector('.gallery-container');
5862    // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted)
5863    const noActiveContent = window.activeGalleryId === null || 
5864                            (galleryContainer && galleryContainer.innerHTML.trim() === '');
5865
5866    if (noActiveContent && window.galleries) {
5867      console.log("toggleEditMode (Live): No active content, attempting to load default page.");
5868      const homePage = window.galleries.find(g => g.isHomePage === true);
5869      let loadedDefault = false;
5870      if (homePage) {
5871          // Load home page regardless of its visibility status for this default loading logic
5872          console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`);
5873          if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; }
5874          else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; }
5875      }
5876      
5877      if (!loadedDefault) {
5878          // If no home page, load the first *visible* gallery/page in live view.
5879          const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
5880          if (firstVisibleGallery) {
5881            console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`);
5882            if (firstVisibleGallery.isPage && window.loadPage) {
5883                window.loadPage(firstVisibleGallery.id);
5884            } else if(window.loadGallery) {
5885                window.loadGallery(firstVisibleGallery.id);
5886            }
5887          } else {
5888            console.log('toggleEditMode (Live): No home page and no visible items to load.');
5889          }
5890      }
5891    }
5892
5893    if (window.MenuStyleCustomizer &&
5894        window.MenuStyleCustomizer._overlayOpen &&
5895        typeof window.MenuStyleCustomizer.applyStyles === 'function') {
5896      window.MenuStyleCustomizer.applyStyles();
5897    }
5898  }
5899  window._editModeToggleInProgress = false;
5900}
5901
5902
5903
5904/**
5905 * Improved toggleSidebarElementForm - Closes other forms first
5906 */
5907function toggleSidebarElementForm() {
5908  const formId = 'sidebarElementForm';
5909  const form = document.getElementById(formId);
5910  
5911  if (!form) return;
5912  
5913  // If this form is already open, just close everything
5914  if (window.currentOpenForm === formId) {
5915    closeAllForms();
5916    return;
5917  }
5918  
5919  // Otherwise close all forms and open this one
5920  closeAllForms();
5921  
5922  // Show the form
5923  form.style.display = 'block';
5924  form.classList.add('visible');
5925  
5926  // Set this as the current open form
5927  window.currentOpenForm = formId;
5928}
5929
5930// New function to add a sidebar element
5931function addSidebarElement() {
5932const typeInput = document.querySelector('input[name="sidebarElementType"]:checked');
5933if (!typeInput) {
5934alert('Please select an element type');
5935return;
5936}
5937
5938const type = typeInput.value;
5939
5940// Use the SidebarManager to add the element
5941if (window.SidebarManager) {
5942const newElementId = window.SidebarManager.addElement(type);
5943toggleSidebarElementForm();
5944
5945if (newElementId) {     
5946  // Save the changes to KV store
5947  saveGalleries().then(() => {
5948    console.log(`New ${type} element added successfully with ID: ${newElementId}`);
5949  }).catch(error => {
5950    console.error('Error saving new element:', error);
5951  });
5952}
5953} else {
5954alert('SidebarManager not available');
5955}
5956}
5957
5958
5959// Global variable to store metadata
5960window.siteMetadata = {
5961  title: '',
5962  description: '',
5963  googleAnalytics: '',
5964  noIndex: false,
5965  favicon: '',
5966  ogImageUrl: '',
5967  contentLanguage: 'en',  // Default to English
5968  copyrightEnabled: false,
5969  copyrightText: '',
5970  customPublicSiteEnabled: false,
5971  customPublicSiteHtmlR2Key: '',
5972  customPublicSiteHtmlFilename: '',
5973  siteWideBasicAuth: undefined
5974};
5975
5976// Global variable to store uploaded favicon URL temporarily
5977window.uploadedFaviconUrl = '';
5978
5979function updateMetadataCopyrightFieldState() {
5980  const toggle = document.getElementById('metadataCopyrightEnabled');
5981  const textInput = document.getElementById('metadataCopyrightText');
5982  const isEnabled = !!(toggle && toggle.checked);
5983
5984  if (textInput) {
5985    textInput.disabled = !isEnabled;
5986  }
5987
5988  // Intentionally leave the surrounding form group in place; the disabled attribute on the input
5989  // provides the visual cue in PicoCSS.
5990}
5991
5992/**
5993 * Improved toggleMetadataEditor - Closes other forms first
5994 */
5995function toggleMetadataEditor() {
5996  const formId = 'metadataEditor';
5997  const editor = document.getElementById(formId);
5998  
5999  if (!editor) return;
6000  
6001  // If this form is already open, just close everything
6002  if (window.currentOpenForm === formId) {
6003    closeAllForms();
6004    return;
6005  }
6006  
6007  // Otherwise close all forms and open this one
6008  closeAllForms();
6009  
6010  // Load current values before showing
6011  loadMetadataValues();
6012  editor.style.display = 'block';
6013  
6014  // Set this as the current open form
6015  window.currentOpenForm = formId;
6016  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
6017    syncCustomPublicSiteBypassVisibility();
6018  }
6019}
6020
6021
6022// Load metadata values into the form
6023function loadMetadataValues() {
6024  // Get the current values from the global object
6025  const metadata = window.siteMetadata || {};
6026  console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata));
6027  console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics);
6028  
6029  console.warn('=== FAVICON LOAD DEBUG ===');
6030  console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata));
6031  console.warn('metadata object:', JSON.stringify(metadata));
6032  console.warn('metadata.favicon value:', metadata.favicon);
6033  console.warn('metadata.favicon type:', typeof metadata.favicon);
6034  
6035  // Populate form fields
6036  document.getElementById('metaTitle').value = metadata.title || '';
6037  document.getElementById('metaDescription').value = metadata.description || '';
6038  document.getElementById('googleAnalytics').value = metadata.googleAnalytics || '';
6039  document.getElementById('noIndexToggle').checked = metadata.noIndex || false;
6040
6041  // Schema / Identity fields
6042  const schemaType = metadata.schemaType || '';
6043  const personRadio = document.getElementById('schemaTypePerson');
6044  const orgRadio = document.getElementById('schemaTypeOrganization');
6045  if (personRadio) personRadio.checked = schemaType === 'Person';
6046  if (orgRadio) orgRadio.checked = schemaType === 'Organization';
6047  const jobTitleEl = document.getElementById('schemaJobTitle');
6048  if (jobTitleEl) jobTitleEl.value = metadata.schemaJobTitle || '';
6049  const cityEl = document.getElementById('schemaCity');
6050  if (cityEl) cityEl.value = metadata.schemaCity || '';
6051  const emailEl = document.getElementById('schemaEmail');
6052  if (emailEl) emailEl.value = metadata.schemaEmail || '';
6053  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
6054  if (imageLicenseUrlEl) imageLicenseUrlEl.value = metadata.imageLicenseUrl || '';
6055  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
6056  if (acquireLicensePageUrlEl) acquireLicensePageUrlEl.value = metadata.acquireLicensePageUrl || '';
6057  if (typeof updateSchemaFields === 'function') updateSchemaFields();
6058
6059  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
6060  const copyrightInput = document.getElementById('metadataCopyrightText');
6061  if (copyrightToggle) {
6062    const enabled = !!metadata.copyrightEnabled;
6063    copyrightToggle.checked = enabled;
6064    if (copyrightInput) {
6065      copyrightInput.value = metadata.copyrightText || '';
6066    }
6067  }
6068  updateMetadataCopyrightFieldState();
6069
6070  const customPubToggle = document.getElementById('customPublicSiteEnabled');
6071  if (customPubToggle) {
6072    customPubToggle.checked = !!metadata.customPublicSiteEnabled;
6073  }
6074  const customPubKeyEl = document.getElementById('customPublicSiteHtmlR2Key');
6075  if (customPubKeyEl) {
6076    customPubKeyEl.value = metadata.customPublicSiteHtmlR2Key || '';
6077  }
6078  const customPubFileLabel = document.getElementById('customPublicSiteHtmlFilenameLabel');
6079  if (customPubFileLabel) {
6080    customPubFileLabel.textContent = metadata.customPublicSiteHtmlFilename
6081      ? ('Current file: ' + metadata.customPublicSiteHtmlFilename)
6082      : 'No HTML file uploaded yet.';
6083  }
6084
6085  const swAuth = metadata.siteWideBasicAuth;
6086  const swEnabled = !!(swAuth && swAuth.username && swAuth.password);
6087  const swEnableEl = document.getElementById('siteWideBasicAuthEnabled');
6088  if (swEnableEl) swEnableEl.checked = swEnabled;
6089  const swUserEl = document.getElementById('siteWideBasicAuthUsername');
6090  if (swUserEl) swUserEl.value = swAuth && swAuth.username ? swAuth.username : '';
6091  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6092  if (swPassEl) swPassEl.value = swAuth && swAuth.password ? swAuth.password : '';
6093  const swRealmEl = document.getElementById('siteWideBasicAuthRealm');
6094  if (swRealmEl) swRealmEl.value = swAuth && swAuth.realm ? swAuth.realm : '';
6095  if (typeof updateSiteWideBasicAuthFieldState === 'function') updateSiteWideBasicAuthFieldState();
6096  
6097  // Set content language dropdown (defaults to 'en')
6098  const contentLanguageSelect = document.getElementById('contentLanguage');
6099  if (contentLanguageSelect) {
6100    contentLanguageSelect.value = metadata.contentLanguage || 'en';
6101    console.log('Loaded content language:', contentLanguageSelect.value);
6102  }
6103  
6104  const faviconInput = document.getElementById('faviconUrl');
6105  if (faviconInput) {
6106    // Check if we have a recently uploaded favicon URL
6107    const faviconValue = metadata.favicon || window.uploadedFaviconUrl || '';
6108    faviconInput.value = faviconValue;
6109    console.warn('Set favicon input value to:', faviconInput.value);
6110    console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl);
6111  } else {
6112    console.error('Favicon input element not found!');
6113  }
6114  
6115  // Show favicon preview if there's an existing favicon
6116  if (metadata.favicon) {
6117    console.warn('Showing favicon preview for:', metadata.favicon);
6118    showFaviconPreview(metadata.favicon);
6119  } else {
6120    console.warn('No favicon to preview');
6121  }
6122
6123  const ogInput = document.getElementById('ogImageUrl');
6124  if (ogInput) {
6125    const ogVal = (metadata.ogImageUrl || '').trim();
6126    ogInput.value = ogVal;
6127  }
6128  const ogBtnText = document.getElementById('uploadOgImageBtnText');
6129  if (ogBtnText) {
6130    ogBtnText.textContent = (metadata.ogImageUrl || '').trim() ? 'Change image' : 'Upload image';
6131  }
6132  if ((metadata.ogImageUrl || '').trim()) {
6133    showOgImagePreview(metadata.ogImageUrl.trim());
6134  } else {
6135    const ogCont = document.getElementById('ogImagePreviewContainer');
6136    if (ogCont) ogCont.innerHTML = '';
6137  }
6138  
6139  console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value);
6140}
6141
6142// Save metadata values
6143function saveMetadata() {
6144  // Get values from form
6145  const googleAnalyticsElement = document.getElementById('googleAnalytics');
6146  console.log('Google Analytics element found:', !!googleAnalyticsElement);
6147  console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND');
6148  
6149  const faviconInput = document.getElementById('faviconUrl');
6150  const faviconValue = faviconInput ? faviconInput.value : '';
6151  const ogImageInput = document.getElementById('ogImageUrl');
6152  const ogImageValue = ogImageInput ? (ogImageInput.value || '').trim() : '';
6153  
6154  const contentLanguageSelect = document.getElementById('contentLanguage');
6155  const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en';
6156
6157  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
6158  const copyrightInput = document.getElementById('metadataCopyrightText');
6159  const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false;
6160  const copyrightText = copyrightInput ? copyrightInput.value.trim() : '';
6161  
6162  console.warn('=== FAVICON SAVE DEBUG ===');
6163  console.warn('Favicon input element exists:', !!faviconInput);
6164  console.warn('Favicon input value:', faviconValue);
6165  console.warn('Favicon input value length:', faviconValue.length);
6166  
6167  console.warn('=== CONTENT LANGUAGE SAVE ===');
6168  console.warn('Content language selected:', contentLanguageValue);
6169  
6170  const rawGa = googleAnalyticsElement ? googleAnalyticsElement.value : '';
6171  // Normalize GA to ID only (client may not have normalizeGoogleAnalyticsId in scope)
6172  let gaId = (rawGa && typeof rawGa === 'string') ? rawGa.trim() : '';
6173  if (gaId && !/^G-[A-Z0-9]+$/i.test(gaId) && !/^UA-d+-d+$/.test(gaId)) {
6174    const match = gaId.match(/(G-[A-Z0-9]+|UA-d+-d+)/i);
6175    gaId = match ? match[1] : '';
6176  }
6177  const customPubKeyInput = document.getElementById('customPublicSiteHtmlR2Key');
6178  const customPubFilename = document.getElementById('customPublicSiteHtmlFilenameStore');
6179  const schemaPersonRadio = document.getElementById('schemaTypePerson');
6180  const schemaOrgRadio = document.getElementById('schemaTypeOrganization');
6181  const schemaType = schemaPersonRadio && schemaPersonRadio.checked ? 'Person'
6182    : schemaOrgRadio && schemaOrgRadio.checked ? 'Organization'
6183    : '';
6184  const schemaJobTitleEl = document.getElementById('schemaJobTitle');
6185  const schemaCityEl = document.getElementById('schemaCity');
6186  const schemaEmailEl = document.getElementById('schemaEmail');
6187  const imageLicenseUrlEl = document.getElementById('imageLicenseUrl');
6188  const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl');
6189  const imageLicenseUrlRaw = imageLicenseUrlEl ? imageLicenseUrlEl.value.trim() : '';
6190  const acquireLicensePageUrlRaw = acquireLicensePageUrlEl ? acquireLicensePageUrlEl.value.trim() : '';
6191
6192  const normalizePublicAbsoluteUrl = (value) => {
6193    if (!value) return '';
6194    try {
6195      const parsed = new URL(value);
6196      if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return '';
6197      return parsed.href;
6198    } catch {
6199      return '';
6200    }
6201  };
6202  const imageLicenseUrl = normalizePublicAbsoluteUrl(imageLicenseUrlRaw);
6203  const acquireLicensePageUrl = normalizePublicAbsoluteUrl(acquireLicensePageUrlRaw);
6204  if ((imageLicenseUrlRaw || acquireLicensePageUrlRaw) && (!imageLicenseUrl || !acquireLicensePageUrl)) {
6205    alert('Image licensing: enter two public http(s) URLs, or leave both fields empty.');
6206    return;
6207  }
6208
6209  const siteWideBasicAuthEnabled = !!(document.getElementById('siteWideBasicAuthEnabled') && document.getElementById('siteWideBasicAuthEnabled').checked);
6210  const swUser = document.getElementById('siteWideBasicAuthUsername') ? document.getElementById('siteWideBasicAuthUsername').value.trim() : '';
6211  const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6212  const swPass = swPassEl ? swPassEl.value : '';
6213  const swRealm = document.getElementById('siteWideBasicAuthRealm') ? document.getElementById('siteWideBasicAuthRealm').value.trim() : '';
6214
6215  if (siteWideBasicAuthEnabled && (!swUser || !swPass)) {
6216    alert('Site-wide password: enter both username and password, or turn off the option.');
6217    return;
6218  }
6219
6220  const metadata = {
6221    title: document.getElementById('metaTitle').value,
6222    description: document.getElementById('metaDescription').value,
6223    googleAnalytics: gaId,
6224    noIndex: document.getElementById('noIndexToggle').checked,
6225    favicon: faviconValue,
6226    ogImageUrl: ogImageValue,
6227    contentLanguage: contentLanguageValue,
6228    copyrightEnabled,
6229    copyrightText,
6230    customPublicSiteEnabled: !!(document.getElementById('customPublicSiteEnabled') && document.getElementById('customPublicSiteEnabled').checked),
6231    customPublicSiteHtmlR2Key: customPubKeyInput ? customPubKeyInput.value.trim() : '',
6232    customPublicSiteHtmlFilename: customPubFilename ? customPubFilename.value.trim() : '',
6233    schemaType: schemaType || undefined,
6234    schemaJobTitle: schemaJobTitleEl ? schemaJobTitleEl.value.trim() : '',
6235    schemaCity: schemaCityEl ? schemaCityEl.value.trim() : '',
6236    schemaEmail: schemaEmailEl ? schemaEmailEl.value.trim() : '',
6237    ...(imageLicenseUrl && acquireLicensePageUrl ? { imageLicenseUrl, acquireLicensePageUrl } : {})
6238  };
6239
6240  if (siteWideBasicAuthEnabled && swUser && swPass) {
6241    metadata.siteWideBasicAuth = {
6242      username: swUser,
6243      password: swPass,
6244      ...(swRealm ? { realm: swRealm } : {})
6245    };
6246  }
6247  
6248  console.warn('Metadata object being created:', JSON.stringify(metadata));
6249  console.warn('Favicon in metadata object:', metadata.favicon);
6250
6251  // Show normalized GA ID in the form so user sees what was saved (e.g. after pasting full snippet)
6252  if (googleAnalyticsElement && metadata.googleAnalytics !== rawGa.trim()) {
6253    googleAnalyticsElement.value = metadata.googleAnalytics;
6254  }
6255
6256  // Update global metadata object immediately
6257  window.siteMetadata = metadata;
6258  console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata));
6259  if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') {
6260    window.SidebarManager.updateMetadataFooter();
6261  }
6262
6263  // Ensure we have the latest galleries, sidebar elements, and styles
6264  // before constructing the customData object.
6265
6266  // Synchronize galleries first if the function exists
6267  if (typeof synchronizeGalleries === 'function') {
6268      synchronizeGalleries();
6269      console.log('Galleries synchronized within saveMetadata');
6270  }
6271
6272  // Retrieve current galleries (use window.galleries as the source of truth after sync)
6273  const currentGalleries = window.galleries || galleries || [];
6274  console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`);
6275
6276  // Retrieve current sidebar elements
6277  const currentSidebarElements = window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function'
6278    ? window.SidebarManager.getElementsForSave()
6279    : null;
6280
6281  // Retrieve current menu styles
6282  const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {};
6283
6284  // Now construct customData using the retrieved current state
6285  if (window.saveGalleries) {
6286    const customData = {
6287      galleries: currentGalleries, // Use the synchronized/retrieved galleries
6288      siteMetadata: metadata, // Use the metadata gathered from the form
6289      menuStyles: currentMenuStyles // Use the retrieved menu styles
6290    };
6291    if (Array.isArray(currentSidebarElements) && currentSidebarElements.length > 0) {
6292      customData.sidebarElements = currentSidebarElements;
6293    }
6294  
6295
6296    console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`);
6297    console.log("Metadata being saved:", JSON.stringify(metadata));
6298    console.log("Google Analytics specifically:", metadata.googleAnalytics);
6299    console.log("Sidebar elements being saved:", Array.isArray(customData.sidebarElements) ? customData.sidebarElements.length : '(omitted)');
6300    console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles));
6301
6302    // Save with our correctly constructed custom data object
6303    window.saveGalleries(customData)
6304      .then(() => {
6305        alert('Metadata saved successfully');
6306        toggleMetadataEditor(); // Hide the editor
6307      })
6308      .catch(error => {
6309        console.error('Error saving metadata:', error);
6310        alert('Error saving metadata. Please try again.');
6311      });
6312  } else {
6313    alert('Save function not available');
6314  }
6315}
6316
6317  
6318// Function to add new entries to the style editor
6319function showStyleEditor(display = false) {
6320  if (display && window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.openOverlay === 'function') {
6321    window.MenuStyleCustomizer.openOverlay();
6322    window.currentOpenForm = 'menuStyleEditor';
6323    return;
6324  }
6325  hideStyleEditor();
6326}
6327
6328// New function to hide the style editor
6329function hideStyleEditor() {
6330  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6331    window.MenuStyleCustomizer.closeOverlay();
6332  }
6333  const styleEditor = document.getElementById('menuStyleEditor');
6334  if (styleEditor) {
6335    styleEditor.style.display = 'none';
6336  }
6337}
6338
6339
6340/**
6341 * Improved toggleAddForm - Closes other forms first and ensures proper sizing
6342 */
6343function toggleAddForm() {
6344  const formId = 'addForm';
6345  const form = document.getElementById(formId);
6346  
6347  if (!form) return;
6348  
6349  // If this form is already open, just close everything
6350  if (window.currentOpenForm === formId) {
6351    closeAllForms();
6352    return;
6353  }
6354  
6355  // Otherwise close all forms and open this one
6356  closeAllForms();
6357  
6358  // Determine the appropriate max-height based on content
6359  // First check if we're showing the page form by checking the radio button
6360  const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked;
6361  
6362  // Allow extra space for the page form
6363  const maxHeight = isPageSelected ? '600px' : '500px';
6364  
6365  // Show the form with proper max height
6366  form.style.maxHeight = maxHeight;
6367  form.style.overflow = 'visible';
6367 // Ensure all content is visible
6368  form.classList.add('visible');
6369  
6370  // Also add inline styles to ensure visibility during animation
6371  form.style.padding = '15px';
6372  
6373  // Force recalculation of max-height after UI is visible
6374  setTimeout(() => {
6375    // Get actual content height + padding
6376    const contentHeight = form.scrollHeight + 30; // Add padding
6377    
6378    // Set max-height based on content with minimum of 500px
6379    form.style.maxHeight = Math.max(contentHeight, 500) + 'px';
6380    
6381    console.log(`Setting add form max height to ${form.style.maxHeight}`);
6382  }, 50);
6383  
6384  // Populate parent options
6385  const parentSelect = document.getElementById('galleryParent');
6386  if (parentSelect) {
6387    parentSelect.innerHTML = '<option value="">None (Top Level)</option>';
6388    
6389    // Add all galleries that could be parents (including submenus)
6390    galleries.forEach(gallery => {
6391      parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
6392    });
6393  }
6394  
6395  // Set this as the current open form
6396  window.currentOpenForm = formId;
6397}
6398
6399
6400// Store the currently open form ID
6401window.currentOpenForm = null;
6402
6403/**
6404 * Close a child panel opened on top of the Style Customizer (palette explorer, custom code, color mixer).
6405 * Returns true when a child was closed so callers can keep the Style Customizer open.
6406 */
6407function closeTopmostStyleCustomizerChildOverlay() {
6408  if (document.querySelector('.pe-palette-explorer-overlay')) {
6409    if (window.PaletteExplorer && typeof window.PaletteExplorer.close === 'function') {
6410      const wasOpen = !!document.querySelector('.pe-palette-explorer-overlay');
6411      window.PaletteExplorer.close();
6412      if (wasOpen && document.querySelector('.pe-palette-explorer-overlay')) {
6413        return false;
6414      }
6415      return wasOpen;
6416    }
6417  }
6418  if (document.querySelector('.pe-style-history-overlay, .sc-style-history-overlay') &&
6419      window.MenuStyleCustomizer &&
6420      typeof window.MenuStyleCustomizer.closeStyleHistoryOverlay === 'function') {
6421    window.MenuStyleCustomizer.closeStyleHistoryOverlay();
6422    return true;
6423  }
6424  if (document.querySelector('.pe-font-explorer-overlay') &&
6425      window.MenuStyleCustomizer &&
6426      typeof window.MenuStyleCustomizer.closeFontExplorer === 'function') {
6427    window.MenuStyleCustomizer.closeFontExplorer();
6428    return true;
6429  }
6430  if (document.querySelector('.pe-custom-code-overlay') &&
6431      window.MenuStyleCustomizer &&
6432      typeof window.MenuStyleCustomizer.closeCustomCodeOverlay === 'function') {
6433    window.MenuStyleCustomizer.closeCustomCodeOverlay();
6434    return true;
6435  }
6436  const colorMixer = document.getElementById('colorMixerPopup');
6437  if (colorMixer && colorMixer.classList.contains('is-open')) {
6438    colorMixer.classList.remove('is-open');
6439    return true;
6440  }
6441  return false;
6442}
6443
6444/**
6445 * Close all editor forms with improved handling for add form
6446 */
6447function closeAllForms() {
6448  if (typeof closeTopmostStyleCustomizerChildOverlay === 'function') {
6449    while (closeTopmostStyleCustomizerChildOverlay()) {
6450      // Close nested Style Customizer panels (palette, custom code, color mixer) first.
6451    }
6452  }
6453
6454  const keepMinimizedStyleCustomizer = !!(window.MenuStyleCustomizer &&
6455    window.MenuStyleCustomizer._overlayOpen &&
6456    window.MenuStyleCustomizer._overlayMinimized);
6457
6458  if (!keepMinimizedStyleCustomizer &&
6459      window.MenuStyleCustomizer &&
6460      typeof window.MenuStyleCustomizer.closeOverlay === 'function') {
6461    const styleOverlayWasOpen = window.MenuStyleCustomizer._overlayOpen;
6462    window.MenuStyleCustomizer.closeOverlay();
6463    if (styleOverlayWasOpen && window.MenuStyleCustomizer._overlayOpen) {
6464      return;
6465    }
6466  }
6467
6468  // Add Form - Special handling to ensure proper animation
6469  const addForm = document.getElementById('addForm');
6470  if (addForm) {
6471    addForm.classList.remove('visible');
6472    
6473    // Also force style reset after animation completes
6474    setTimeout(() => {
6475      if (!addForm.classList.contains('visible')) {
6476        addForm.style.maxHeight = '0';
6477        addForm.style.overflow = 'hidden';
6478        addForm.style.padding = '0 15px';
6479      }
6480    }, 350); // Set slightly longer than CSS transition
6481  }
6482  
6483  // Style Editor (do not hide the overlay host while the minimized mutate bar is live)
6484  const styleEditor = document.getElementById('menuStyleEditor');
6485  if (styleEditor && !keepMinimizedStyleCustomizer) {
6486    styleEditor.style.display = 'none';
6487  }
6488  
6489  // Metadata Editor
6490  const metadataEditor = document.getElementById('metadataEditor');
6491  if (metadataEditor) {
6492    metadataEditor.style.display = 'none';
6493  }
6494  if (typeof syncCustomPublicSiteBypassVisibility === 'function') {
6495    syncCustomPublicSiteBypassVisibility();
6496  }
6497  
6498  // Sidebar Element Form
6499  const sidebarElementForm = document.getElementById('sidebarElementForm');
6500  if (sidebarElementForm) {
6501    sidebarElementForm.style.display = 'none';
6502    sidebarElementForm.classList.remove('visible');
6503  }
6504  
6505  // Import Classic Form
6506  const importClassicForm = document.getElementById('importClassicForm');
6507  if (importClassicForm) {
6508    importClassicForm.style.display = 'none';
6509    importClassicForm.classList.remove('visible');
6510  }
6511  
6512  // Clear the current open form
6513  window.currentOpenForm = null;
6514}
6515function downloadJson() {
6516  const data = JSON.stringify(galleries, null, 2);
6517  const blob = new Blob([data], { type: 'application/json' });
6518  const url = URL.createObjectURL(blob);
6519  const a = document.createElement('a');
6520  a.href = url;
6521  a.download = 'galleries.json';
6522  document.body.appendChild(a);
6523  a.click();
6524  document.body.removeChild(a);
6525  URL.revokeObjectURL(url);
6526}
6527
6528let galleriesBackup = [];
6529
6530
6531
6532/**
6533 * Improved version of initializeNestedSortables with a better nesting approach
6534 */
6535function initializeNestedSortables() {
6536  console.log('Initializing nested sortables with improved nesting logic...');
6537
6538  // Destroy leftover instances before binding again. A second Sortable on the
6539  // same list makes items preview during drag and snap back on release.
6540  document.querySelectorAll('#galleryTree .nested-sortable').forEach((el) => {
6541    try {
6542      const existing = typeof Sortable !== 'undefined' && Sortable.get ? Sortable.get(el) : null;
6543      if (existing && typeof existing.destroy === 'function') existing.destroy();
6544    } catch (error) {
6545      console.warn('Could not destroy existing nested sortable:', error);
6546    }
6547  });
6548
6549  // Create a backup of galleries
6550  try {
6551    galleriesBackup = JSON.parse(JSON.stringify(galleries));
6552  } catch (error) {
6553    console.error('Error creating galleries backup:', error);
6554    galleriesBackup = [];
6555  }
6556
6557  // Find all nested sortable lists
6558  const nestedSortables = document.querySelectorAll('.nested-sortable');
6559  if (nestedSortables.length === 0) {
6560    console.warn('No nested sortable elements found! Check your HTML 
6560structure.');
6561    return;
6562  }
6563
6564  // Helper: compute depth based on ancestor nested lists
6565  function computeDepth(liEl) {
6566    let depth = 0;
6567    let node = liEl;
6568    while (node) {
6569      const parentOl = node.closest('ol.nested-sortable');
6570      if (!parentOl) break;
6571      const parentLi = parentOl.closest('li');
6572      if (parentLi) {
6573        depth += 1;
6574        node = parentLi;
6575      } else {
6576        break;
6577      }
6578    }
6579    // Top-level depth should be 0
6580    return Math.max(depth - 1, 0);
6581  }
6582
6583  // Helper: apply indentation styling to an li and its subtree
6584  function applyIndentationFromDepth(rootLi) {
6585    if (!rootLi) return;
6586    const applyToItem = (li) => {
6587      const depth = computeDepth(li);
6588      // Update data-nesting-level attribute
6589      li.setAttribute('data-nesting-level', depth);
6590      // Apply padding-left style
6591      const content = li.querySelector('.gallery-item-content');
6592      if (content) {
6593        content.style.paddingLeft = (depth * 16) + 'px';
6594      }
6595    };
6596    applyToItem(rootLi);
6597    rootLi.querySelectorAll('li').forEach(applyToItem);
6598  }
6599
6600  // Helper: ensure parent has proper state and nested list
6601  function ensureParentChildState(parentLi) {
6602    if (!parentLi) return null;
6603    parentLi.classList.add('has-children');
6604    let nestedList = parentLi.querySelector('ol.nested-sortable');
6605    if (!nestedList) {
6606      nestedList = document.createElement('ol');
6607      nestedList.className = 'nested-sortable';
6608      parentLi.appendChild(nestedList);
6609    }
6610    return nestedList;
6611  }
6612
6613  // Helper: clean parent if it has no children
6614  function cleanupParentIfEmpty(parentLi) {
6615    if (!parentLi) return;
6616    const nestedList = parentLi.querySelector(':scope > ol.nested-sortable');
6617    const parentId = parseInt(parentLi.dataset && parentLi.dataset.id, 10);
6618    const parentItem = !isNaN(parentId) ? galleries.find(g => g.id === parentId) : null;
6619    // Opted-in pages/galleries must keep an empty drop zone and chevron.
6620    if (isNavigableMenuParent(parentItem)) {
6621      parentLi.classList.add('has-children');
6622      return;
6623    }
6624    if (nestedList && nestedList.children.length === 0) {
6625      nestedList.remove();
6626      parentLi.classList.remove('has-children');
6627    }
6628  }
6629
6630  // Initialize each sortable with improved nesting settings
6631  let sortableInstances = [];
6632
6633  for (let i = 0; i < nestedSortables.length; i++) {
6634    try {
6635      const container = nestedSortables[i];
6636      
6637      // Track where the placeholder indicates the item should be placed
6638      // This helps us detect when SortableJS incorrectly auto-nests based on mouse position
6639      let intendedPlacementContainer = null;
6640      let intendedRelatedElement = null;
6641      
6642      // Track last move to prevent oscillation
6643      let lastMoveTime = 0;
6644      let lastMoveRelatedId = null;
6645      const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds)
6646      
6647      // Track if we're dragging the last item in a folder (to prevent boundary oscillation)
6648      let isLastItemInFolder = false;
6649      let draggedItemOriginalContainer = null;
6650      
6651      // Track container changes to prevent rapid switching (hysteresis)
6652      let firstPositionFixLoopActive = false; // Prevent multiple fix loops
6653      let lastContainerDecision = null; // 'inside' or 'outside'
6654      let lastContainerDecisionTime = 0;
6655      const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches
6656      
6657      const sortable = new Sortable(container, {
6658        group: 'nested',
6659        animation: 150, // Reduced animation during drag to reduce lag
6660        easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function
6661        fallbackOnBody: false, // Disable forced fallback on desktop for better performance
6662        forceFallback: false, // Use native drag on desktop
6663        
6664        // CRITICAL: These settings make nesting much easier
6665        // Higher swap threshold makes it easier to drag over items
6666        swapThreshold: 0.65, // Increased to reduce oscillation within same folder
6667        
6668        // IMPROVED NESTING SETTINGS
6669        // Used when determining if an item should be nested
6670        invertSwap: false,   // Disable to reduce oscillation
6671        direction: 'vertical', // Lock direction to vertical to reduce oscillation
6672        
6673        // Use handle for dragging
6674        handle: '.nested-sortable-handle',
6675        
6676        // Filter out only the toggle button - this prevents SortableJS from intercepting clicks on it
6677        // The folder text (.menu-item.folder) is NOT filtered, so it can be used for both:
6678        // - Quick click → toggles folder (onclick handler fires before drag starts)
6679        // - Click and drag → drags the item (SortableJS handles the drag)
6680        // The delay helps distinguish between click and drag, but on desktop with delayOnTouchOnly: true,
6681        // the onclick should fire quickly enough before SortableJS intercepts
6682        filter: '.submenu-toggle, .submenu-toggle *, .toggle-icon',
6683        preventOnFilter: true, // Prevent drag when clicking filtered elements (toggle button only)
6684        
6685        // Distinguish between click and drag
6686        delay: 120,
6687        delayOnTouchOnly: true,
6688        
6689        // Empty container settings
6690        emptyInsertThreshold: 10,
6691        
6692        // Add custom ghost class
6693        ghostClass: 'sortable-ghost',
6694        chosenClass: 'sortable-chosen',
6695        dragClass: 'sortable-drag',
6696        
6697        // Make sure we listen to external move events
6698        dragoverBubble: true,
6699        
6700        // Better drag preview stability
6701        scroll: true,
6702        scrollSensitivity: 100,
6703        scrollSpeed: 20,
6704        
6705        // Reduce oscillation by making placeholder movement less sensitive
6706        forceFallback: false, // Use native drag for better performance
6707        fallbackTolerance: 0, // No tolerance - use exact position
6708        
6709        /**
6710         * NESTING LOGIC: Prevent auto-nesting based on hover
6711         * Instead, rely on SortableJS's placeholder position which is accurate
6712         * We'll verify and correct placement in onEnd based on actual DOM position
6713         */
6714        onMove: function(evt, originalEvent) {
6715          const dragged = evt.dragged;
6716          const related = evt.related;
6717          
6718          if (!dragged || !related) return true;
6719          
6720          // Safety check: Ensure items have data-id attributes
6721          // This can be missing if item was just added and DOM not fully updated
6722          if (!dragged.dataset.id || !related.dataset.id) {
6723            console.warn('⚠️ [Drag] Item missing data-id attribute, aborting drag', {
6724              draggedHasId: !!dragged.dataset.id,
6725              relatedHasId: !!related.dataset.id
6726            });
6727            return false;
6728          }
6729          
6730          // Get the IDs to check for circular references
6731          const draggedId = parseInt(dragged.dataset.id);
6732          const relatedId = parseInt(related.dataset.id);
6733          
6734          // Don't allow dropping inside itself or its descendants
6735          if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) {
6736            return false;
6737          }
6738          
6739          const currentTime = Date.now();
6740          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
6741          const draggedParent = draggedItemOriginalContainer || dragged.parentElement;
6742          
6743          // Determine if we're moving inside or outside a folder
6744          // IMPORTANT: Check if related element IS the folder item itself (parent li)
6745          // When targeting first position, SortableJS might use the folder item as related
6746          const isRelatedFolderItem = related.classList && related.classList.contains('has-children');
6747          
6748          // CRITICAL: Handle empty folder case FIRST - before any other logic
6749          // When dragging into an empty folder, related is the folder item itself
6750          // This is essentially the same as targeting first position, but for an empty folder
6751          // We need to allow this immediately, bypassing all debounce and boundary checks
6752          if (isRelatedFolderItem) {
6753            const folderNestedSortable = related.querySelector(':scope > ol.nested-sortable');
6754            const relatedIsNavigableParent = related.classList.contains('navigable-parent-item');
6755            // Navigable parents look like normal links. Only treat them as an
6756            // empty folder when the pointer is actually over the empty list.
6757            let allowEmptyFolderInsert = !!(folderNestedSortable && folderNestedSortable.children.length === 0);
6758            if (allowEmptyFolderInsert && relatedIsNavigableParent && originalEvent) {
6759              const listRect = folderNestedSortable.getBoundingClientRect();
6760              const pointerY = originalEvent.clientY;
6761              allowEmptyFolderInsert = pointerY >= listRect.top - 6 && pointerY <= listRect.bottom + 10;
6762            }
6763            if (allowEmptyFolderInsert) {
6764              // Empty folder detected - this is a drop into empty folder (first position)
6765              // Always allow it, bypass all debounce and boundary logic
6766              lastMoveTime = currentTime;
6767              lastMoveRelatedId = relatedId;
6768              intendedPlacementContainer = folderNestedSortable;
6769              intendedRelatedElement = null; // No related element in empty folder
6770              // Mark as moving inside folder for tracking
6771              lastContainerDecision = 'inside';
6772              lastContainerDecisionTime = currentTime;
6773              
6774              // CRITICAL: Manually move placeholder into empty folder's nested-sortable
6775              // SortableJS might not do this automatically for empty containers
6776              requestAnimationFrame(() => {
6777                const placeholder = document.querySelector('.sortable-ghost');
6778                if (placeholder && placeholder.parentElement !== folderNestedSortable) {
6779                  // Move placeholder into the empty folder
6780                  folderNestedSortable.appendChild(placeholder);
6781                }
6782                
6783                // Update ghost indentation for empty folder
6784                const ghost = document.querySelector('.sortable-ghost');
6785                if (ghost) {
6786                  const ghostContent = ghost.querySelector('.gallery-item-content');
6787                  if (ghostContent) {
6788                    // Calculate depth from parent folder
6789                    const parentFolder = folderNestedSortable.closest('li.has-children');
6790                    if (parentFolder) {
6791                      const targetDepth = computeDepth(parentFolder) + 1;
6792                      ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
6793                    }
6794                  }
6795                }
6796              });
6797              
6798              // Return true immediately to allow the move
6799              return true;
6800            }
6801          }
6802          
6803          let actualRelatedParent = related.parentElement;
6804          
6805          // If related IS the folder item, we need to get its nested sortable container
6806          if (isRelatedFolderItem) {
6807            const nestedSortable = related.querySelector('ol.nested-sortable');
6808            if (nestedSortable) {
6809              actualRelatedParent = nestedSortable;
6810            }
6811          }
6812          
6813          const relatedParent = actualRelatedParent;
6814          
6815          const isMovingInsideFolder = relatedParent && 
6816                                       relatedParent.classList.contains('nested-sortable') && 
6817                                       relatedParent !== rootContainer;
6818          const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer;
6819          const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null);
6820          
6821          // Determine if we're moving within the same folder - this is the key check
6822          // If related IS the folder item, we're still moving within the same folder
6823          let isMovingWithinSameFolder = (isMovingInsideFolder && 
6824                                          draggedParent && 
6825                                          draggedParent.classList.contains('nested-sortable') &&
6826                                          draggedParent !== rootContainer &&
6827                                          relatedParent === draggedParent) ||
6828                                          // OR: if related is the folder item itself and we're dragging from that folder
6829                                          (isRelatedFolderItem && 
6830                                           draggedParent && 
6831                                           draggedParent.classList.contains('nested-sortable') &&
6832                                           draggedParent !== rootContainer &&
6833                                           related.querySelector('ol.nested-sortable') === draggedParent);
6834          
6835          // Special case: If related is the folder item, we're definitely trying to move within that folder
6836          // This happens when targeting first position - SortableJS uses the folder item as related
6837          if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6838            const folderNestedSortable = related.querySelector('ol.nested-sortable');
6839            if (folderNestedSortable === draggedParent) {
6840              // We're dragging within the folder, and related is the folder item
6841              // This means we're targeting first position - FORCE it to be treated as same-folder move
6842              // FORCE same-folder detection - override any previous calculation
6843              isMovingWithinSameFolder = true;
6844            }
6845          }
6846          
6847          // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens
6848          // (Don't log every single move - too noisy)
6849          if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6850            const folderItems = Array.from(draggedParent.children);
6851            const draggedIndex = folderItems.indexOf(dragged);
6852            const relatedIndex = folderItems.indexOf(related);
6853            
6854            // Get placeholder element to see where it actually is
6855            const placeholder = document.querySelector('.sortable-ghost');
6856            const placeholderParent = placeholder ? placeholder.parentElement : null;
6857            const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') 
6858              ? Array.from(placeholderParent.children).indexOf(placeholder) 
6859              : -1;
6860            
6861            const folderItem = draggedParent.closest('li.has-children');
6862            const folderItemId = folderItem ? folderItem.dataset.id : null;
6863            const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null;
6864            const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null;
6865            
6866            // Check if we're trying to target first position
6867            // related could be the first item OR the folder item itself (when SortableJS uses folder as related)
6868            const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder;
6869            const isRelatedFolderItemCheck = related === folderItem;
6870            const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean
6871            
6872            // When targeting first via folder item, placeholder should be at index 0
6873            // But we're seeing it at index 1 - this is the issue!
6874            const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0;
6875            
6876            // Check if placeholder is outside the folder when it should be inside
6877            const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable'));
6878            const shouldBeInside = draggedIndex >= 0; // We're dragging from inside
6879            
6880            // Only log when there's a potential issue or interesting state change
6881            const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0;
6882            const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0;
6883            const jumpingOutToRoot = placeholderIsOutside && shouldBeInside;
6884            
6885            // First position issue: trying to target first but:
6886            // - placeholder is outside OR 
6887            // - placeholder is at index 1 instead of 0 (when targeting first via folder item)
6888            const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition);
6889            
6890            // ALWAYS log when targeting first position via folder item (this is the key case we're debugging)
6891            // OR when placeholder jumps out when it should stay in
6892            // OR when placeholder should be at first position but isn't
6893            const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition;
6894            
6895            if (shouldLog) {
6896              console.error('🔍 [Placeholder Position]', {
6897                draggedId,
6898                draggedIndex,
6899                relatedId,
6900                relatedIndex,
6901                placeholderIndex,
6902                relatedElementTag: related.tagName,
6903                isRelatedFolderItem: isRelatedFolderItemCheck,
6904                isTargetingFirstViaFolderItem,
6905                isMovingWithinSameFolder,
6906                placeholderMismatch,
6907                jumpingOut,
6908                jumpingOutToRoot,
6909                isFirstPosition,
6910                placeholderIsOutside,
6911                placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'),
6912                folderItemId,
6913                firstItemInFolderId,
6914                firstPositionIssue,
6915                shouldBeAtFirstPosition,
6916                expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex
6917              });
6918            }
6919          }
6920          
6921          // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic
6922          // This allows smooth reordering just like items not in folders
6923          if (isMovingWithinSameFolder) {
6924            // Simple debounce only - same as root level items
6925            const timeSinceLastMove = currentTime - lastMoveTime;
6926            // Only prevent very rapid oscillation to same element
6927            if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) {
6928              return false;
6929            }
6930            // Allow all moves within same folder - update tracking and continue
6931            
6932            // SPECIAL FIX: When targeting first position via folder item, fix placeholder position
6933            // SortableJS places it at index 1 when related is the folder item, but we want it at index 0
6934            // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive
6935            if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
6936              const folderItems = Array.from(draggedParent.children);
6937              const firstItem = folderItems.length > 0 ? folderItems[0] : null;
6938              
6939              // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up)
6940              // This prevents the fix from being too aggressive when moving to other positions
6941              const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions
6942              
6943              if (firstItem && firstItem !== dragged && isNearFirstPosition) {
6944                // Fix immediately (synchronously) first - don't wait for next frame
6945                const placeholder = document.querySelector('.sortable-ghost');
6946                if (placeholder && placeholder.parentElement === draggedParent) {
6947                  const currentIndex = Array.from(draggedParent.children).indexOf(placeholder);
6948                  // Only fix if placeholder is at index 1 when we're targeting first position
6949                  // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3)
6950                  if (currentIndex === 1 && draggedIndex <= 1) {
6951                    draggedParent.insertBefore(placeholder, firstItem);
6952                  }
6953                }
6954              } else {
6955                // Not near first position - stop any active fix loop
6956                firstPositionFixLoopActive = false;
6957              }
6958            } else {
6959              // Not targeting first position anymore - reset flag
6960              firstPositionFixLoopActive = false;
6961            }
6962            
6963            lastMoveTime = currentTime;
6964            lastMoveRelatedId = relatedId;
6965            // Skip all the boundary case logic below - just continue to allow the move
6966            // Fall through to update intendedPlacementContainer and return true
6967          } else {
6968            // Only apply complex boundary logic for moves BETWEEN folders or to/from root
6969            
6970            // Hysteresis: Prevent rapid switching between inside/outside folder
6971            const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : [];
6972            const isFirstPositionInFolder = isMovingInsideFolder && 
6973                                           targetFolderItems.length > 0 && 
6974                                           related === targetFolderItems[0];
6975            // Special case: Empty folder - allow easy dropping into it
6976            const isEmptyFolder = isMovingInsideFolder && targetFolderItems.length === 0;
6977            
6978            if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) {
6979              const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime;
6980              
6981              // Make first position easier to target from outside
6982              if (isFirstPositionInFolder) {
6983                // Moving to first position from outside - use shorter debounce
6984                const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3;
6985                if (timeSinceContainerSwitch < effectiveDebounce) {
6986                  // Only block if it's very rapid (oscillation)
6987                  if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) {
6988                    return false;
6989                  }
6990                }
6991              } else if (isEmptyFolder) {
6992                // Empty folder - allow easy dropping, minimal debounce
6993                // Only block very rapid oscillation
6994                if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS * 0.2) {
6995                  return false;
6996                }
6997              } else {
6998                // Normal container switch - apply full hysteresis
6999                if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) {
7000                  return false;
7001                }
7002              }
7003            }
7004          }
7005          
7006          // Special handling for boundary cases (last item in folder, or dragging into folder)
7007          // BUT: Skip all this logic if we're moving within the same folder (already handled above)
7008          if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) {
7009            const folderItem = draggedParent.closest('li.has-children');
7010            const currentFolderItems = Array.from(draggedParent.children);
7011            const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && 
7012                                                   currentFolderItems[0] !== dragged &&
7013                                                   (related === currentFolderItems[0] || 
7014                                                    (isMovingInsideFolder && relatedParent === draggedParent && 
7015                                                     related === currentFolderItems[0]));
7016            
7017            // Case 0: This case is now handled earlier for same-folder moves
7018            // Only handle cross-folder moves here
7019            
7020            // Case 1: Dragging last item out of folder
7021            if (isLastItemInFolder && isMovingOutsideFolder) {
7022              const timeSinceLastMove = currentTime - lastMoveTime;
7023              
7024              // BUT: If we're trying to move to first position, don't block it
7025              // Check if the related element is the folder itself or right after it
7026              if (folderItem) {
7027                const folderNextSibling = folderItem.nextElementSibling;
7028                // If we're moving to position right after folder, it might be trying to go to first position
7029                // Allow it if it's been a reasonable time
7030                if (folderNextSibling && related === folderNextSibling) {
7031                  // This is ambiguous - could be first position or outside
7032                  // Require more time but not as much as pure outside move
7033                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
7034                    if (relatedId === lastMoveRelatedId) {
7035                      return false;
7036                    }
7037                  }
7038                } else {
7039                  // Definitely moving outside - require longer debounce
7040                  if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
7041                    if (relatedId === lastMoveRelatedId) {
7042                      return false; // Reject if oscillating
7043                    }
7044                  }
7045                }
7046              } else {
7047                // No folder item found - apply standard debounce
7048                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) {
7049                  if (relatedId === lastMoveRelatedId) {
7050                    return false;
7051                  }
7052                }
7053              }
7054            }
7055            
7056            // Case 2: Dragging item into folder from outside
7057            if (isMovingInsideFolder && relatedParent !== draggedParent) {
7058              const targetFolderItems = Array.from(relatedParent.children);
7059              const isEmptyFolder = targetFolderItems.length === 0;
7060              const isNearBottom = targetFolderItems.length > 0 && 
7061                                   (related === targetFolderItems[targetFolderItems.length - 1] ||
7062                                    targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
7063              const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0];
7064              
7065              // Special handling for empty folder - allow easy dropping
7066              if (isEmptyFolder) {
7067                const timeSinceLastMove = currentTime - lastMoveTime;
7068                // Very minimal debounce for empty folder - only prevent rapid oscillation
7069                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2 && relatedId === lastMoveRelatedId) {
7070                  return false;
7071                }
7072                // Allow move into empty folder - don't block it
7073              } else if (isFirstPos) {
7074                // Moving to first position - make it easy
7075                const timeSinceLastMove = currentTime - lastMoveTime;
7076                // Very short debounce for first position
7077                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) {
7078                  return false;
7079                }
7080                // Allow move to first position
7081              } else if (isNearBottom) {
7082                // For bottom position, still use some debounce to prevent oscillation
7083                const timeSinceLastMove = currentTime - lastMoveTime;
7084                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) {
7085                  if (relatedId === lastMoveRelatedId) {
7086                    return false;
7087                  }
7088                }
7089              }
7090            }
7091            
7092          // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within
7093          // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last
7094          if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) {
7095            // We're dragging from within a folder but detected as moving outside
7096            // Check if the related element is adjacent to the folder (which might indicate targeting first/last)
7097            const folderIndex = Array.from(rootContainer.children).indexOf(folderItem);
7098            const relatedIndex = Array.from(rootContainer.children).indexOf(related);
7099            
7100            // If related is immediately before or after folder, might be targeting first/last
7101            if (folderIndex >= 0 && relatedIndex >= 0) {
7102              const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1);
7103              
7104              if (isAdjacentToFolder) {
7105                // Might be targeting first/last position - be lenient
7106                const timeSinceLastMove = currentTime - lastMoveTime;
7107                // Only block if it's very rapid oscillation
7108                if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) {
7109                  return false;
7110                }
7111                // Otherwise allow - user might be trying to target first/last position
7112                // Don't block based on this check
7113              }
7114            }
7115          }
7116          }
7117          
7118          // Case 3: Dragging from root into folder
7119          if (draggedParent === rootContainer && isMovingInsideFolder) {
7120            const targetFolderItems = Array.from(relatedParent.children);
7121            const isEmptyFolder = targetFolderItems.length === 0;
7122            const isNearBottom = targetFolderItems.length > 0 && 
7123                                 (related === targetFolderItems[targetFolderItems.length - 1] ||
7124                                  targetFolderItems.indexOf(related) >= targetFolderItems.length - 1);
7125            const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0];
7126            
7127            // Special handling for empty folder - allow easy dropping
7128            if (isEmptyFolder) {
7129              const timeSinceLastMove = currentTime - lastMoveTime;
7130              // Very minimal debounce for empty folder - only prevent rapid oscillation
7131              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2) {
7132                if (relatedId === lastMoveRelatedId) {
7133                  return false;
7134                }
7135              }
7136              // Allow move into empty folder - don't block it
7137            } else if (isFirstPosition) {
7138              // Special handling for first position - make it easier to target
7139              const timeSinceLastMove = currentTime - lastMoveTime;
7140              // Make first position easier - only prevent very rapid oscillation
7141              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) {
7142                if (relatedId === lastMoveRelatedId) {
7143                  return false;
7144                }
7145              }
7146              // Allow move to first position - don't block it
7147              // Continue to track but don't apply strict debouncing
7148            } else if (isNearBottom) {
7149              // Keep strict debouncing for bottom position
7150              const timeSinceLastMove = currentTime - lastMoveTime;
7151              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) {
7152                if (relatedId === lastMoveRelatedId) {
7153                  return false;
7154                }
7155              }
7156            }
7157          }
7158          
7159          // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position)
7160          // If we're at first position and trying to move outside, require deliberate movement
7161          if (isMovingInsideFolder) {
7162            const currentFolderItems = Array.from(relatedParent.children);
7163            const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0];
7164            
7165            // If we're moving FROM first position to outside (reverse hysteresis)
7166            if (isFirstPos && lastContainerDecision === 'outside') {
7167              const timeSinceLastMove = currentTime - lastMoveTime;
7168              // If we just moved outside, require more time before allowing back to first position
7169              // BUT: if it's been a reasonable time, allow it (don't make it too hard)
7170              if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) {
7171                // Very rapid - might be oscillation
7172                return false;
7173              }
7174            }
7175          }
7176          
7177          // Case 5: Prevent oscillation between first and second position within folder
7178          // BUT: Only apply if NOT moving within same folder (to allow smooth reordering)
7179          if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) {
7180            const folderItems = Array.from(relatedParent.children);
7181            const isFirstPos = folderItems.length > 0 && related === folderItems[0];
7182            const isSecondPos = folderItems.length > 1 && related === folderItems[1];
7183            
7184            // If moving from first to second (or vice versa) very rapidly, prevent oscillation
7185            if ((isFirstPos || isSecondPos) && lastMoveRelatedId) {
7186              const lastRelated = document.querySelector('#galleryTree li[data-id="' + lastMoveRelatedId + '"]') || document.querySelector('li[data-id="' + lastMoveRelatedId + '"]');
7187              if (lastRelated && lastRelated.parentElement === relatedParent) {
7188                const lastRelatedIndex = folderItems.indexOf(lastRelated);
7189                const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0);
7190                
7191                if (isOscillating) {
7192                  const timeSinceLastMove = currentTime - lastMoveTime;
7193                  // Allow moving to first position easily, but prevent rapid oscillation between first/second
7194                  if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) {
7195                    return false; // Prevent jumping from first to second too easily
7196                  }
7197                  // But allow first position more easily
7198                  if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) {
7199                    return false; // Only prevent very rapid oscillation
7200                  }
7201                }
7202              }
7203            }
7204          }
7205          
7206          // Prevent rapid oscillation when dragging within same container
7207          // NOTE: For same-folder moves, we already updated tracking above, so skip this 
7207section
7208          if (!isMovingWithinSameFolder) {
7209            const timeSinceLastMove = currentTime - lastMoveTime;
7210            
7211            // If moving to the same related element very quickly, reject to prevent oscillation
7212            if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) {
7213              return false;
7214            }
7215            
7216            // Update tracking variables (only for non-same-folder moves)
7217            lastMoveTime = currentTime;
7218            lastMoveRelatedId = relatedId;
7219          }
7220          // For same-folder moves, tracking was already updated in the earlier block
7221          
7222          // Update container decision tracking
7223          if (currentDecision) {
7224            if (currentDecision !== lastContainerDecision) {
7225              lastContainerDecisionTime = currentTime;
7226            }
7227            lastContainerDecision = currentDecision;
7228          }
7229          
7230          // Track where the placeholder indicates the item should be placed
7231          // The related element is next to where the placeholder will be positioned
7232          // Check if it's at root level or nested level
7233          if (related) {
7234            intendedRelatedElement = related;
7235            const relatedParent = related.parentElement;
7236            const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7237            
7238            // Determine if the placeholder is at root level or nested level
7239            if (relatedParent === rootContainer) {
7240              // Placeholder is at root level (not nested)
7241              intendedPlacementContainer = rootContainer;
7242            } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) {
7243              // Placeholder is in a nested list (inside a folder)
7244              intendedPlacementContainer = relatedParent;
7245            }
7246            
7247            // Update ghost element indentation to match the target position
7248            requestAnimationFrame(() => {
7249              const ghost = document.querySelector('.sortable-ghost');
7250              if (ghost && intendedPlacementContainer) {
7251                const ghostContent = ghost.querySelector('.gallery-item-content');
7252                if (ghostContent) {
7253                  // Calculate the depth where the ghost will be placed
7254                  // Use the related element to determine the depth (if it exists and is in the target container)
7255                  let targetDepth = 0;
7256                  if (intendedPlacementContainer === rootContainer) {
7257                    targetDepth = 0; // Root level
7258                  } else if (related && related.parentElement === intendedPlacementContainer) {
7259                    // Use the related element's depth to determine target depth
7260                    targetDepth = computeDepth(related);
7261                  } else {
7262                    // Fallback: find first child in the container or calculate from parent folder
7263                    const firstChild = intendedPlacementContainer.querySelector('li');
7264                    if (firstChild) {
7265                      targetDepth = computeDepth(firstChild);
7266                    } else {
7267                      // Empty container (like empty folder) - calculate from parent folder
7268                      const parentFolder = intendedPlacementContainer.closest('li.has-children');
7269                      if (parentFolder) {
7270                        targetDepth = computeDepth(parentFolder) + 1;
7271                      }
7272                    }
7273                  }
7274                  
7275                  // Special handling: If intendedPlacementContainer is empty and placeholder isn't in it yet,
7276                  // ensure placeholder is moved there and indentation is correct
7277                  if (intendedPlacementContainer.children.length === 0) {
7278                    const placeholder = document.querySelector('.sortable-ghost');
7279                    if (placeholder && placeholder.parentElement !== intendedPlacementContainer) {
7280                      // Move placeholder into empty container
7281                      intendedPlacementContainer.appendChild(placeholder);
7282                    }
7283                  }
7284                  ghostContent.style.paddingLeft = (targetDepth * 16) + 'px';
7285                }
7286              }
7287            });
7288          }
7289          
7290          // Let SortableJS handle the placement based on placeholder position
7291          // We'll verify and correct if needed in onEnd
7292          return true; // Allow the move
7293        },
7294        
7295        // Capture indentation before drag starts (fires when item is chosen)
7296        onChoose: function(evt) {
7297          if (!evt.item) return;
7298          
7299          const draggedItem = evt.item;
7300          const content = draggedItem.querySelector('.gallery-item-content');
7301          
7302          // Store the original padding-left for the ghost
7303          if (content) {
7304            const computedStyle = window.getComputedStyle(content);
7305            const originalPaddingLeft = computedStyle.paddingLeft;
7306            draggedItem.dataset.originalPaddingLeft = originalPaddingLeft;
7307          }
7308        },
7309        
7310        // Visual feedback on drag start
7311        onStart: function(evt) {
7312          if (!evt.item) return;
7313          
7314          // Don't set dimensions on the dragged item - let it maintain natural size
7315          // Setting width/height can cause layout shifts and menu expansion
7316          const draggedItem = evt.item;
7317          
7318          // Store original container and check if it's the last item in folder
7319          draggedItemOriginalContainer = draggedItem.parentElement;
7320          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7321          if (draggedItemOriginalContainer && 
7322              draggedItemOriginalContainer.classList.contains('nested-sortable') && 
7323              draggedItemOriginalContainer !== rootContainer) {
7324            const siblings = Array.from(draggedItemOriginalContainer.children);
7325            isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem;
7326          } else {
7327            isLastItemInFolder = false;
7328          }
7329          
7330          // Apply the stored padding-left to the ghost element
7331          // The ghost is created by SortableJS at this point
7332          requestAnimationFrame(() => {
7333            const ghost = document.querySelector('.sortable-ghost');
7334            if (ghost) {
7335              const ghostContent = ghost.querySelector('.gallery-item-content');
7336              const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft;
7337              if (ghostContent && originalPaddingLeft) {
7338                ghostContent.style.paddingLeft = originalPaddingLeft;
7339              }
7340            }
7341          });
7342          
7343          // Reset oscillation tracking variables
7344          lastMoveTime = 0;
7345          lastMoveRelatedId = null;
7346          lastContainerDecision = null;
7347          lastContainerDecisionTime = 0;
7348          
7349          // Create a backup
7350          try {
7351            window._dragStartElementIds = [];
7352            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7353              if (li.dataset.id) {
7354                window._dragStartElementIds.push(li.dataset.id);
7355              }
7356            });
7357            
7358            galleriesBackup = JSON.parse(JSON.stringify(galleries));
7359          } catch (error) {
7360            console.error('Error creating backup:', error);
7361          }
7362          
7363          // Add indicator classes
7364          document.body.classList.add('menu-item-dragging');
7365          
7366          // Add hover indicator class to submenu items
7367          document.querySelectorAll('.has-children').forEach(item => {
7368            item.classList.add('potential-parent');
7369          });
7370          
7371          // Prevent layout recalculation during drag
7372          document.body.style.setProperty('--is-dragging', '1');
7373        },
7374        
7375        // Cleanup on end
7376        onEnd: function(evt) {
7377          // Verify and correct placement based on placeholder's intended position
7378          // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level
7379          const dragged = evt.item;
7380          const previousParentLi = evt.from ? evt.from.closest('li') : null;
7381          const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7382          
7383          if (!dragged || !rootContainer) {
7384            intendedPlacementContainer = null;
7385            intendedRelatedElement = null;
7386            // Restore normal sizing
7387            dragged.style.width = '';
7388            dragged.style.height = '';
7389            dragged.style.minHeight = '';
7390            document.body.style.removeProperty('--is-dragging');
7391            return;
7392          }
7393          
7394          // Get the actual parent container where SortableJS placed the item
7395          const actualParentList = dragged.parentElement;
7396          
7397          // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing
7398          requestAnimationFrame(() => {
7399            dragged.style.width = '';
7400            dragged.style.height = '';
7401            dragged.style.minHeight = '';
7402            document.body.style.removeProperty('--is-dragging');
7403          });
7404          
7405          if (intendedPlacementContainer && !document.body.contains(intendedPlacementContainer)) {
7406            intendedPlacementContainer = null;
7407            intendedRelatedElement = null;
7408          }
7409
7410          // Check if the intended placement (where placeholder showed) differs from actual placement
7411          // This happens when SortableJS auto-nests based on mouse position over a folder
7412          if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) {
7413            // The item ended up in a different container than the placeholder indicated
7414            // Move it to where the placeholder showed it should be (next to intendedRelatedElement)
7415            if (intendedPlacementContainer === rootContainer) {
7416              // Placeholder was at root level - move item to root, positioned next to related element
7417              if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) {
7418                // Insert before the related element (where placeholder was)
7419                rootContainer.insertBefore(dragged, intendedRelatedElement);
7420                console.log("Corrected placement: moved from folder to root level where placeholder indicated");
7421              } else if (intendedRelatedElement) {
7422                // Related element exists but not at root - find its folder and insert before it at root
7423                const folderItem = intendedRelatedElement.closest('li.has-children');
7424                if (folderItem && folderItem.parentElement === rootContainer) {
7425                  rootContainer.insertBefore(dragged, folderItem);
7426                  console.log("Corrected placement: moved to root level above folder where placeholder indicated");
7427                } else {
7428                  rootContainer.appendChild(dragged);
7429                }
7430              } else {
7431                rootContainer.appendChild(dragged);
7432              }
7433              // Update indentation since it's now at root
7434              applyIndentationFromDepth(dragged);
7435            } else {
7436              // Placeholder was in a nested list - move it there
7437              if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) {
7438                // Insert before the related element in the nested list
7439                intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement);
7440              } else {
7441                intendedPlacementContainer.appendChild(dragged);
7442              }
7443              console.log("Corrected placement: moved to nested list where placeholder indicated");
7444              // Update indentation for nested item
7445              applyIndentationFromDepth(dragged);
7446              
7447              // Ensure parent folder has proper structure
7448              const folderItem = intendedPlacementContainer.closest('li.has-children');
7449              if (folderItem) {
7450                const nestedList = folderItem.querySelector('ol.nested-sortable');
7451                if (nestedList && intendedPlacementContainer !== nestedList) {
7452                  nestedList.appendChild(dragged);
7453                }
7454                
7455                const parentDepth = computeDepth(folderItem);
7456                const nestedDepth = parentDepth + 1;
7457                
7458                if (nestedList) {
7459                  nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7460                }
7461                
7462                folderItem.classList.add('has-children');
7463              }
7464            }
7465          } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) {
7466            // Item is correctly in a nested list - ensure proper structure
7467            const folderItem = actualParentList.closest('li.has-children');
7468            if (folderItem) {
7469              const nestedList = folderItem.querySelector('ol.nested-sortable');
7470              if (nestedList && actualParentList !== nestedList) {
7471                nestedList.appendChild(dragged);
7472              }
7473              
7474              const parentDepth = computeDepth(folderItem);
7475              const nestedDepth = parentDepth + 1;
7476              
7477              if (nestedList) {
7478                nestedList.className = 'nested-sortable nested-level-' + nestedDepth;
7479              }
7480              
7481              folderItem.classList.add('has-children');
7482            }
7483            applyIndentationFromDepth(dragged);
7484          } else {
7485            // Item is correctly at root level
7486            applyIndentationFromDepth(dragged);
7487          }
7488
7489          // Reset tracking variables
7490          intendedPlacementContainer = null;
7491          intendedRelatedElement = null;
7492          isLastItemInFolder = false;
7493          draggedItemOriginalContainer = null;
7494          lastContainerDecision = null;
7495          lastContainerDecisionTime = 0;
7496
7497          // Apply indentation updates for ALL affected items after drag
7498          // Use requestAnimationFrame to batch DOM updates and prevent visual jumps
7499          requestAnimationFrame(() => {
7500            if (dragged) {
7501              // Update the dragged item and all its descendants
7502              applyIndentationFromDepth(dragged);
7503              
7504              // Update all items in the new container (siblings of dragged item)
7505              const newContainer = dragged.parentElement;
7506              if (newContainer && newContainer.classList.contains('nested-sortable')) {
7507                const siblings = Array.from(newContainer.children);
7508                siblings.forEach(sibling => {
7509                  if (sibling !== dragged && sibling.tagName === 'LI') {
7510                    applyIndentationFromDepth(sibling);
7511                  }
7512                });
7513              }
7514              
7515              // Update all items in the old container (if it still exists and has items)
7516              if (evt.from && evt.from.classList.contains('nested-sortable')) {
7517                const oldSiblings = Array.from(evt.from.children);
7518                oldSiblings.forEach(sibling => {
7519                  if (sibling.tagName === 'LI') {
7520                    applyIndentationFromDepth(sibling);
7521                  }
7522                });
7523              }
7524              
7525              // Also update all items in the entire tree to ensure consistency
7526              // This handles edge cases where indentation might be off
7527              const allItems = document.querySelectorAll('#galleryTree .nested-sortable li');
7528              allItems.forEach(li => {
7529                const depth = computeDepth(li);
7530                li.setAttribute('data-nesting-level', depth);
7531                const content = li.querySelector('.gallery-item-content');
7532                if (content) {
7533                  content.style.paddingLeft = (depth * 16) + 'px';
7534                }
7535              });
7536            }
7537          });
7538
7539          // Clean up previous parent if it lost its last child
7540          if (previousParentLi) {
7541            cleanupParentIfEmpty(previousParentLi);
7542          }
7543          
7544          // Remove all indicator classes
7545          document.body.classList.remove('menu-item-dragging');
7546          document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => {
7547            el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested');
7548          });
7549          
7550          // Standard cleanup for ghost elements
7551          document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => {
7552            item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag');
7553          });
7554          
7555          // Check if drop was outside a valid container
7556          if (!evt.to || !evt.to.classList.contains('nested-sortable')) {
7557            console.warn('Item was dropped outside a valid container - reverting');
7558            restoreFromBackup();
7559            return;
7560          }
7561          
7562          // Verify no elements were lost
7563          try {
7564            // Check DOM elements
7565            const endElementIds = [];
7566            document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => {
7567              if (li.dataset.id) {
7568                endElementIds.push(li.dataset.id);
7569              }
7570            });
7571            
7572            if (window._dragStartElementIds && window._dragStartElementIds.length > 0) {
7573              const startCount = window._dragStartElementIds.length;
7574              const endCount = endElementIds.length;
7575              
7576              if (endCount < startCount) {
7577                console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`);
7578                restoreFromBackup();
7579                return;
7580              }
7581            }
7582            
7583            // Update the data structure
7584            updateGalleryStructure();
7585            
7586            // Verify data model
7587            const originalCount = countGalleries(galleriesBackup);
7588            const newCount = countGalleries(galleries);
7589            
7590            if (newCount < originalCount) {
7591              console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`);
7592              restoreFromBackup();
7593              return;
7594            }
7595            
7596            // updateGalleryStructure already persists; do not save or re-render again
7597            // here. A second save/render can rebind Sortable and snap the drop back.
7598          } catch (error) {
7599            console.error('Error updating gallery structure:', error);
7600            restoreFromBackup();
7601          }
7602        }
7603      });
7604      
7605      sortableInstances.push(sortable);
7606    } catch (error) {
7607      console.error(`Error initializing sortable for container ${i}:`, error);
7608    }
7609  }
7610
7611  // Add event delegation for folder toggles (will be added by setupFolderToggleHandler)
7612  setupFolderToggleHandler();
7613
7614  return sortableInstances;
7615}
7616
7617// Helper function to set up folder toggle event delegation
7618// This must be called after galleries are rendered, in both view and edit modes
7619function setupFolderToggleHandler() {
7620  const galleryTree = document.getElementById('galleryTree');
7621  if (galleryTree && !galleryTree.dataset.folderToggleHandlerAdded) {
7622    galleryTree.addEventListener('click', function(e) {
7623      
7624      const target = e.target;
7625      // Resolve the clicked row first — do not use target.closest('.menu-item.folder'),
7626      // which matches ancestor folder labels when clicking nested pages/galleries.
7627      const listItem = target.closest('li[data-id]');
7628      if (!listItem) {
7629        return;
7630      }
7631
7632      const contentDiv = listItem.querySelector(':scope > .gallery-item-content');
7633      if (!contentDiv) {
7634        return;
7635      }
7636
7637      const toggleButton = contentDiv.querySelector('.submenu-toggle');
7638      const folderText = contentDiv.querySelector('.menu-item.folder');
7639      const isToggleClick = toggleButton && toggleButton.contains(target);
7640      const isFolderTextClick = folderText && folderText.contains(target);
7641      
7642      
7643      if (isToggleClick || isFolderTextClick) {
7644        const folderId = parseInt(listItem.dataset.id, 10);
7645        
7646        if (folderId && !isNaN(folderId)) {
7647          // Prevent SortableJS from intercepting this click
7648          e.stopPropagation();
7649          e.stopImmediatePropagation();
7650          toggleFolder(folderId, e);
7651        }
7652      }
7653    }, true); // Use capture phase to run before SortableJS
7654    
7655    galleryTree.dataset.folderToggleHandlerAdded = 'true';
7656  }
7657}
7658
7659// NEW HELPER FUNCTION: Gets the nesting level of an element
7660function getElementNestingLevel(element) {
7661  let level = 0;
7662  let current = element;
7663  
7664  // Count how many nested-sortable parents this element has
7665  while (current && current.parentElement) {
7666    if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) {
7667      level++;
7668    }
7669    current = current.parentElement;
7670  }
7671  
7672  return level;
7673}
7674
7675// NEW HELPER FUNCTION: Determines if an item would become nested
7676function wouldBeNestedItem(draggedEl, relatedEl, event) {
7677  if (!relatedEl || !draggedEl) return false;
7678  
7679  // Get mouse position
7680  const mouseX = event.clientX;
7681  
7682  // Get the bounding rect of the related element
7683  const rect = relatedEl.getBoundingClientRect();
7684  
7685  // Determine the nesting threshold - how far from the left edge
7686  // the mouse needs to be to consider it a nesting operation
7687  const nestingThreshold = 25; // pixels from left edge
7688  
7689  // Check if mouse is within the nesting threshold from the left edge
7690  const distanceFromLeft = mouseX - rect.left;
7691  
7692  // Check if we should nest (mouse is within threshold of left edge)
7693  const wouldNest = distanceFromLeft <= nestingThreshold;
7694  
7695  // Log the calculation for debugging
7696  console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`);
7697  
7698  return wouldNest;
7699}
7700
7701/**
7702 * Helper function to find gallery by ID in a tree structure
7703 * @param {Array} galleries - The gallery array to search
7704 * @param {number} id - The ID to find
7705 * @returns {Object|null} - The found gallery or null
7706 */
7707function findGalleryById(galleries, id) {
7708  if (window.HelpSystem && typeof window.HelpSystem.isDemoId === 'function' && window.HelpSystem.isDemoId(id)) {
7709    return window.HelpSystem.getDemoRecord(id);
7710  }
7711  if (!Array.isArray(galleries)) return null;
7712  
7713  // First check at the current level
7714  const directMatch = galleries.find(g => g.id == id);
7715  if (directMatch) return directMatch;
7716  
7717  // Then check in children
7718  for (const gallery of galleries) {
7719    if (gallery.children && gallery.children.length > 0) {
7720      const childMatch = findGalleryById(gallery.children, id);
7721      if (childMatch) return childMatch;
7722    }
7723  }
7724  
7725  return null;
7726}
7727
7728/** Detect page menu items even when isPage flag was not persisted */
7729function isPageMenuItem(item) {
7730  if (!item) return false;
7731  if (item.isPage === true) return true;
7732  if (item.pageId && String(item.pageId).indexOf('page_') === 0) return true;
7733  if (Array.isArray(item.pageElements) && item.pageElements.length > 0) return true;
7734  return false;
7735}
7736
7737/**
7738 * Return true only for pages/galleries that explicitly opted into navigable-parent behavior.
7739 * Never infer this from children or parentId; legacy items must retain their existing behavior.
7740 */
7741function isNavigableMenuParent(item) {
7742  if (!item || item.actsAsMenuParent !== true) return false;
7743  if (item.isFolder === true || item.isSubmenu === true || item.isSpacer === true || item.isExternal === true) {
7744    return false;
7745  }
7746  return isPageMenuItem(item) || item.isIntegrated === true;
7747}
7748
7749/**
7750 * Get all gallery IDs from a tree structure
7751 * @param {Array} galleries - Array of galleries
7752 * @returns {Array} - Flat array of all IDs
7753 */
7754function getAllGalleryIds(galleries) {
7755  if (!Array.isArray(galleries)) return [];
7756  
7757  let ids = [];
7758  
7759  galleries.forEach(gallery => {
7760    if (gallery && gallery.id) {
7761      ids.push(gallery.id);
7762    }
7763    
7764    // Add child IDs if any
7765    if (gallery.children && Array.isArray(gallery.children)) {
7766      ids = ids.concat(getAllGalleryIds(gallery.children));
7767    }
7768  });
7769  
7770  return ids;
7771}
7772
7773/**
7774 * Enhanced count function that handles edge cases
7775 * @param {Array} galleryArray - The array to count
7776 * @returns {number} - The total count of galleries
7777 */
7778function countGalleries(galleryArray) {
7779  if (!Array.isArray(galleryArray)) return 0;
7780
7781  // Menu items are stored flat and also get a derived children[] at render time.
7782  // Counting children recursively double-counts those items and falsely triggers
7783  // restoreFromBackup after a successful drop.
7784  const ids = new Set();
7785  function walk(items) {
7786    if (!Array.isArray(items)) return;
7787    items.forEach(function(gallery) {
7788      if (!gallery) return;
7789      if (gallery.id != null) ids.add(gallery.id);
7790      if (gallery.children && Array.isArray(gallery.children)) walk(gallery.children);
7791    });
7792  }
7793  walk(galleryArray);
7794  return ids.size;
7795}
7796
7797/**
7798 * Enhanced backup restoration function with better debugging and notification
7799 */
7800function restoreFromBackup() {
7801  console.warn('Restoring galleries from backup due to invalid operation');
7802  
7803  // Verify we have a valid backup
7804  if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) {
7805    console.error('No valid backup available for restoration');
7806    
7807    // Create a backup from the current DOM as a last resort
7808    try {
7809      console.log('Attempting to rebuild from DOM structure...');
7810      rebuildGalleriesFromDOM();
7811      return;
7812    } catch (rebuildError) {
7813      console.error('Failed to rebuild from DOM:', rebuildError);
7814      // Continue with normal user notification
7815    }
7816  } else {
7817    // Restore from backup
7818    galleries = JSON.parse(JSON.stringify(galleriesBackup));
7819  }
7820  
7821  // Re-render the tree with the restored data
7822  renderGalleries();
7823  
7824  // Force reinitialize nested sortables after restoration
7825  setTimeout(() => {
7826    initializeNestedSortables();
7827  }, 300);
7828}
7829
7830/**
7831 * Emergency recovery function that tries to rebuild galleries from DOM
7832 * This is a last resort when backup restoration fails
7833 */
7834function rebuildGalleriesFromDOM() {
7835  console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure');
7836  
7837  // Create a new galleries array
7838  const rebuiltGalleries = [];
7839  
7840  // This will track items we've already processed
7841  const processedIds = new Set();
7842  
7843  // Function to process a container and its children
7844  function processContainer(container, parentId = null) {
7845    // Get all immediate list items
7846    const items = container.querySelectorAll(':scope > li');
7847    
7848    items.forEach(item => {
7849      // Get item ID from data attribute
7850      const id = parseInt(item.dataset.id);
7851      if (isNaN(id)) return; // Skip if no valid ID
7852      
7853      // Skip already processed items to avoid duplicates
7854      if (processedIds.has(id)) return;
7855      processedIds.add(id);
7856      
7857      // Try to find the original gallery data
7858      let galleryData = null;
7859      
7860      // First check current galleries array
7861      const existingGallery = findGalleryById(galleries, id);
7862      if (existingGallery) {
7863        galleryData = { ...existingGallery };
7864        delete galleryData.children; // We'll rebuild the hierarchy
7865      }
7866      // If not found, check if we have a backup
7867      else if (galleriesBackup && Array.isArray(galleriesBackup)) {
7868        const backupGallery = findGalleryById(galleriesBackup, id);
7869        if (backupGallery) {
7870          galleryData = { ...backupGallery };
7871          delete galleryData.children; // We'll rebuild the hierarchy
7872        }
7873      }
7874      
7875      // If we still don't have data, create minimal placeholder
7876      if (!galleryData) {
7877        // Try to extract title from DOM
7878        const titleElement = item.querySelector('.menu-item');
7879        const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`;
7880        
7881        galleryData = {
7882          id: id,
7883          title: title,
7884          visible: !item.classList.contains('hidden-gallery')
7885        };
7886      }
7887      
7888      // Update parent ID to match DOM structure
7889      galleryData.parentId = parentId;
7890      
7891      // Add to rebuilt galleries
7892      rebuiltGalleries.push(galleryData);
7893      
7894      // Process children if any
7895      const childContainer = item.querySelector('ol.nested-sortable');
7896      if (childContainer) {
7897        processContainer(childContainer, id);
7898      }
7899    });
7900  }
7901  
7902  // Start with root container
7903  const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable');
7904  if (rootContainer) {
7905    processContainer(rootContainer);
7906    
7907    // Replace global galleries array with our rebuilt one
7908    galleries = rebuiltGalleries;
7909    
7910    // Re-render with the rebuilt data
7911    renderGalleries();
7912    
7913    console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`);
7914    return true;
7915  } else {
7916    console.error('RECOVERY FAILED: Root container not found');
7917    return false;
7918  }
7919}
7920
7921// Make sure these functions are available to the global scope
7922window.initializeNestedSortables = initializeNestedSortables;
7923window.restoreFromBackup = restoreFromBackup;
7924window.getElementNestingLevel = getElementNestingLevel;
7925window.wouldBeNestedItem = wouldBeNestedItem;
7926
7927
7928
7929function hasDescendant(itemId, possibleDescendantId) {
7930// Check if possibleDescendantId is a descendant of itemId
7931const children = galleries.filter(g => g.parentId === itemId);
7932
7933if (children.some(child => child.id === possibleDescendantId)) {
7934return true;
7935}
7936
7937return children.some(child => hasDescendant(child.id, possibleDescendantId));
7938}
7939
7940function updateGalleryStructure() {
7941// Create a new array to hold the updated gallery structure
7942const newGalleries = [];
7943
7944// Function to recursively process nested lists
7945function processNestedList(container, parentId = null) {
7946if (!container || !container.children) {
7947  console.warn('Invalid container in processNestedList', container);
7948  return;
7949}
7950
7951const items = container.children;
7952
7953Array.from(items).forEach(item => {
7954  if (item.tagName !== 'LI') return; // Skip non-list items
7955  
7956  const id = parseInt(item.dataset.id);
7957  if (isNaN(id)) {
7958    console.warn('Invalid ID in list item:', item);
7959    return;
7960  }
7961  
7962  const gallery = galleries.find(g => g.id === id);
7963  
7964  if (gallery) {
7965    // Create a new gallery object without children
7966    const newGallery = { ...gallery };
7967    if (newGallery.children) delete newGallery.children;
7968    
7969    // Update parent ID
7970    newGallery.parentId = parentId;
7971    
7972    // Add to new galleries array
7973    newGalleries.push(newGallery);
7974    
7975    // Process any nested OL with class 'nested-sortable' within this item
7976    const nestedList = item.querySelector('ol.nested-sortable');
7977    if (nestedList) {
7978      processNestedList(nestedList, id);
7979    }
7980  } else {
7981    console.warn(`Gallery with ID ${id} not found in galleries array`);
7982  }
7983});
7984}
7985
7986// Start processing from the root list
7987const rootList = document.querySelector('#galleryTree > ol.nested-sortable');
7988if (rootList) {
7989try {
7990  processNestedList(rootList);
7991  
7992  // Verify that we haven't lost any galleries
7993  if (newGalleries.length < galleries.length) {
7994    console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`);
7995    
7996    // Find missing galleries
7997    const originalIds = galleries.map(g => g.id);
7998    const newIds = newGalleries.map(g => g.id);
7999    const missingIds = originalIds.filter(id => !newIds.includes(id));
8000    
8001    if (missingIds.length > 0) {
8002      console.error('Missing gallery IDs:', missingIds);
8003      
8004      // Add missing galleries to the new structure
8005      missingIds.forEach(id => {
8006        const missingGallery = galleries.find(g => g.id === id);
8007        if (missingGallery) {
8008          // Add it as a top-level item
8009          const newGallery = { ...missingGallery, parentId: null };
8010          if (newGallery.children) delete newGallery.children;
8011          newGalleries.push(newGallery);
8012          console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`);
8013        }
8014      });
8015    }
8016  }
8017  
8018  // Update the galleries array with the new structure
8019  galleries = newGalleries;
8020  if (typeof window !== 'undefined') {
8021    window.galleries = galleries;
8022  }
8023  
8024  // Save the updated structure
8025  saveGalleries();
8026} catch (error) {
8027  console.error('Error in updateGalleryStructure:', error);
8028  throw error; // Re-throw to trigger the backup restore
8029}
8030} else {
8031console.warn('Root list not found');
8032throw new Error('Root list not found'); // Throw error to trigger backup restore
8033}
8034}
8035
8036function toggleSubmenu(id, event) {
8037// Ensure the event doesn't interfere with drag operations
8038event.stopPropagation();
8039
8040const listItem = getGalleryTreeListItem(id, event);
8041if (listItem) {
8042// Toggle the expanded class
8043listItem.classList.toggle('expanded');
8044
8045// Find the submenu toggle icon and rotate it
8046const toggleIcon = listItem.querySelector('.toggle-icon');
8047if (toggleIcon) {
8048  toggleIcon.classList.toggle('rotated');
8049}
8050}
8051}
8052
8053/**
8054 * Improved slugify function with robust character handling
8055 * @param {string} text - The text to convert to a slug
8056 * @returns {string} A URL-friendly slug
8057 */
8058function slugify(text) {
8059  // Guard against null or empty input
8060  if (!text || text.trim() === '') {
8061    return 'page-' + Date.now();
8062  }
8063  
8064  // First convert to lowercase
8065  let slug = text.toLowerCase();
8066  
8067  // Replace spaces with hyphens
8068  slug = slug.split(' ').join('-');
8069  
8070  // Filter out unwanted characters (keep only a-z, 0-9, and hyphens)
8071  let filtered = '';
8072  for (let i = 0; i < slug.length; i++) {
8073    const char = slug.charAt(i);
8074    if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') {
8075      filtered += char;
8076    }
8077  }
8078  slug = filtered;
8079  
8080  /*
8081  // Clean up multiple hyphens
8082  while (slug.indexOf('--') !== -1) {
8083    slug = slug.replace('--', '-');
8084  }
8085    */
8086  
8087  // Remove leading and trailing hyphens
8088  if (slug.startsWith('-')) {
8089    slug = slug.substring(1);
8090  }
8091  if (slug.endsWith('-')) {
8092    slug = slug.substring(0, slug.length - 1);
8093  }
8094  
8095  // If slug is empty after processing, use a default
8096  if (!slug || slug === '-') {
8097    return 'page-' + Date.now();
8098  }
8099  
8100  return slug;
8101}
8102
8103// Update the addPage function to use our new robust slugify function
8104/**
8105 * Adds a new page to the galleries array
8106 * @returns {number|null} The new page ID or null if error
8107 */
8108function addPage() {
8109  try {
8110    // Get the page title
8111    const titleInput = document.getElementById('galleryTitle');
8112    if (!titleInput) {
8113      console.error('Page title input not found');
8114      return;
8115    }
8116    const title = titleInput.value.trim();
8117
8118    // Validate title is not empty
8119    if (!title) {
8120      alert('Please enter a page title');
8121      return;
8122    }
8123
8124    // Get parent ID with error handling
8125    const parentSelect = document.getElementById('galleryParent');
8126    const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null;
8127
8128    // Generate a unique ID for the page
8129    const id = Date.now();
8130    const pageId = `page_${id}`;
8131
8132    // Calculate slug using our more robust slugify function
8133    const pageSlug = slugify(title);
8134 
8135    // Create the page object with explicit properties
8136    const page = {
8137      id: id,
8138      title: title,
8139      isPage: true,
8140      isIntegrated: true,
8141      isSubmenu: false,
8142      pageId: pageId,
8143      visible: true,
8144      parentId: parentId,
8145      siteId: siteId,
8146      // Set slug and URL explicitly
8147      slug: pageSlug,
8148      url: '/' + pageSlug
8149    };
8150
8151    console.log(`Creating new page with ID: ${id}, title: "${title}"`);
8152
8153    // Add to galleries array
8154    galleries.push(page);
8155    
8156    // CRITICAL: Ensure window.galleries is synchronized
8157    if (typeof window.galleries === 'undefined') {
8158      window.galleries = galleries;
8159    } else {
8160      // Make sure the page is in window.galleries too
8161      const existingIndex = window.galleries.findIndex(g => g.id === id);
8162      if (existingIndex >= 0) {
8163        window.galleries[existingIndex] = page;
8164      } else {
8165        window.galleries.push(page);
8166      }
8167    }
8168
8169    // Initialize empty page elements array in PageManager
8170    if (window.PageManager) {
8171      window.PageManager.elements[pageId] = [];
8172    }
8173
8174    // Update UI first
8175    renderGalleries();
8176    clearAddForm();
8177    toggleAddForm();
8178
8179    // Set as active page
8180    console.log(`Setting new page "${page.title}" as active`);
8181    activeGalleryId = id;
8182    window.activeGalleryId = id;
8183    
8184    // Update active states in the menu
8185    updateActiveStates();
8186
8187    // Save to server
8188    saveGalleries().then(() => {
8189      console.log(`Page "${page.title}" saved successfully`);
8190      
8191      // Double check that our gallery is still in the array
8192      const pageExists = galleries.some(g => g.id === id);
8193      console.log(`Page exists in galleries array: ${pageExists}`);
8194      
8195      if (pageExists) {
8196        // Load the new page after a slight delay to ensure DOM is updated
8197        setTimeout(() => {
8198          console.log(`Loading new page "${page.title}" with ID: ${id}`);
8199          loadPage(id);
8200        }, 50);
8201      } else {
8202        console.error(`Page with ID ${id} not found in galleries array after save`);
8203      }
8204    }).catch(error => {
8205      alert('Error saving page. Please try again.');
8206    });
8207
8208    return id;
8209  } catch (error) {
8210    console.error('Error adding page:', error);
8211    alert('Error adding page. Please try again.');
8212    return null;
8213  }
8214}
8215
8216/**
8217 * Ensures a slug is unique among existing galleries
8218 * @param {string} slug - The base slug to check
8219 * @param {Array} existingGalleries - Array of existing gallery configs
8220 * @returns {string} A unique slug
8221 */
8222function ensureUniqueSlug(slug, existingGalleries) {
8223  // Guard against invalid slugs
8224  if (!slug || slug === '-') {
8225    slug = 'page-' + Date.now();
8226  }
8227
8228  let finalSlug = slug;
8229  let counter = 1;
8230  
8231  // Check if slug already exists
8232  while (existingGalleries.some(g => g.slug === finalSlug)) {
8233    finalSlug = `${slug}-${counter}`;
8234    counter++;
8235  }
8236  
8237  return finalSlug;
8238}
8239
8240/**
8241 * Adds a new gallery based on the selected type
8242 * @returns {number|null} The new gallery ID or null if error
8243 */
8244function addGallery() {
8245  try {
8246    // Get the gallery title
8247    const titleInput = document.getElementById('galleryTitle');
8248    if (!titleInput) {
8249      console.error('Gallery title input not found');
8250      return;
8251    }
8252    const title = titleInput.value.trim();
8253    
8254    // Get the selected gallery type
8255    const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked');
8256    if (!galleryTypeRadio) {
8257      console.error('No gallery type selected');
8258      alert('Please select a gallery type');
8259      return;
8260    }
8261
8262    const galleryType = galleryTypeRadio.value;
8263    console.log("Selected gallery type:", galleryType);
8264
8265    // If this is a page, use the addPage function
8266    if (galleryType === 'page') {
8267      console.log("Creating page instead of gallery");
8268      return addPage();
8269    }
8270
8271    // For spacer type, we don't require a title
8272    if (galleryType !== 'spacer' && !title) {
8273      alert('Please enter a gallery title');
8274      return;
8275    }
8276
8277    // Continue with gallery creation logic
8278    // Generate unique ID
8279    const id = Date.now();
8280    
8281    // Get parent gallery if selected
8282    const parentSelect = document.getElementById('galleryParent');
8283    const parentId = parentSelect.value ? parseInt(parentSelect.value) : null;
8284    
8285    // Get URL if this is an external gallery
8286    const galleryUrl = document.getElementById('galleryUrl');
8287    const url = galleryTypeRadio.value === 'external' && galleryUrl 
8288      ? galleryUrl.value.trim() 
8289      : "";
8290    
8291    // Set basic properties
8292    const gallery = {
8293      id: id,
8294      title: title || "Spacer", // Use "Spacer" as internal title if no title provided
8295      parentId: parentId,
8296      visible: true
8297    };
8298    
8299    // Set type-specific properties
8300    if (galleryTypeRadio.value === 'spacer') {
8301      // Spacer properties
8302      gallery.isSpacer = true;
8303      gallery.title = title || "Spacer"; // For internal reference
8304    } else if (galleryTypeRadio.value === 'integrated') {
8305      // Generate pageId for integrated galleries
8306      gallery.isIntegrated = true;
8307      gallery.pageId = generatePageId();
8308      
8309      // Generate and ensure unique slug
8310      const baseSlug = slugify(title);
8311      gallery.slug = ensureUniqueSlug(baseSlug, galleries);
8312      gallery.url = "/" + gallery.slug;
8313      
8314      // Get the siteId from multiple possible sources
8315      const siteId = window.Parameters?.siteId || 
8316                    window.siteId || 
8317                    document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') ||
8318                    '';
8319      
8320      // Add galleryOptions with GUID=siteId as the manualCollectionName
8321      gallery.galleryOptions = {
8322        manualCollectionName: `GUID=${siteId}`,
8323        isIntegratedGallery: true,
8324        pageId: gallery.pageId,  // Store pageId for reference
8325        siteId: siteId
8326      };
8327    } else if (galleryTypeRadio.value === 'folder') {
8328      gallery.isFolder = true;
8329      gallery.isCollapsed = true; // Make folders closed by default
8330    } else if (galleryTypeRadio.value === 'external') {
8331      // External URL
8332      gallery.url = url;
8333      gallery.isExternal = true; // Add this flag to identify external URLs
8334    }
8335    
8336    console.log(`Creating new gallery with ID: ${id}, title: "${title}"`);
8337    
8338    // Add to galleries array
8339    galleries.push(gallery);
8340    
8341    // CRITICAL: Ensure window.galleries is synchronized
8342    if (typeof window.galleries === 'undefined') {
8343      window.galleries = galleries;
8344    } else {
8345      // Make sure the gallery is in window.galleries too
8346      const existingIndex = window.galleries.findIndex(g => g.id === id);
8347      if (existingIndex >= 0) {
8348        window.galleries[existingIndex] = gallery;
8349      } else {
8350        window.galleries.push(gallery);
8351      }
8352    }
8353    
8354    // Update UI first
8355    renderGalleries();
8356    clearAddForm();
8357    toggleAddForm();
8358    
8359    // Save galleries after UI update
8360    saveGalleries().then(() => {
8361      console.log(`Gallery "${gallery.title}" saved successfully`);
8362      
8363      // Double check that our gallery is still in the array
8364      const galleryExists = galleries.some(g => g.id === id);
8365      console.log(`Gallery exists in galleries array: ${galleryExists}`);
8366      
8367      // For non-folder types, load the new gallery content after a slight delay
8368      if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) {
8369        setTimeout(() => {
8370          console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`);
8371          if (gallery.isIntegrated) {
8372            loadGallery(id);
8373          } else if (gallery.isExternal && gallery.url) {
8374            // For external URLs, set the iframe src and mark active after navigation target is ready
8375            activeGalleryId = id;
8376            window.activeGalleryId = id;
8377            updateActiveStates();
8378            const frame = document.getElementById('galleryFrame');
8379            if (frame) frame.src = gallery.url;
8380          }
8381        }, 50);
8382      }
8383    }).catch(error => {
8384      console.error(`Error saving gallery "${gallery.title}":`, error);
8385      alert('Error saving gallery. Please try again.');
8386    });
8387    
8388    // Return the new gallery ID
8389    return id;
8390  } catch (error) {
8391    console.error('Error adding gallery:', error);
8392    alert('Error adding gallery. Please try again.');
8393  }
8394}
8395
8396function debugDOMState(message) {
8397console.log(`[DEBUG] ${message}`);
8398const galleryContainer = document.querySelector('.gallery-container');
8399const iframe = document.getElementById('galleryFrame');
8400const pageContainers = document.querySelectorAll('.page-container');
8401
8402console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`);
8403console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`);
8404console.log(`- Page containers: ${pageContainers.length} found`);
8405
8406pageContainers.forEach((container, index) => {
8407console.log(`  - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`);
8408});
8409}
8410
8411
8412/**
8413* Global helper: whether edit tools are explicitly toggled open.
8414* Do not use Parameters.isInEditor — that is also true for logged-in admins (cache bypass).
8415* Do not infer from sidebar/page .editing alone (can desync from the toggle).
8416*/
8417window.isInEditMode = function() {
8418  if (document.body.classList.contains('edit-mode-active')) {
8419    return true;
8420  }
8421  if (typeof window.isEditing === 'boolean') {
8422    return window.isEditing;
8423  }
8424  return false;
8425};
8426
8427/**
8428* This function synchronizes edit mode state across all components
8429*/
8430window.updateGlobalEditState = function(isEditing) {
8431console.log('Updating global edit state:', isEditing);
8432
8433// Update global variable
8434window.isEditing = isEditing;
8435
8436// Update Parameters object if it exists
8437if (window.Parameters) {
8438window.Parameters.isInEditor = isEditing;
8439}
8440
8441// Update sidebar class
8442const sidebar = document.querySelector('.sidebar');
8443if (sidebar) {
8444if (isEditing) {
8445  sidebar.classList.add('editing');
8446} else {
8447  sidebar.classList.remove('editing');
8448}
8449}
8450
8451// Update page containers
8452const pageContainers = document.querySelectorAll('.page-container');
8453pageContainers.forEach(container => {
8454if (isEditing) {
8455  container.classList.add('editing');
8456} else {
8457  container.classList.remove('editing');
8458}
8459});
8460
8461// Notify all components via event
8462document.dispatchEvent(new CustomEvent('edit-mode-changed', {
8463detail: { editing: isEditing }
8464}));
8465
8466if (document.body.classList.contains('menu-layout-horizontal') && typeof window.syncContentMarginToHorizontalHeader === 'function') {
8467  window.syncContentMarginToHorizontalHeader();
8468}
8469
8470if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8471  window.syncGalleryOptionsButtonVisibility();
8472}
8473
8474// Gallery Options panel styling depends on body.edit-mode-active — hide it when leaving edit mode
8475if (!isEditing && typeof window.closeOptionsPanel === 'function') {
8476  window.closeOptionsPanel();
8477}
8478};
8479
8480/** Show or hide gallery options (#editorButton) based on edit mode and active content type */
8481window.syncGalleryOptionsButtonVisibility = function() {
8482  const btn = document.getElementById('editorButton');
8483  if (!btn) return;
8484
8485  const inEditMode = document.body.classList.contains('edit-mode-active');
8486  // When content type is explicitly gallery, ignore leftover .page-container nodes
8487  // (common when Help sandbox switches from a page to Example Gallery).
8488  const onPageView = window._activeContentType === 'gallery'
8489    ? false
8490    : (window._activeContentType === 'page' ||
8491      !!document.querySelector('.gallery-container .page-container'));
8492  let isActiveGallery = false;
8493  const activeId = window.activeGalleryId;
8494  const list = window.galleries || (typeof galleries !== 'undefined' ? galleries : null);
8495  const activeItem = activeId != null && list && typeof findGalleryById === 'function'
8496    ? findGalleryById(list, activeId)
8497    : (Array.isArray(list) ? list.find(function(g) { return g && g.id === activeId; }) : null);
8498  if (activeId != null && inEditMode && !onPageView) {
8499    isActiveGallery = !!(activeItem && !isPageMenuItem(activeItem) && !activeItem.isFolder && !activeItem.isSpacer && !activeItem.isExternal);
8500  }
8501
8502  const shouldShow = inEditMode && !onPageView && isActiveGallery;
8503  if (shouldShow) {
8504    if (window.Parameters) {
8505      window.Parameters.isInEditor = true;
8506    }
8507    btn.classList.remove('gallery-options-force-hidden');
8508    btn.style.removeProperty('display');
8509    btn.style.setProperty('display', 'flex', 'important');
8510    btn.style.pointerEvents = 'auto';
8511  } else {
8512    btn.classList.add('gallery-options-force-hidden');
8513    btn.style.setProperty('display', 'none', 'important');
8514    btn.style.pointerEvents = '';
8515  }
8516};
8517
8518document.addEventListener('gallery-loaded', function() {
8519  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8520    window.syncGalleryOptionsButtonVisibility();
8521  }
8522});
8523
8524document.addEventListener('page-loaded', function() {
8525  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8526    window.syncGalleryOptionsButtonVisibility();
8527  }
8528});
8529
8530/**
8531 * Wait for PageManager after CDN→proxy late script load (ISP/Firefox clients).
8532 * Home/page bootstrap can run before the proxy copy of page-manager-v260720-021.js executes.
8533 */
8534function whenPageManagerReady(callback, options) {
8535  const maxAttempts = (options && options.maxAttempts) || 50;
8536  const intervalMs = (options && options.intervalMs) || 100;
8537  let attempts = 0;
8538  const run = function() {
8539    if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8540      callback(window.PageManager);
8541      return;
8542    }
8543    attempts += 1;
8544    if (attempts >= maxAttempts) {
8545      console.error('❌ [loadPage] Timed out waiting for PageManager after', attempts, 'attempts');
8546      if (options && typeof options.onTimeout === 'function') options.onTimeout();
8547      return;
8548    }
8549    if (attempts === 1 || attempts % 10 === 0) {
8550      console.warn('⏳ [loadPage] Waiting for PageManager (CDN proxy late load?) attempt', attempts + '/' + maxAttempts);
8551    }
8552    setTimeout(run, intervalMs);
8553  };
8554  run();
8555}
8556
8557/**
8558 * loadPage function that safely handles undefined galleries
8559 * @param {number} id - The gallery ID
8560 * @param {Event} event - Optional event object
8561 */
8562function loadPage(id, event){
8563  if (event && event.target && event.target.isConnected === false) {
8564    console.log('loadPage: Ignoring navigation from detached element');
8565    return;
8566  }
8567  if (event) {
8568    event.preventDefault();
8569    event.stopPropagation();
8570    const now = Date.now();
8571    const lastCallTime = window._lastPageLoadTime || 0;
8572    window._lastPageLoadTime = now;
8573    if (now - lastCallTime < 100) {
8574      console.log('Ignoring duplicate loadPage call');
8575      return;
8576    }
8577  }
8578  console.log('loadPage: Loading page with gallery ID:', id);
8579
8580  stopAutoAdvanceTimer();
8581  if (typeof dismissGalleryPasswordOverlay === 'function') {
8582    dismissGalleryPasswordOverlay();
8583  }
8584
8585  let galleriesData = window.galleries || galleries;
8586  const gallery = findGalleryById(galleriesData, id);
8587
8588  if (!gallery) {
8589    console.warn('No gallery found with ID:', id, 'for page load.');
8590    return;
8591  }
8592
8593  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
8594
8595  window.activeGalleryId = id;
8596  activeGalleryId = id;
8597  window._activeContentType = 'page';
8598  if (window.EnhancedGallerySystem) {
8599    window.EnhancedGallerySystem.currentGalleryId = null;
8600  }
8601
8602  if (typeof window.removeGalleryScriptsWithPause === 'function') {
8603    window.removeGalleryScriptsWithPause();
8604  }
8605
8606  const galleryContainer = document.querySelector('.gallery-container');
8607  if (galleryContainer) {
8608    galleryContainer.innerHTML = '';
8609  }
8610
8611  if (!gallery.pageId) gallery.pageId = `page_${id}`;
8612  if (!gallery.isPage) gallery.isPage = true;
8613
8614  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8615    window.syncGalleryOptionsButtonVisibility();
8616  }
8617
8618  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
8619  window._pageIdToGalleryId[gallery.pageId] = id;
8620
8621  if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
8622    if (window.PageManager && window.PageManager.elements) {
8623      window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8624    }
8625  } else if (window.PageManager && window.PageManager.elements &&
8626             window.PageManager.elements[gallery.pageId] &&
8627             window.PageManager.elements[gallery.pageId].length > 0) {
8628    gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
8629  }
8630
8631  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
8632    try {
8633      window.PageManager.loadPage(gallery.pageId);
8634      const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
8635      if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
8636        window.PageManager.setEditMode(isCurrentlyEditing);
8637      }
8638    } catch (error) {
8639      console.error('Error loading page with PageManager:', error);
8640    }
8641  } else {
8642    console.error('PageManager not found yet — waiting for CDN proxy script');
8643    whenPageManagerReady(function() {
8644      try {
8645        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
8646          console.log('loadPage: Skipping stale PageManager retry for', gallery.pageId);
8647          return;
8648        }
8649        if (gallery.pageElements && Array.isArray(gallery.pageElements) && window.PageManager.elements) {
8650          window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
8651        }
8652        window.PageManager.loadPage(gallery.pageId);
8653        console.log('loadPage: PageManager became available — page render retried');
8654      } catch (error) {
8655        console.error('Error loading page with PageManager after wait:', error);
8656      }
8657    });
8658  }
8659
8660  // Apply menu visibility
8661  const bodyEl = document.body;
8662  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
8663      bodyEl.classList.add('menu-hidden-on-page');
8664  } else {
8665      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
8666  }
8667  applyMenuOverlayClassForGallery(gallery);
8668
8669  if (typeof updateActiveStates === 'function') updateActiveStates();
8670  if (typeof updateMobileTitle === 'function') updateMobileTitle();
8671  if (typeof closeMobileMenu === 'function') closeMobileMenu();
8672  // Close the gallery options panel when navigating to a different page
8673  if (typeof window.closeOptionsPanel === 'function') {
8674    window.closeOptionsPanel();
8675  }
8676  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
8677  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
8678    window.syncGalleryOptionsButtonVisibility();
8679  }
8680  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
8681    window.dispatchEvent(new CustomEvent('page-loaded', {
8682      detail: { pageId: gallery.pageId, galleryId: id }
8683    }));
8684  }
8685}
8686
8687
8688
8689
8690
8691function generatePageId() {
8692const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
8693let result = '';
8694const charactersLength = characters.length;
8695for (let i = 0; i < 8; i++) {
8696  result += characters.charAt(Math.floor(Math.random() * charactersLength));
8697}
8698return result;
8699};
8700
8701
8702function normalizeNameForComparison(name) {
8703if (!name) {
8704console.log("Empty name passed to normalization");
8705return '';
8706}
8707
8708console.log(`Normalizing name: "${name}"`);
8709
8710// Step 1: Convert to lowercase
8711let normalized = name.toLowerCase();
8712console.log(`After lowercase: "${normalized}"`);
8713
8714// Step 2: Replace spaces with hyphens (without regex)
8715normalized = normalized.split(' ').join('-');
8716console.log(`After space replacement: "${normalized}"`);
8717
8718// Step 3: Filter out unwanted characters (without regex)
8719let filtered = '';
8720for (let i = 0; i < normalized.length; i++) {
8721const char = normalized.charAt(i);
8722// Keep only a-z, 0-9, and hyphens
8723if ((char >= 'a' && char <= 'z') || 
8724    (char >= '0' && char <= '9') || 
8725    char === '-') {
8726  filtered += char;
8727}
8728}
8729normalized = filtered;
8730console.log(`After character filtering: "${normalized}"`);
8731
8732// Step 4: Clean up multiple hyphens (without regex)
8733while (normalized.indexOf('--') !== -1) {
8734normalized = normalized.split('--').join('-');
8735}
8736
8737// Step 5: Remove leading and trailing hyphens (without regex)
8738if (normalized.charAt(0) === '-') {
8739normalized = normalized.substring(1);
8740}
8741if (normalized.charAt(normalized.length - 1) === '-') {
8742normalized = normalized.substring(0, normalized.length - 1);
8743}
8744
8745// Check if the result is valid
8746if (!normalized || normalized === '-') {
8747console.log(`WARNING: Normalization produced invalid result: "${normalized}"`);
8748} else {
8749console.log(`Final normalized slug: "${normalized}"`);
8750}
8751
8752return normalized;
8753}
8754
8755function ensureUniqueSlug(slug, existingGalleries) {
8756if (!slug) return 'gallery';
8757
8758let finalSlug = slug;
8759let counter = 1;
8760
8761// Check if slug exists in any gallery
8762const slugExists = function(s) {
8763for (let i = 0; i < existingGalleries.length; i++) {
8764  if (existingGalleries[i].slug === s) {
8765    return true;
8766  }
8767}
8768return false;
8769};
8770
8771// Keep incrementing counter until unique
8772while (slugExists(finalSlug)) {
8773finalSlug = slug + '-' + counter;
8774counter++;
8775}
8776
8777return finalSlug;
8778}
8779
8780/**
8781 * Gathers the current complete site configuration from the client-side state
8782 * and triggers a download of the data as a single JSON file.
8783 * This provides a simple way to back up or export the current site structure.
8784 */
8785function downloadCurrentSiteJSON() {
8786  try {
8787    console.log('Gathering current site data for download...');
8788
8789    // Get the current hostname to use in the filename.
8790    const hostname = window.location.hostname.replace('www.', '');
8791
8792    // --- FIX: Ensure we get the most up-to-date menu styles ---
8793    // The MenuStyleCustomizer.settings object is the most reliable source
8794    // for the current styles being used on the client.
8795    const currentMenuStyles = window.MenuStyleCustomizer?.settings || {};
8796
8797    // Get current galleries
8798    const currentGalleries = window.galleries || galleries || [];
8799
8800    // --- STREAMLINED: Process galleries without including image data ---
8801    const enhancedGalleries = currentGalleries.map(gallery => {
8802      // If this is a Classic collection gallery, add note about manual retrieval
8803      if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) {
8804        const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5);
8805        console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`);
8806        
8807        return {
8808          ...gallery,
8809          _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`,
8810          _classicGuid: classicGuid,
8811          _requiresClassicData: true
8812        };
8813      }
8814      
8815      return gallery;
8816    });
8817
8818    // Assemble the complete site configuration object from global state variables.
8819    // This structure mirrors what the `save-config` API endpoint expects, ensuring
8820    // the exported file is a complete and re-importable replica.
8821    const siteData = {
8822      // The core menu/page structure with enhanced Classic collection data.
8823      galleries: enhancedGalleries,
8824      
8825      // Admin emails are managed server-side for security. We export the list if it's
8826      // available on the client from a previous check, but the server remains the
8827      // source of truth. A re-import will not overwrite the admin list.
8828      adminEmails: window.siteConfig?.adminEmails || [],
8829      
8830      // The unique ID for the site.
8831      siteId: window.siteId || window.Parameters?.siteId || '',
8832      
8833      // Custom styling rules for the menu.
8834      menuStyles: currentMenuStyles,
8835      
8836      // All elements configured to appear in the sidebar.
8837      sidebarElements: window.SidebarManager?.elements || [],
8838      
8839      // Global site metadata for SEO and analytics.
8840      siteMetadata: window.siteMetadata || {},
8841      
8842      // Add metadata about Classic collections that need manual data retrieval
8843      _classicCollections: enhancedGalleries
8844        .filter(g => g._requiresClassicData)
8845        .map(g => ({
8846          title: g.title,
8847          guid: g._classicGuid,
8848          note: g._note
8849        }))
8850    };
8851
8852    console.log('Enhanced site data collected for export:', siteData);
8853
8854    // Convert the JavaScript object to a formatted JSON string.
8855    const jsonString = JSON.stringify(siteData, null, 2);
8856
8857    // Create a Blob, which is a file-like object of immutable, raw data.
8858    const blob = new Blob([jsonString], { type: 'application/json' });
8859
8860    // Create a temporary link element to trigger the browser's download functionality.
8861    const url = URL.createObjectURL(blob);
8862    const a = document.createElement('a');
8863    a.href = url;
8864    a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json'
8865    document.body.appendChild(a);
8866    a.click();
8867
8868    // Clean up by removing the temporary link and revoking the object URL to free up memory.
8869    document.body.removeChild(a);
8870    URL.revokeObjectURL(url);
8871
8872    console.log('Enhanced JSON download initiated successfully.');
8873
8874    // Show a helpful message about Classic collections
8875    const classicCount = siteData._classicCollections?.length || 0;
8876    
8877    if (classicCount > 0) {
8878      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 
8878need to manually fetch the Classic collection JSON files.`);
8879    }
8880
8881  } catch (error) {
8882    console.error('Error creating JSON download:', error);
8883    alert('An error occurred while preparing the download. Please check the console.');
8884  }
8885}
8886
8887// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array)
8888function buildHydraClassicNdjson(classicGuid, images) {
8889  const header = {
8890    title: 'Untitled',
8891    description: '',
8892    version: '1',
8893    isClassic: true,
8894    isClassicCollection: true,
8895    classicGuid: 'GUID=' + String(classicGuid || '')
8896  };
8897
8898  const imageArray = Array.isArray(images) ? images.map(function(img, idx) {
8899    var path = '';
8900    if (img) {
8901      path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || '';
8902    }
8903    // Derive filename base from any provided path/filename and normalize extension to .jpg
8904    var fnameBase = '';
8905    if (typeof path === 'string' && path.length > 0) {
8906      var slash = path.lastIndexOf('/');
8907      var raw = slash >= 0 ? path.substring(slash + 1) : path;
8908      // remove query/hash
8909      var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q);
8910      var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h);
8911      // strip extension if present
8912      var dot = raw.lastIndexOf('.');
8913      fnameBase = dot > 0 ? raw.substring(0, dot) : raw;
8914    } else if (img && typeof img.filename === 'string') {
8915      var rf = img.filename;
8916      var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2);
8917      var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2);
8918      var d2 = rf.lastIndexOf('.');
8919      fnameBase = d2 > 0 ? rf.substring(0, d2) : rf;
8920    }
8921    var ext = (img && img.metadata && typeof img.metadata.extension === 'string' && img.metadata.extension.startsWith('.'))
8922      ? img.metadata.extension : '.jpg';
8923    var filename = fnameBase ? (fnameBase + ext) : '';
8924    // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename}
8925    // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path
8926    var guidRaw = String(classicGuid || '').replace(/^GUID=/, '');
8927    var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path;
8928    var rec = {
8929      caption: (img && (img.caption || img.description)) || '',
8930      description: (img && (img.description || img.caption)) || '',
8931      isPreview: idx === 1,
8932      title: (img && img.title) || '',
8933      'alt-text': (img && (img['alt-text'] || img.altText || '')) || '',
8934      image: cdnImageUrl || '',
8935      link: '',
8936      'link-text': '',
8937      filename: filename,
8938      isClassic: true,
8939      isClassicCollection: true,
8940      classicGuid: 'GUID=' + String(classicGuid || '')
8941    };
8942    return rec;
8943  }) : [];
8944
8945  // Build NDJSON as: header line, imageMetadata array line, then one line per full image record
8946  var body = { imageMetadata: imageArray };
8947  var out = [JSON.stringify(header), JSON.stringify(body)];
8948  for (var k = 0; k < imageArray.length; k++) {
8949    out.push(JSON.stringify(imageArray[k]));
8950  }
8951  return out.join('\n') + '\n';
8952}
8953
8954async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) {
8955  // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB)
8956  const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson';
8957  const flyUrl = 'https://hydra-press-v2.fly.dev/publish';
8958  const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys';
8959  
8960  console.error('Starting dual NDJSON upload for:', fileName);
8961  console.error('Content length:', ndjsonContent.length, 'characters');
8962  
8963  // COMPRESSION: Compress for Irys only (not Tigris)
8964  console.error('🔄 Compressing NDJSON with gzip for Irys...');
8965  const encoder = new TextEncoder();
8966  const ndjsonBytes = encoder.encode(ndjsonContent);
8967  const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip'));
8968  const compressedBlob = await new Response(compressedStream).blob();
8969  const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer());
8970  
8971  const originalSize = ndjsonBytes.length;
8972  const compressedSize = compressedBytes.length;
8973  const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2);
8974  
8975  console.error('📊 Compression results:', {
8976    original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)',
8977    compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)',
8978    ratio: compressionRatio + '%'
8979  });
8980  
8981  const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload
8982  const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT;
8983  
8984  console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)');
8985  
8986  // STEP 1: Upload to Irys (non-blocking, compressed if <800KB)
8987  let irysResult = null;
8988  if (shouldUploadToIrys) {
8989    try {
8990      console.error('🔄 Starting Irys upload (compressed, non-blocking)...');
8991      
8992      // Convert compressed bytes to base64
8993      const compressedBase64 = btoa(String.fromCharCode(...compressedBytes));
8994      
8995      const irysResp = await fetch(irysUrl, {
8996        method: 'POST',
8997        headers: { 'content-type': 'application/json' },
8998        body: JSON.stringify({ 
8999          compressedData: compressedBase64,
9000          originalSize: originalSize,
9001          compressedSize: compressedSize,
9002          siteId: siteId,
9003          pageId: pageId
9004        })
9005      });
9006      
9007      if (irysResp.ok) {
9008        irysResult = await irysResp.json();
9009        console.error('✅ Irys upload successful:', irysResult.txId);
9010        console.error('   Irys Gateway:', irysResult.urls?.irys);
9011        console.error('   ar.io Gateway:', irysResult.urls?.arweave);
9012        
9013        // Log wallet balance and cost
9014        if (irysResult.walletBalance !== undefined) {
9015          console.error('💰 Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR');
9016        }
9017        if (irysResult.uploadCost !== undefined) {
9018          const costUSD = (irysResult.uploadCost * 20).toFixed(4);
9019          if (irysResult.uploadCost === 0) {
9020            console.error('💸 Upload Cost: FREE (0 AR)');
9021          } else {
9022            console.error('💸 Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)');
9023          }
9024        }
9025      } else {
9026        const errorText = await irysResp.text().catch(() => 'Unknown error');
9027        console.warn('⚠️ Irys upload failed (non-critical):', irysResp.status, errorText);
9028      }
9029    } catch (irysError) {
9030      console.warn('⚠️ Irys upload error (non-critical):', irysError.message);
9031      // Continue - Irys failure doesn't block Fly.io upload
9032    }
9033  } else {
9034    console.error('⏭️ Skipping Irys upload (compressed size exceeds 800KB limit)');
9035  }
9036  
9037  // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed)
9038  // Hydra sites require Bearer auth - get token from TokenManager/localStorage (same as Edition Publisher)
9039  try {
9040    console.error('🔄 Starting Fly.io upload (uncompressed)...');
9041    const flyHeaders = { 'content-type': 'application/json' };
9042    let authToken = null;
9043    try {
9044      if (window.TokenManager) {
9045        if (typeof window.TokenManager.getHydraAuthToken === 'function') {
9046          try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
9047        }
9048        if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
9049          try {
9050            const tw = window.TokenManager.getBestToken();
9051            authToken = tw && tw.startsWith('Bearer ') ? tw.substring(7) : tw;
9052          } catch (e) {}
9053        }
9054      }
9055      if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
9056      if (!authToken) authToken = window.authToken;
9057      if (authToken) {
9058        flyHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
9059          ? 'Bearer ' + authToken
9060          : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
9061        console.error('🔐 Fly.io auth: token attached');
9062      } else {
9063        console.warn('⚠️ No auth token - Fly.io may reject Hydra site upload (401)');
9064      }
9065    } catch (authErr) {
9066      console.warn('⚠️ Auth token lookup failed:', authErr.message);
9067    }
9068    const flyResp = await fetch(flyUrl, {
9069      method: 'POST',
9070      headers: flyHeaders,
9071      body: JSON.stringify({ ndjsonContent, dataFileName: fileName })
9072    });
9073    
9074    console.log('Server response status:', flyResp.status);
9075    
9076    if (!flyResp.ok) {
9077      const errorText = await flyResp.text().catch(() => 'Unknown error');
9078      console.error('❌ Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText);
9079      if (flyResp.status === 401) {
9080        console.error('💡 Tip: Open the Edition Publisher (or log in as admin) first to authenticate for Fly.io uploads.');
9081      }
9082      return { success: false, irysResult: null };
9083    }
9084    
9085    const flyData = await flyResp.json().catch(() => null);
9086    if (flyData && flyData.urls) {
9087      console.log('✅ Fly.io upload successful:', flyData.urls);
9088      console.log('   Tigris URL:', flyData.urls.urlTigris);
9089      
9090      // Return comprehensive result
9091      const result = {
9092        success: true,
9093        flyUrls: flyData.urls,
9094        irysResult: irysResult,
9095        uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'],
9096        fileSize: compressedSize, // Irys compressed size
9097        originalFileSize: originalSize // Original uncompressed size
9098      };
9099      
9100      console.log('📦 Upload complete:', {
9101        destinations: result.uploadedTo.join(' + '),
9102        txId: irysResult?.txId || 'N/A',
9103        flyUrl: flyData.urls.urlTigris
9104      });
9105      
9106      return result;
9107    } else {
9108      console.warn('Fly.io upload response missing URLs:', flyData);
9109      return { success: false, irysResult: null };
9110    }
9111  } catch (flyError) {
9112    console.error('❌ Fly.io upload error for', fileName + ':', flyError);
9113    return { success: false, irysResult: null };
9114  }
9115}
9116
9117async function importClassicGalleries() {
9118  // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS
9119  function cleanDescriptionLeadingLines(text) {
9120    if (!text || typeof text !== 'string') return text;
9121
9122    let result = text;
9123
9124    // Remove leading empty TEXTFORMAT blocks: <TEXTFORMAT><P><FONT></FONT></P></TEXTFORMAT>
9125    result = result.replace(
9126      new RegExp(
9127        '^(\s*\<TEXTFORMAT[^\>]*\>\s*\<P[^\>]*\>\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+',
9128        'i'
9129      ),
9130      ''
9131    );
9132
9133    // Remove leading empty P tags
9134    result = result.replace(
9135      new RegExp('^(\s*\<P[^\>]*\>\s*\<\/P\>\s*)+', 'i'),
9136      ''
9137    );
9138
9139    // Remove leading empty FONT tags
9140    result = result.replace(
9141      new RegExp('^(\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*)+', 'i'),
9142      ''
9143    );
9144
9145    // Remove leading BR tags
9146    result = result.replace(
9147      new RegExp('^(\s*\<br\s*\/\?>\s*)+', 'i'),
9148      ''
9149    );
9150
9151    // Remove any remaining leading whitespace
9152    return result.replace(new RegExp('^\s+', 'i'), '').trim();
9153  }
9154
9155  // Browser-scoped helper: fix multi-line links (template-safe escaped regex)
9156  function fixMultilineLinks(text) {
9157    if (!text || typeof text !== 'string') return text;
9158    return text.replace(
9159      new RegExp('<a[^>]*>.*?Click.*?<\/a><a[^>]*>.*?HERE.*?<\/a><a[^>]*>.*?to Add to Cart.*?<\/a>', 'gis'),
9160      function (match) {
9161        const first = match.match(new RegExp('<a href="([^"]*)" target="([^"]*)"', 'i'));
9162        if (first) {
9163          return '<a href="' + first[1] + '" target="' + first[2] + '">
9163Click HERE to Add to Cart</a>';
9164        }
9165        return match;
9166      }
9167    );
9168  }
9169
9170  // Browser-scoped helper: convert plain text link phrase to HTML link
9171  function convertPlainTextLinks(text) {
9172    if (!text || typeof text !== 'string') return text;
9173    return text.replace(
9174      new RegExp('Click HERE to Add to Cart', 'g'),
9175      '<a href="#" target="_blank">Click HERE to Add to Cart</a>'
9176    );
9177  }
9178  // Get input elements, including the new checkbox
9179  const guidInputEl = document.getElementById('classicGuid');
9180  const pastedJsonEl = document.getElementById('pastedJson');
9181  const parentIdInputEl = document.getElementById('importParent');
9182  const createSubmenuCheckboxEl = document.getElementById('createSubmenu');
9183  const submenuTitleInputEl = document.getElementById('submenuTitle');
9184  const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox');
9185
9186  // Get values from inputs
9187  const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : '';
9188  const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : '';
9189  const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false;
9190  
9191  let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null;
9192  let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false;
9193  let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : '';
9194
9195  // If replacing the menu, ignore parent/submenu settings
9196  if (replaceMenu) {
9197      parentId = null;
9198      createSubmenu = false;
9199  }
9200
9201  // Validate submenu title if checkbox is checked
9202  if (createSubmenu && !submenuTitle) {
9203    alert('Please provide a title for the submenu');
9204    return;
9205  }
9206
9207  // Show status display
9208  const statusEl = document.getElementById('importStatus');
9209  const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null;
9210  const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null;
9211  const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null;
9212  const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null;
9213
9214  if (statusEl && progressBar && statusMessage) {
9215    statusEl.style.display = 'block';
9216    statusEl.classList.add('importing');
9217    progressBar.style.width = '10%';
9218    statusMessage.textContent = 'Preparing to import...';
9219  }
9220
9221  try {
9222    let classicData;
9223    let classicGuid; // Will hold the primary GUID for image path lookups
9224
9225    // --- LOGIC TO GET DATA: Prioritize Pasted JSON ---
9226    if (pastedJson) {
9227      if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...';
9228      classicData = JSON.parse(pastedJson);
9229
9230      if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9231        throw new Error('Pasted JSON is invalid: "galleries" array not found.');
9232      }
9233      // Use the first available GUID from the pasted data as the default for images
9234      classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId;
9235
9236    } else if (guidInput) {
9237      // Check if this is the new import_ format
9238      if (guidInput.startsWith('import_')) {
9239        if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...';
9240        const importGuid = guidInput.substring(7); // Remove 'import_' prefix
9241        
9242        const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`;
9243        const response = await fetch(jsonUrl);
9244        if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
9245        
9246        classicData = await response.json();
9247        
9248        if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9249          throw new Error('Fetched import JSON is invalid: "galleries" array not found.');
9250        }
9251        
9252        // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata
9253        if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...';
9254        
9255        const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`;
9256        
9257        try {
9258          const originalResponse = await fetch(originalJsonUrl);
9259          if (originalResponse.ok) {
9260            const originalData = await originalResponse.json();
9261            
9262            // Get the default language from site configuration
9263            let defaultLanguageID = 'en'; // fallback
9264            if (originalData.languages && Array.isArray(originalData.languages)) {
9265              const defaultLang = originalData.languages.find(lang => lang.isDefault === true);
9266              if (defaultLang) {
9267                defaultLanguageID = defaultLang.id;
9268              }
9269            }
9270            
9271            // Merge image metadata from original JSON into import JSON galleries
9272            if (originalData.galleries && Array.isArray(originalData.galleries)) {
9273              classicData.galleries = classicData.galleries.map(importGallery => {
9274                // Find matching gallery in original data by name or categoryID
9275                
9276                const originalGallery = originalData.galleries.find(orig => 
9277                  orig.title === importGallery.title || // Primary match by title
9278                  orig.name === importGallery.title ||  // Fallback: orig.name matches import.title
9279                  orig.categoryID === importGallery.categoryID // Fallback: categoryID match
9280                );
9281                
9282                
9283                if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) {
9284                  
9285                  // Process all images from original gallery with metadata extraction
9286                    const processedImages = originalGallery.images.map(originalImage => {
9287                    // Apply EXACT same metadata processing logic as Edition Publisher
9288                    let title = originalImage.title || '';
9289                    let dateline = originalImage.dateline || '';
9290                    let caption = originalImage.caption || '';
9291                    
9292                    // Check languageVersions array for metadata (this is where most data is stored)
9293                    if (originalImage.languageVersions && originalImage.languageVersions.length > 0) {
9294                      // Look for site's default language first, then fall back to first available
9295                      let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID);
9296                      if (!langData) {
9297                        langData = originalImage.languageVersions[0];
9298                      }
9299                      
9300                      // For language-enabled JSON files, prioritize languageVersions data
9301                      // Only fall back to top-level if languageVersions data is empty
9302                      if (!title || title.trim() === '') title = langData.title || '';
9303                      if (!dateline || dateline.trim() === '') dateline = langData.dateline || '';
9304                      if (!caption || caption.trim() === '') caption = langData.caption || '';
9305                    }
9306                    
9307                    // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals)
9308                    let description = caption;
9309                    if (caption && caption.includes('TEXTFORMAT')) {
9310                      try {
9311                        // Use properly escaped HTML characters for template literal context
9312                        description = caption
9313                          .replace(/\<TEXTFORMAT[^\>]*\>/g, '')
9314                          .replace(/\<\/TEXTFORMAT\>/g, '')
9315                          .replace(/\<P[^\>]*\>/g, '\<p\>')
9316                          .replace(/\<\/P\>/g, '\<\/p\>')
9317                          .replace(/\<FONT[^\>]*\>/g, '')
9318                          .replace(/\<\/FONT\>/g, '')
9319                          .replace(/\<A HREF="([^"]*)"[^\>]*\>/g, '\<a href="$1" target="_blank"\>')
9320                          .replace(/\<\/A\>/g, '\<\/a\>')
9321                          .replace(/\<U\>/g, '\<u\>')
9322                          .replace(/\<\/U\>/g, '\<\/u\>')
9323                          .replace(/\<B\>/g, '\<strong\>')
9324                          .replace(/\<\/B\>/g, '\<\/strong\>')
9325                          .replace(/\<I\>/g, '\<em\>')
9326                          .replace(/\<\/I\>/g, '\<\/em\>')
9327                          .replace(/&quot;/g, '"')
9328                          .replace(/&apos;/g, "'")
9329                          .replace(/\s+/g, ' ')
9330                          .trim();
9331                      } catch (error) {
9332                        console.warn('TEXTFORMAT conversion failed, using original caption:', error);
9333                        description = caption;
9334                      }
9335                    }
9336                    
9337                    // Prepend dateline to description if it exists (convert to uppercase)
9338                    if (dateline) {
9339                      const uppercaseDateline = dateline.toUpperCase();
9340                      description = `${uppercaseDateline} ${description}`.trim();
9341                    }
9342                    
9343                    // Apply description fixes
9344                    description = cleanDescriptionLeadingLines(description);
9345                    description = fixMultilineLinks(description);
9346                    description = convertPlainTextLinks(description);
9347                    
9348                  const imageEntry = {
9349                        // Match EXACT structure from Edition Publisher hydraImageEntry
9350                        image: originalImage.image || originalImage.urls?.original || '',
9351                        caption: description, // Use processed description as caption
9352                        description: description, // ALSO set description field (like Edition Publisher)
9353                        title: title,
9354                        'alt-text': title, // Use title as alt-text like Edition Publisher
9355                        link: originalImage.urls?.original || '',
9356                        'link-text': originalImage.urls?.original || '',
9357                        isPreview: false,
9358                        filename: originalImage.filename || '',
9359                        originalUrl: originalImage.image || originalImage.urls?.original || '',
9360                        dateline: dateline
9361                  };
9362
9363                  return imageEntry;
9364                  });
9365                  
9366                  return {
9367                    ...importGallery, // Keep import gallery structure (order, settings, etc.)
9368                    images: processedImages // Replace with processed original images
9369                  };
9370                }
9371                return importGallery;
9372              });
9373            }
9374          } else {
9375            console.warn('Could not fetch original JSON for metadata, using import data only');
9376          }
9377        } catch (error) {
9378          console.warn('Error fetching original JSON for metadata:', error);
9379        }
9380        
9381        // Extract original GUID for image path lookups
9382        // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID
9383        // GUIDs are typically 13 characters, so split on common separators
9384        let baseGuid = importGuid;
9385        const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i);
9386        if (guidMatch) {
9387          baseGuid = guidMatch[1];
9388        } else {
9389          // Fallback: try to extract first 13-character hex string
9390          const hexMatch = importGuid.match(/^([a-f0-9]{13})/i);
9391          if (hexMatch) {
9392            baseGuid = hexMatch[1];
9393          }
9394        }
9395        
9396        // Use the base GUID for image path lookups (e.g., /images/filename.jpg)
9397        classicGuid = baseGuid;
9398        console.log('Successfully loaded hybrid import data with import GUID:', importGuid);
9399        console.log('Using base GUID for image paths:', classicGuid);
9400        
9401             } else {
9402         // Standard GUID format - existing logic
9403         if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...';
9404         classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput;
9405         console.log('Detected standard GUID format, GUID:', classicGuid);
9406         
9407         const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`;
9408         console.log('Fetching classic JSON from:', jsonUrl);
9409         const response = await fetch(jsonUrl);
9410
9411         if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`);
9412         classicData = await response.json();
9413         
9414         if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9415           throw new Error('Fetched JSON is invalid: "galleries" array not found.');
9416         }
9417         
9418         console.log('Successfully loaded classic data with GUID:', classicGuid);
9419       }
9420
9421    } else {
9422      alert('Please either paste site JSON or enter a Classic GUID to import.');
9423      if (statusEl) statusEl.style.display = 'none';
9424      return;
9425    }
9426    
9427    const galleriesForImport = classicData.galleries;
9428    if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`;
9429
9430    // import_ format: galleries come from DB/import JSON, not classic site - treat as new Hydra NDJSON galleries
9431    const isImportFormat = !!(guidInput && guidInput.startsWith('import_'));
9432
9433    // --- HIERARCHY RECONSTRUCTION LOGIC ---
9434    
9435    const currentSiteId = window.Parameters?.siteId || siteId;
9436    let submenuParentId = parentId;
9437    const tempNewItems = [];
9438    const originalIdToNewIdMap = {};
9439
9440    // Create the optional top-level submenu if requested
9441    if (createSubmenu) {
9442        const submenuItem = {
9443            id: Date.now(),
9444            title: submenuTitle,
9445            isSubmenu: true,
9446            isCollapsed: true,
9447            visible: true,
9448            parentId: parentId,
9449            siteId: currentSiteId,
9450            children: []
9451        };
9452        galleries.push(submenuItem);
9453        submenuParentId = submenuItem.id;
9454    }
9455
9456    // --- Pass 1: Create new items with correct data and map old IDs to new IDs ---
9457    for (let index = 0; index < galleriesForImport.length; index++) {
9458        const classicItem = galleriesForImport[index];
9459        const newItemId = Date.now() + index + 100;
9460        originalIdToNewIdMap[classicItem.id] = newItemId;
9461
9462        if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`;
9463        if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`;
9464
9465        const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`;
9466        
9467 let slug;
9468        // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean.
9469        // Otherwise (for GUID import), trust the existing slug if it's there.
9470        if (pastedJson) {
9471            slug = normalizeNameForComparison(title);
9472        } else if (classicItem.slug && classicItem.slug.trim() !== '') {
9473            slug = classicItem.slug;
9474        } else {
9475            slug = normalizeNameForComparison(title);
9476        }
9477const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness
9478
9479        // Determine the GUID to use for this specific item's image paths
9480        const itemGuid = classicItem.classicGuid || classicGuid;
9481
9482     let newItem;
9483
9484        // --- Check if it's an External URL ---
9485        if (classicItem.isExternal === true && classicItem.url) {
9486            console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`);
9487            newItem = {
9488                id: newItemId, 
9489                title, 
9490                url: classicItem.url, 
9491                isExternal: true, 
9492                visible: classicItem.visible !== false, 
9493                parentId: null,
9494                siteId: currentSiteId, 
9495                slug: finalSlug,
9496                importSource: 'external-url'
9497            };
9498        }
9499        // --- Check if it's a Page and build its elements ---
9500        else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) {
9501            const pageUniqueId = `page_${newItemId}`;
9502            
9503            let pageElements = [];
9504            
9505            // =================================================================
9506            // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format
9507            // =================================================================
9508            if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) {
9509                console.log(`Using existing pageElements for page: "${title}"`);
9510                
9511                // Check if we have both pageElements and images - create two-column layout
9512                if (classicItem.images && classicItem.images.length > 0) {
9513                    console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`);
9514                    
9515                    // Create metadata element
9516                    pageElements.push({
9517                        id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9518                        metaTitle: classicItem.meta?.title || title,
9519                        metaDescription: classicItem.meta?.description || "",
9520                        metaKeywords: classicItem.meta?.keywords || ""
9521                    });
9522                    
9523                    // Create two-column container
9524                    const leftColumnElements = [];
9525                    const rightColumnElements = [];
9526                    
9527                    // Add images to left column
9528                    const slides = classicItem.images.map((img) => {
9529                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9530                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9531                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9532                        
9533                        return { 
9534                            imageUrl, 
9535                            imageKey, 
9536                            imageFilename, 
9537                            imageType: img.metadata?.mimetype || 'image/jpeg',
9538                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9539                            title: img.title || '',
9540                            byline: img.byline || '',
9541                            dateline: img.dateline || '',
9542                            altText: img.metadata?.alt_text || ''
9543                        };
9544                    });
9545                    
9546                    leftColumnElements.push({
9547                        id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0,
9548                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9549                    });
9550                    
9551                    // Add text content to right column
9552                    classicItem.pageElements.forEach((el, elIndex) => {
9553                        if (el.type === 'text' && el.content) {
9554                            rightColumnElements.push({
9555                                id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex,
9556                                textContent: el.content, textWidth: 70
9557                            });
9558                        }
9559                    });
9560                    
9561                    const columns = [
9562                        { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements },
9563                        { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }
9564                    ];
9565                    
9566                    pageElements.push({
9567                        id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1,
9568                        columns,
9569                        backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9570                        backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9571                    });
9572                    
9573                } else {
9574                    // No images, just transform the pageElements as before
9575                    pageElements = classicItem.pageElements.map((el, elIndex) => {
9576                        const elementId = Date.now() + index + 1000 + elIndex;
9577                        
9578                        // Handle import_guid.json format (type + content)
9579                        if (el.type && el.content) {
9580                            if (el.type === 'text') {
9581                                return {
9582                                    id: elementId,
9583                                    type: 'text',
9584                                    title: 'Text Block',
9585                                    visible: true,
9586                                    position: elIndex,
9587                                    textContent: el.content,
9588                                    textWidth: 100
9589                                };
9590                            } else if (el.type === 'metadata') {
9591                                return {
9592                                    id: elementId,
9593                                    type: 'metadata',
9594                                    title: 'Metadata',
9595                                    visible: true,
9596                                    position: elIndex,
9597                                    metaTitle: el.metaTitle || title,
9598                                    metaDescription: el.metaDescription || '',
9599                                    metaKeywords: el.metaKeywords || ''
9600                                };
9601                            }
9602                        }
9603                        
9604                        // Handle existing format (already has id, position, etc.)
9605                        return {
9606                            ...el,
9607                            id: elementId,
9608                            position: el.position !== undefined ? el.position : elIndex
9609                        };
9610                    });
9611                }
9612
9613            } else {
9614                // Otherwise (for GUID-based import), generate the page structure from scratch.
9615                console.log(`Generating new pageElements for page: "${title}"`);
9616                pageElements.push({
9617                  id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0,
9618                  metaTitle: classicItem.meta?.title || title,
9619                  metaDescription: classicItem.meta?.description || "",
9620                  metaKeywords: classicItem.meta?.keywords || ""
9621                });
9622
9623                // Add images if they exist in the page
9624                if (classicItem.images && classicItem.images.length > 0) {
9625                    const slides = classicItem.images.map((img) => {
9626                        const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9627                        const imageKey = `${itemGuid}/images/${imageFilename}`;
9628                        const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9629                        
9630                        return { 
9631                            imageUrl, 
9632                            imageKey, 
9633                            imageFilename, 
9634                            imageType: img.metadata?.mimetype || 'image/jpeg',
9635                            caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))),
9636                            title: img.title || '',
9637                            byline: img.byline || '',
9638                            dateline: img.dateline || '',
9639                            altText: img.metadata?.alt_text || ''
9640                        };
9641                    });
9642                    
9643                    pageElements.push({
9644                        id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1,
9645                        slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true
9646                    });
9647                }
9648
9649                pageElements.push({
9650                  id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2,
9651                  textContent: "<p>&nbsp;</p><p><br></p>", textWidth: 100
9652                });
9653
9654                const columns = [];
9655                const leftColumnElements = [];
9656                const rightColumnElements = [];
9657
9658                if (classicItem.images && classicItem.images.length > 0) {
9659                  const slides = classicItem.images.map((img) => {
9660                    const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg');
9661                    const imageKey = `${itemGuid}/images/${imageFilename}`;
9662                    const imageUrl = `https://storage.neonsky.app/${imageKey}`;
9663                    
9664                    // Preserve all image metadata including captions, titles, etc.
9665                    return { 
9666                      imageUrl, 
9667                      imageKey, 
9668                      imageFilename, 
9669                      imageType: img.metadata?.mimetype || 'image/jpeg',
9670                      // Preserve caption information - handle both direct and language version storage patterns
9671                      caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))),
9672                      title: img.title || img.languageVersions?.[0]?.title || '',
9673                      byline: img.byline || img.languageVersions?.[0]?.byline || '',
9674                      dateline: img.dateline || img.languageVersions?.[0]?.dateline || '',
9675                      altText: img.metadata?.alt_text || '',
9676                      // Preserve any additional metadata
9677                      metaTitle: img.metadata?.meta_title || '',
9678                      metaDescription: img.metadata?.meta_description || '',
9679                      metaKeywords: img.metadata?.meta_keywords || '',
9680                      // Preserve gallery-specific data
9681                      sortOrder: img.galleryData?.sortOrder || 0,
9682                      linkURL: img.galleryData?.linkURL || null,
9683                      linkTarget: img.galleryData?.linkTarget || '_self'
9684                    };
9685                  });
9686                  leftColumnElements.push({
9687                    id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0,
9688                    slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true
9689                  });
9690                }
9691
9692                let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || "";
9693                if (rawDetails && typeof rawDetails === 'string') {
9694                    // =================================================================
9695                    // CHANGE 2: Fix for "Invalid regular expression flags" error.
9696                    // Changed from /.../gi literal to new RegExp('...', 'gi') constructor
9697                    // to prevent syntax errors.
9698                    // =================================================================
9699                    let processedText = rawDetails.replace(new RegExp('<TEXTFORMAT.*?>', 'gi'), '').replace(new RegExp('</TEXTFORMAT>', 'gi'), '');
9700                    processedText = processedText.replace(new RegExp('<P ALIGN=".*?"', 'gi'), '<p').replace(new RegExp('</P>', 'gi'), '</p>');
9701                    processedText = processedText.replace(new RegExp('<FONT.*?>', 'gi'), '').replace(new RegExp('</FONT>', 'gi'), '');
9702                    processedText = processedText.replace(new RegExp('<B>
9702', 'gi'), '<strong>').replace(new RegExp('</B>', 'gi'), '</strong>');
9703                    processedText = processedText.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
9704                    
9705                    // Apply description fixes to processed text
9706                    processedText = cleanDescriptionLeadingLines(processedText);
9707                    processedText = fixMultilineLinks(processedText);
9708                    processedText = convertPlainTextLinks(processedText);
9709                    
9710                    rightColumnElements.push({
9711                      id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0,
9712                      textContent: processedText, textWidth: 70
9713                    });
9714                }
9715                
9716                columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements });
9717                columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements });
9718
9719                pageElements.push({
9720                  id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2,
9721                  columns,
9722                  backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false },
9723                  backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 }
9724                });
9725            }
9726
9727            newItem = {
9728                id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null,
9729                siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements,
9730                classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page'
9731            };
9732                } else if (classicItem.isFolder === true) {
9733            // --- Handle folders from import_guid.json format ---
9734            console.log(`Creating folder: "${title}"`);
9735            newItem = {
9736                id: newItemId, 
9737                title, 
9738                isFolder: true, 
9739                isCollapsed: classicItem.isCollapsed !== false, 
9740                visible: classicItem.visible !== false, 
9741                parentId: null,
9742                siteId: currentSiteId, 
9743                slug: finalSlug,
9744                importSource: 'folder'
9745            };
9746        } else {
9747            // --- This is a Gallery ---
9748            const galleryPageId = generatePageId();
9749            
9750            // --- ENHANCED: Try to fetch and include actual image data with captions ---
9751            let enhancedGalleryOptions = {
9752                manualCollectionName: `GUID=${itemGuid}`, 
9753                categoryId: classicItem.categoryID, 
9754                importedFromClassic: true,
9755                siteId: currentSiteId, 
9756                pageId: galleryPageId
9757            };
9758            
9759            // --- ENHANCED: Preserve gallery display settings from Classic JSON ---
9760            if (classicItem.galleryOptions) {
9761                console.log(`Preserving gallery settings for: ${title}`);
9762                
9763                // Preserve layout and display settings
9764                const preservedSettings = [
9765                    'startInSingles', 'layoutType', 'columns', 'spacing',
9766                    'showDescription', 'showFilename', 'displayAllInfo',
9767                    'navigationMode', 'showTextBlock', 'fixedHeroImage',
9768                    'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration',
9769                    'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor',
9770                    'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity',
9771                    'lightboxCloseColor', 'lightboxArrowColor', 'useTitles',
9772                    'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign',
9773                    'showDescriptionInOverlay', 'includeRolloverImageInOverlay',
9774                    'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles',
9775                    'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames',
9776                    'openMultipleLightboxes', 'batchSize', 'autoplaySingles',
9777                    'autoplayDuration', 'autoplayTransition', 'autoplayRandomStart'
9778                ];
9779                
9780                preservedSettings.forEach(setting => {
9781                    if (classicItem.galleryOptions[setting] !== undefined) {
9782                        enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting];
9783                    }
9784                });
9785                
9786                // Preserve text content
9787                if (classicItem.galleryOptions.horizontalScrollerText) {
9788                    enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText;
9789                }
9790                
9791                // Preserve JSON collections if present
9792                if (classicItem.galleryOptions.jsonCollections) {
9793                    enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections;
9794                } else {
9795                    // Create jsonCollections structure with processed image metadata
9796                    const collectionKey = 'GUID=' + itemGuid;
9797                    const processedImages = classicItem.images || [];
9798                    
9799                    enhancedGalleryOptions.jsonCollections = {
9800                        [collectionKey]: {
9801                            isClassicCollection: true,
9802                            guid: collectionKey,
9803                            metadata: processedImages,
9804                            totalItems: processedImages.length
9805                        }
9806                    };
9807                    
9808                    console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images');
9809
9810                    // Build and try to upload NDJSON for this gallery.
9811                    // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON.
9812                    // Skip NDJSON generation for galleries with no images to avoid creating empty files
9813                    if (processedImages.length > 0) {
9814                      try {
9815                        console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)');
9816                        const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9817                        console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters');
9818                        
9819                        const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9820                      if (uploadResult && uploadResult.success) {
9821                        try {
9822                          // import_ format: treat as new Hydra NDJSON gallery (not classic-dependent)
9823                          if (isImportFormat) {
9824                            const hydraCollectionKey = 'GUID=' + currentSiteId;
9825                            enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9826                            enhancedGalleryOptions.jsonCollections = {
9827                              [hydraCollectionKey]: {
9828                                isClassicCollection: false,
9829                                guid: hydraCollectionKey,
9830                                metadata: {},
9831                                totalItems: processedImages.length
9832                              }
9833                            };
9834                            enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9835                            enhancedGalleryOptions.initialPageUuid = galleryPageId;
9836                            enhancedGalleryOptions.isHydra = true;
9837                            enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9838                            console.log('NDJSON uploaded for import_ gallery "' + title + '" - configured as Hydra NDJSON (not classic)');
9839                          } else {
9840                            enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {};
9841                            enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length;
9842                          }
9843                          
9844                          // Store dual upload metadata if Irys was successful
9845                          if (uploadResult.irysResult && uploadResult.irysResult.txId) {
9846                            enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9847                            enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing
9848                            enhancedGalleryOptions.isPerma = true;
9849                            enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo;
9850                            enhancedGalleryOptions.fileSize = uploadResult.fileSize;
9851                            enhancedGalleryOptions.siteId = currentSiteId;
9852                            enhancedGalleryOptions.pageId = galleryPageId;
9853                            console.log('✅ Dual upload complete - Irys txId:', uploadResult.irysResult.txId);
9854                            console.log('   Uploaded to:', uploadResult.uploadedTo.join(' + '));
9855                          } else {
9856                            console.log('✅ Single upload (Fly.io only) - No Irys data');
9857                          }
9858                        } catch (applyErr) {
9859                          console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr);
9860                        }
9861                      } else {
9862                        console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array');
9863                      }
9864                      } catch (e) {
9865                        console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e);
9866                      }
9867                    } else {
9868                      console.log('Skipping NDJSON generation for "' + title + '" - no images to process');
9869                    }
9870                }
9871                
9872                // Preserve additional metadata
9873                if (classicItem.galleryOptions.siteAlias) {
9874                    enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias;
9875                }
9876                if (classicItem.galleryOptions.initialPageUuid) {
9877                    enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid;
9878                }
9879                if (classicItem.galleryOptions.initialPageAlias) {
9880                    enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias;
9881                }
9882                if (classicItem.galleryOptions.timestamp) {
9883                    enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp;
9884                }
9885            } else if (isImportFormat && (classicItem.images || []).length > 0) {
9886                // import_ format without galleryOptions: still create NDJSON for new Hydra galleries
9887                const processedImages = classicItem.images || [];
9888                const collectionKey = 'GUID=' + itemGuid;
9889                enhancedGalleryOptions.jsonCollections = {
9890                  [collectionKey]: {
9891                    isClassicCollection: true,
9892                    guid: collectionKey,
9893                    metadata: processedImages,
9894                    totalItems: processedImages.length
9895                  }
9896                };
9897                console.log('Created jsonCollections for import_ gallery (no galleryOptions):', title, '(' + processedImages.length + ' images)');
9898                try {
9899                  const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages);
9900                  const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent);
9901                  if (uploadResult && uploadResult.success) {
9902                    const hydraCollectionKey = 'GUID=' + currentSiteId;
9903                    enhancedGalleryOptions.manualCollectionName = hydraCollectionKey;
9904                    enhancedGalleryOptions.jsonCollections = {
9905                      [hydraCollectionKey]: {
9906                        isClassicCollection: false,
9907                        guid: hydraCollectionKey,
9908                        metadata: {},
9909                        totalItems: processedImages.length
9910                      }
9911                    };
9912                    enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId;
9913                    enhancedGalleryOptions.initialPageUuid = galleryPageId;
9914                    enhancedGalleryOptions.isHydra = true;
9915                    enhancedGalleryOptions.flyIoSaveTimestamp = Date.now();
9916                    if (uploadResult.irysResult?.txId) {
9917                      enhancedGalleryOptions.txId = uploadResult.irysResult.txId;
9918                      enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId;
9919                      enhancedGalleryOptions.isPerma = true;
9920                      enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo || ['fly'];
9921                    }
9922                    console.log('NDJSON uploaded for import_ gallery "' + title + '" (no galleryOptions) - configured as Hydra NDJSON');
9923                  }
9924                } catch (e) {
9925                  console.warn('NDJSON build/upload skipped for import_ gallery "' + title + '":', e);
9926                }
9927            }
9928            
9929            // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions
9930            // Image data is only included when importing from Classic GUID directly
9931            console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`);
9932            
9933            newItem = {
9934                id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null,
9935                siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid,
9936                importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug,
9937                galleryOptions: enhancedGalleryOptions
9938            };
9939        }
9940
9941        tempNewItems.push(newItem);
9942    }
9943
9944    // --- Pass 2: Link children to parents using the ID map ---
9945    tempNewItems.forEach(newItem => {
9946        const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id);
9947        const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId));
9948
9949        if (originalItem && originalItem.parentId) {
9950            const newParentId = originalIdToNewIdMap[originalItem.parentId];
9951            if (newParentId) {
9952                newItem.parentId = newParentId;
9953                const parentItem = tempNewItems.find(p => p.id === newParentId);
9954                if (parentItem) {
9955                    parentItem.isSubmenu = true;
9956                    parentItem.isCollapsed = true;
9957                }
9958            } else {
9959                newItem.parentId = submenuParentId;
9960            }
9961        } else {
9962            newItem.parentId = submenuParentId;
9963        }
9964    });
9965
9966    if (replaceMenu) {
9967      console.log("Replacing existing menu structure.");
9968      // If replacing, overwrite the entire galleries array.
9969      galleries = tempNewItems;
9970    } else {
9971      console.log("Appending to existing menu structure.");
9972      // If not replacing, push the new items to the existing array.
9973      galleries.push(...tempNewItems);
9974    }
9975
9976
9977    
9978    if (progressBar) progressBar.style.width = '90%';
9979    if (statusMessage) statusMessage.textContent = 'Saving changes...';
9980
9981    // Pass import flag to skip localStorage for large import operations
9982    await saveGalleries({ isImport: true });
9983
9984    if (progressBar) progressBar.style.width = '100%';
9985    if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`;
9986    if (statusEl) statusEl.classList.remove('importing');
9987
9988    setTimeout(() => {
9989        renderGalleries();
9990        toggleImportClassicForm();
9991        if (progressBar) progressBar.style.width = '0';
9992        if (statusEl) statusEl.style.display = 'none';
9993        if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) {
9994            window.location.reload();
9995        }
9996    }, 1000);
9997
9998  } catch (error) {
9999    console.error('Error importing classic galleries:', error);
10000    if (statusMessage) statusMessage.textContent = `Error: ${error.message}`;
10001    if (progressBar) {
10002      progressBar.style.width = '100%';
10003      progressBar.style.backgroundColor = '#dc3545';
10004    }
10005    if (statusEl) statusEl.classList.remove('importing');
10006  }
10007}
10008
10009
10010
10011/**
10012 * Improved toggleImportClassicForm - Closes other forms first
10013 */
10014function toggleImportClassicForm() {
10015  // First check if user is logged in and in edit mode
10016  if (!isAuthenticated || !isAdmin || !window.isInEditMode()) {
10017    alert('You must be logged in as an admin and in edit mode to import galleries.');
10018    return;
10019  }
10020
10021  const formId = 'importClassicForm';
10022  const form = document.getElementById(formId);
10023  
10024  if (!form) {
10025    console.error('Import form element not found');
10026    return;
10027  }
10028  
10029  // If this form is already open, just close everything
10030  if (window.currentOpenForm === formId) {
10031    closeAllForms();
10032    return;
10033  }
10034  
10035  // Otherwise close all forms and open this one
10036  closeAllForms();
10037  
10038  // Show the form
10039  form.style.display = 'block';
10040  
10041  // Use setTimeout to allow the display change to take effect before adding the class
10042  setTimeout(() => {
10043    form.classList.add('visible');
10044  }, 10);
10045
10046  // Populate parent options
10047  const parentSelect = document.getElementById('importParent');
10048  if (parentSelect) {
10049    parentSelect.innerHTML = '<option value="">Top Level (No Parent)</option>';
10050    
10051    // Add all galleries that could be parents (including submenus)
10052    galleries.forEach(gallery => {
10053      // Only include items that are not already marked as imported
10054      if (!gallery.importSource) {
10055        parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`;
10056      }
10057    });
10058  }
10059
10060  // Set default values
10061  const submenuTitle = document.getElementById('submenuTitle');
10062  if (submenuTitle && !submenuTitle.value) {
10063    submenuTitle.value = 'Classic Galleries';
10064  }
10065
10066  // Reset any previous import status
10067  const statusEl = document.getElementById('importStatus');
10068  if (statusEl) {
10069    statusEl.style.display = 'none';
10070    const progressBar = statusEl.querySelector('.progress-bar');
10071    if (progressBar) {
10072      progressBar.style.width = '0';
10073      progressBar.style.backgroundColor = '#4682B4'; // Reset to blue
10074    }
10075  }
10076  
10077  // Set this as the current open form
10078  window.currentOpenForm = formId;
10079}
10080
10081// Add event listener for the radio buttons to adjust height when page is selected
10082document.addEventListener('DOMContentLoaded', function() {
10083  const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]');
10084  galleryTypeRadios.forEach(radio => {
10085    radio.addEventListener('change', function() {
10086      const addForm = document.getElementById('addForm');
10087      if (addForm && addForm.classList.contains('visible')) {
10088        const isPageSelected = this.value === 'page';
10089        
10090        // Give more height for page type
10091        if (isPageSelected) {
10092          addForm.style.maxHeight = '600px';
10093        } else {
10094          // For other types, measure content height
10095          const contentHeight = addForm.scrollHeight + 30;
10096          addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px';
10097        }
10098      }
10099    });
10100  });
10101});
10102
10103
10104/**
10105 * Improved toggleStyleEditor - Closes other forms first
10106 */
10107function toggleStyleEditor(display = false, event) {
10108  const formId = 'menuStyleEditor';
10109  const overlayOpen = window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen;
10110
10111  if (overlayOpen && window.MenuStyleCustomizer._overlayMinimized && display !== false) {
10112    if (typeof window.MenuStyleCustomizer._toggleOverlayMinimized === 'function') {
10113      window.MenuStyleCustomizer._toggleOverlayMinimized(false);
10114    }
10115    return;
10116  }
10117
10118  if (display === false || window.currentOpenForm === formId || overlayOpen) {
10119    closeAllForms();
10120    return;
10121  }
10122
10123  closeAllForms();
10124
10125  if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.toggleOverlay === 'function') {
10126    window.MenuStyleCustomizer.toggleOverlay();
10127  }
10128}
10129
10130
10131/**
10132 * This function closes all forms when escape key is pressed
10133 */
10134document.addEventListener('keydown', function(e) {
10135  if (e.key === 'Escape' && window.currentOpenForm) {
10136    if (closeTopmostStyleCustomizerChildOverlay()) {
10137      return;
10138    }
10139    if (window.MenuStyleCustomizer &&
10140        window.MenuStyleCustomizer._overlayOpen &&
10141        window.MenuStyleCustomizer._overlayMinimized) {
10142      return;
10143    }
10144    closeAllForms();
10145  }
10146});
10147
10148/**
10149 * This function closes all forms when clicking outside any form
10150 */
10151document.addEventListener('click', function(e) {
10152  if (!window.currentOpenForm) return;
10153
10154  if (window.MenuStyleCustomizer &&
10155      window.MenuStyleCustomizer._overlayOpen &&
10156      window.MenuStyleCustomizer._overlayMinimized) {
10157    return;
10158  }
10159
10160  // Keep Style Customizer and its nested panels open during in-overlay interactions.
10161  if (e.target.closest(
10162    '.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'
10163  )) {
10164    return;
10165  }
10166
10167  if (e.target.closest('.image-uploader-overlay')) {
10168    return;
10169  }
10170
10171  const currentForm = document.getElementById(window.currentOpenForm);
10172  if (!currentForm) {
10173    if (e.target.closest(
10174      '.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'
10175    )) {
10176      return;
10177    }
10178    closeAllForms();
10179    return;
10180  }
10181
10182  const isInsideForm = currentForm.contains(e.target);
10183  const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null;
10184
10185  if (isInsideForm || isFormToggleButton) {
10186    return;
10187  }
10188
10189  closeAllForms();
10190});
10191
10192// Override the original functions with our improved versions
10193window.toggleAddForm = toggleAddForm;
10194window.toggleStyleEditor = toggleStyleEditor;
10195window.toggleMetadataEditor = toggleMetadataEditor;
10196window.toggleSidebarElementForm = toggleSidebarElementForm;
10197window.toggleImportClassicForm = toggleImportClassicForm;
10198window.closeAllForms = closeAllForms;
10199window.closeTopmostStyleCustomizerChildOverlay = closeTopmostStyleCustomizerChildOverlay;
10200
10201
10202/**
10203 * Opens the edit form for a gallery item.
10204 * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu)
10205 * @param {number} id - The ID of the gallery item.
10206 * @param {Event} event - The click event.
10207 */
10208function editGallery(id, event) {
10209  event.stopPropagation(); // Prevent event bubbling
10210  const gallery = findGalleryById(galleries, id);
10211  const li = getGalleryTreeListItem(id, event);
10212
10213  if (gallery && li) {
10214    const showUrlField = gallery.isPage || gallery.isIntegrated;
10215    const showExternalUrlField = gallery.isExternal;
10216    const showMenuParentOption = !gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && !gallery.isExternal &&
10217      (isPageMenuItem(gallery) || gallery.isIntegrated === true);
10218    /** Sidebar meta title/description apply to gallery routes only; pages use the page metadata element. */
10219    const showSidebarSeoFields = !gallery.isPage;
10220    const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
10221    const currentExternalUrl = gallery.url || '';
10222
10223    // Clear existing content and rebuild form to ensure event listeners are fresh if needed
10224    li.innerHTML = ''; // Clear previous form if any
10225
10226    const formDiv = document.createElement('div');
10227    formDiv.className = 'edit-form';
10228    formDiv.innerHTML = `
10229        <div class="form-group">
10230           <label>Title:</label>
10231           <input type="text" value="${gallery.title || ''}" class="edit-title">
10232        </div>
10233
10234        ${showUrlField ? `
10235        <div class="form-group">
10236           <label>URL Slug:</label>
10237           <input type="text" value="${currentSlug}" class="edit-slug" placeholder="e.g., my-gallery">
10238           <small>Path after domain name (e.g., yoursite.com<b>/my-gallery</b>). Use lowercase letters, numbers, and hyphens.</small>
10239        </div>
10240        ` : ''}
10241
10242        ${showExternalUrlField ? `
10243        <div class="form-group">
10244           <label>External URL:</label>
10245           <input type="text" value="${currentExternalUrl}" class="edit-external-url" placeholder="https://example.com">
10246           <small>Full URL for the external link (e.g., https://example.com/page).</small>
10247        </div>
10248        ` : ''}
10249
10250        ${showSidebarSeoFields ? `
10251        <div class="form-group">
10252           <label>Meta Title (Optional):</label>
10253           <input type="text" value="${gallery.metaTitle || ''}" class="edit-meta-title" placeholder="Title for search results (if different from main title)">
10254           <small>Overrides site title for this item in search results/browser tabs.</small>
10255        </div>
10256
10257        <div class="form-group">
10258           <label>Meta Description (Optional):</label>
10259           <textarea class="edit-meta-description" placeholder="Description for search results (150-160 chars recommended)">${gallery.metaDescription || ''}</textarea>
10260           <small>Overrides site description for this item in search results.</small>
10261        </div>
10262        ` : `
10263        <div class="form-group">
10264           <p class="text-muted" style="margin: 0 0 8px 0; font-size: 12px; line-height: 1.4;">
10265             Search title and description for this page are set in <strong>Page → metadata</strong> (the metadata block on the page), not here.
10266           </p>
10267        </div>
10268        `}
10269
10270        <div class="form-group">
10271          <label style="display: flex; align-items: center; gap: 8px;">
10272            <input type="checkbox" class="edit-hide-menu" ${gallery.hideMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10273            Hide menu when viewing this page
10274          </label>
10275          <small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.</small>
10276        </div>
10277
10278        <div class="form-group">
10279          <label style="display: flex; align-items: center; gap: 8px;">
10280            <input type="checkbox" class="edit-overlay-menu" ${gallery.overlayMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)">
10281            Show menu as overlay on this page
10282          </label>
10283          <small>Keeps menu items visible over a full-bleed page. Menu background is hidden. Solid menu remains in edit mode.</small>
10284        </div>
10285
10286        ${showMenuParentOption ? `
10287        <div class="form-group">
10288          <label style="display: flex; align-items: center; gap: 8px;">
10289            <input type="checkbox" class="edit-acts-as-menu-parent" ${gallery.actsAsMenuParent === true ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10290            Allow nested menu items
10291          </label>
10292          <small>Keeps this ${isPageMenuItem(gallery) ? 'page' : 'gallery'} clickable while allowing it to expand and contain sub-items.</small>
10293        </div>
10294        ` : ''}
10295
10296        <div class="form-group">
10297          <label style="display: flex; align-items: center; gap: 8px;">
10298            <input type="checkbox" class="edit-password-protected" ${gallery.passwordProtected ? 'checked' : ''} style="width: auto; margin-bottom: 0;">
10299            Password protect this page
10300          </label>
10301          <small>Visitors will need to enter a password to access this page.</small>
10302        </div>
10303
10304        <div class="form-group password-field" style="display: ${gallery.passwordProtected ? 'block' : 'none'};">
10305          <label>Password:</label>
10306          <input type="password" value="${gallery.password || ''}" class="edit-password" placeholder="Enter password">
10307          <small>Password visitors must enter to access this page.</small>
10308        </div>
10309
10310        <div class="form-group">
10311          <label style="display: flex; align-items: center; gap: 8px;">
10312            <input type="checkbox" class="edit-no-index" ${(() => {
10313              // Explicit setting takes priority;
10313 otherwise default to checked for hidden non-home pages
10314              if (gallery.noIndex === true) return 'checked';
10315              if (gallery.noIndex === false) return '';
10316              return (gallery.visible === false && !gallery.isHomePage) ? 'checked' : '';
10317            })()} style="width: auto; margin-bottom: 0;">
10318            Hide from search engines (noindex)
10319          </label>
10320          <small>When checked, search engines will not index this page. Automatically enabled for hidden pages — uncheck to allow indexing even when hidden.</small>
10321        </div>
10322
10323        <div class="edit-form-actions"> 
10324          <button class="btn btn-primary" id="saveEditBtn-${id}">Save</button> 
10325          <button class="btn" id="cancelEditBtn-${id}">Cancel</button> 
10326        </div>
10327        <br>
10328    `;
10329    li.appendChild(formDiv); // Append the new form
10330
10331    // Add event listeners programmatically
10332    const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`);
10333    if (saveButton) {
10334        saveButton.addEventListener('click', (e) => saveEdit(id, e));
10335    }
10336
10337    const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`);
10338    if (cancelButton) {
10339        cancelButton.addEventListener('click', () => renderGalleries());
10340    }
10341
10342    // Add password protection toggle functionality
10343    const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected');
10344    const passwordField = formDiv.querySelector('.password-field');
10345    if (passwordProtectedCheckbox && passwordField) {
10346        passwordProtectedCheckbox.addEventListener('change', function() {
10347            passwordField.style.display = this.checked ? 'block' : 'none';
10348        });
10349    }
10350
10351    li.classList.add('edit-mode');
10352  } else {
10353    console.error("Could not find gallery or list item for ID:", id);
10354  }
10355}
10356
10357/**
10358 * Resolve the correct sidebar #galleryTree li for an id. Horizontal dropdowns also use
10359 * li[data-id] (.horizontal-submenu-item); document.querySelector would return that node first
10360 * and break nested folder toggles in edit mode while horizontal layout is on.
10361 * @param {number} id - Gallery ID
10362 * @param {Event|null} event - Optional click event (preferred: closest li inside #galleryTree)
10363 * @returns {HTMLElement|null}
10364 */
10365function getGalleryTreeListItem(id, event) {
10366  var numericId = typeof id === 'number' ? id : parseInt(String(id), 10);
10367  if (isNaN(numericId)) return null;
10368  var galleryTreeEl = document.getElementById('galleryTree');
10369  if (event && event.target && galleryTreeEl) {
10370    var clicked = event.target.closest('li[data-id]');
10371    if (clicked && galleryTreeEl.contains(clicked)) {
10372      var cid = parseInt(clicked.getAttribute('data-id') || '', 10);
10373      if (cid === numericId) return clicked;
10374    }
10375  }
10376  if (galleryTreeEl) {
10377    var scoped = galleryTreeEl.querySelector('li[data-id="' + numericId + '"]');
10378    if (scoped) return scoped;
10379  }
10380  return document.querySelector('li[data-id="' + numericId + '"]');
10381}
10382
10383/**
10384 * Handles click on a folder item to show/hide its contents
10385 * @param {number} id - The gallery ID
10386 * @param {Event} event - The click event
10387 */
10388function toggleFolder(id, event) {
10389  // Ensure the event doesn't interfere with drag operations
10390  if (event) {
10391    event.stopPropagation();
10392  }
10393  
10394  
10395  const gallery = galleries.find(g => g.id === id);
10396  if (!gallery) {
10397    return;
10398  }
10399
10400  if (gallery.isFolder !== true && gallery.isSubmenu !== true && !isNavigableMenuParent(gallery)) {
10401    return;
10402  }
10403  
10404  
10405  // Toggle the collapsed state
10406  gallery.isCollapsed = !gallery.isCollapsed;
10407  
10408  // Find the list item in the DOM (#galleryTree first — not horizontal li.horizontal-submenu-item)
10409  const listItem = getGalleryTreeListItem(id, event);
10410  if (!listItem) {
10411    return;
10412  }
10413  
10414  
10415  // Toggle the expanded class FIRST - this is important for CSS to work correctly
10416  listItem.classList.toggle('expanded', !gallery.isCollapsed);
10417
10418  // Keep ARIA in sync for folder label + chevron (screen readers / keyboard)
10419  const folderExpanded = !gallery.isCollapsed;
10420  listItem.querySelectorAll('.menu-item.folder, .submenu-toggle').forEach((el) => {
10421    el.setAttribute('aria-expanded', folderExpanded ? 'true' : 'false');
10422  });
10423  
10424  // Force a reflow after toggling the class to ensure CSS recalculates
10425  void listItem.offsetHeight;
10426  
10427    // Find the nested list - SIMPLIFIED: Check if we're in sidebar first, then horizontal
10428    // CRITICAL: Check #galleryTree first - this is the definitive indicator of sidebar layout
10429    // Use contains() instead of closest() because closest() only checks ancestors
10430    const galleryTree = document.getElementById('galleryTree');
10431    const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10432    const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10433    // Only consider it horizontal menu if it's explicitly in .horizontal-menu
10433-container AND not in galleryTree
10434    const isInHorizontalMenu = listItem.closest('.horizontal-menu-container') && !isInGalleryTree && !isInSidebar;
10435    
10436    let nestedList = null;
10437    
10438    // If we're in sidebar (#galleryTree), ONLY look for ol.nested-sortable - ignore horizontal structures
10439    // CRITICAL: Prioritize sidebar detection - if we're in galleryTree, we're definitely in sidebar
10440    if (isInGalleryTree) {
10441      nestedList = listItem.querySelector('ol.nested-sortable');
10442      
10443      // Also check direct children as fallback
10444      if (!nestedList) {
10445        const directChildren = Array.from(listItem.children);
10446        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10447      }
10448    } else if (isInSidebar) {
10449      // Fallback: if we're in .sidebar but not in #galleryTree, still treat as sidebar
10450      nestedList = listItem.querySelector('ol.nested-sortable');
10451      if (!nestedList) {
10452        const directChildren = Array.from(listItem.children);
10453        nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable'));
10454      }
10455    }
10456    
10457    // For horizontal layout, check horizontal structures (ONLY if NOT in sidebar/galleryTree)
10458    if (!nestedList && isInHorizontalMenu && !isInGalleryTree && !isInSidebar) {
10459      nestedList = listItem.querySelector('.horizontal-submenu');
10460      if (!nestedList) {
10461        nestedList = listItem.querySelector('.horizontal-dropdown');
10462      }
10463      if (!nestedList) {
10464        // For horizontal nested dropdowns, they're appended to document.body, not the list item
10465        // They're linked via data-flyout-id attribute
10466        const flyoutId = listItem.getAttribute('data-flyout-id');
10467        if (flyoutId) {
10468          // Look specifically for div.horizontal-nested-dropdown, not the list item itself
10469          const allElementsWithId = document.querySelectorAll('[data-flyout-id="' + flyoutId + '"]');
10470          
10471          // Find the actual dropdown div, not the list item
10472          const flyout = Array.from(allElementsWithId).find(el => 
10473            el.classList.contains('horizontal-nested-dropdown') && el !== listItem
10474          );
10475          
10476          
10477          if (flyout) {
10478            nestedList = flyout.querySelector('.horizontal-nested-list') || flyout;
10479          }
10480        }
10481      }
10482      if (!nestedList) {
10483        nestedList = listItem.querySelector('.horizontal-nested-dropdown');
10484      }
10485    }
10486    
10487    // Fallback: try all selectors if not found yet
10488    if (!nestedList) {
10489      nestedList = listItem.querySelector('ol.nested-sortable');
10490    }
10491    if (!nestedList) {
10492      // Debug: Log the actual DOM structure to understand what's there
10493      
10494      // Try to find any nested structure at all
10495      const anyNested = listItem.querySelector('ol, ul, div[class*="submenu"], div[class*="nested"], div[class*="dropdown"]');
10496      if (anyNested) {
10497        nestedList = anyNested; // Use it anyway
10498      }
10499      
10500      // Last resort: Check if nested list exists as a direct child but wasn't found by querySelector
10501      // This can happen if the element was removed/re-added or if there's a timing issue
10502      const directChildren = Array.from(listItem.children);
10503      const olChild = directChildren.find(child => child.tagName === 'OL');
10504      if (olChild) {
10505        nestedList = olChild;
10506      }
10507    }
10508    
10509    if (!nestedList) {
10510      
10511      // If the nested list doesn't exist, we need to create it
10512      // This can happen if children aren't rendered until the folder is opened
10513      // First try flat array
10514      let gallery = galleries.find(g => g.id === id);
10515      
10516      // If gallery not found or has no children in flat array, build tree structure to find it
10517      if (!gallery || !gallery.children || gallery.children.length === 0) {
10518        
10519        // Build tree structure from flat array
10520        const buildTree = (items, parentId = null) => {
10521          return items
10522            .filter(item => item.parentId === parentId)
10523            .map(item => ({
10524              ...item,
10525              children: buildTree(items, item.id)
10526            }));
10527        };
10528        
10529        const galleryTree = buildTree(galleries);
10530        
10531        // Recursive function to find gallery in tree
10532        const findInTree = (items) => {
10533          for (const item of items) {
10534            if (item.id === id) {
10535              return item;
10536            }
10537            if (item.children && item.children.length > 0) {
10538              const found = findInTree(item.children);
10539              if (found) return found;
10540            }
10541          }
10542          return null;
10543        };
10544        
10545        const treeGallery = findInTree(galleryTree);
10546        if (treeGallery) {
10547          gallery = treeGallery;
10548        }
10549      }
10550      
10551        if (gallery && gallery.children && gallery.children.length > 0) {
10552          
10553          // Determine nesting level from list item
10554          const currentLevel = parseInt(listItem.getAttribute('data-nesting-level') || '0', 10);
10555          const nextLevel = currentLevel + 1;
10556          
10557          // Create the nested list element
10558          nestedList = document.createElement('ol');
10559          nestedList.id = 'gallery-children-' + id;
10560          nestedList.className = 'nested-sortable';
10561          if (nextLevel > 0) {
10562            nestedList.classList.add('nested-level-' + nextLevel);
10563          }
10564          
10565          // Render the children - ensure we're in the right context (not in a horizontal dropdown wrapper)
10566          gallery.children.forEach(child => {
10567            const childHtml = renderGalleryItem(child, nextLevel, 20);
10568            const tempDiv = document.createElement('div');
10569            tempDiv.innerHTML = childHtml;
10570            const childLi = tempDiv.querySelector('li');
10571            if (childLi) {
10572              nestedList.appendChild(childLi);
10573            }
10574          });
10575          
10576          // CRITICAL: Only check for horizontal dropdowns if we're actually in a horizontal menu context
10577          // If we're in #galleryTree (sidebar), skip all horizontal menu logic
10578          const galleryTree = document.getElementById('galleryTree');
10579          const isInGalleryTree = galleryTree && galleryTree.contains(listItem);
10580          const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree;
10581          
10582          if (!isInGalleryTree && !isInSidebar) {
10583            const isInHorizontalMenu = listItem.closest('.horizontal-menu-container');
10584            if (isInHorizontalMenu) {
10585              const horizontalDropdown = listItem.closest('.horizontal-dropdown, .horizontal-nested-dropdown');
10586              if (horizontalDropdown && horizontalDropdown !== listItem) {
10587                horizontalDropdown.style.setProperty('display', 'block', 'important');
10588                horizontalDropdown.style.setProperty('visibility', 'visible', 'important');
10589                horizontalDropdown.style.setProperty('opacity', '1', 'important');
10590              }
10591            }
10592          }
10593          
10594          // CRITICAL: Ensure list item itself is visible and has correct display
10595          listItem.style.setProperty('display', 'list-item', 'important');
10596          listItem.style.setProperty('visibility', 'visible', 'important');
10597          
10598          // CRITICAL: Ensure ALL parent elements up to the sidebar are visible
10599          // BUT: Don't make .horizontal-menu-container visible - that's the entire horizontal menu!
10600          let parent = listItem.parentElement;
10601          let depth = 0;
10602          while (parent && depth < 10) {
10603            // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10604            if (parent.classList.contains('horizontal-menu-container')) {
10605              parent = parent.parentElement;
10606              depth++;
10607              continue;
10608            }
10609            
10610            const parentStyles = window.getComputedStyle(parent);
10611            if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10612              if (parentStyles.display === 'none') {
10613                parent.style.setProperty('display', 'block', 'important');
10614              }
10615              if (parentStyles.visibility === 'hidden') {
10616                parent.style.setProperty('visibility', 'visible', 'important');
10617              }
10618              if (parentStyles.opacity === '0') {
10619                parent.style.setProperty('opacity', '1', 'important');
10620              }
10621            }
10622            // Stop at sidebar or galleryTree
10623            if (parent.classList.contains('sidebar') || parent.id === 'galleryTree' || parent.id === 'sidebar') {
10624              break;
10625            }
10626            parent = parent.parentElement;
10627            depth++;
10628          }
10629          
10630          // Append directly to the list item (not to any wrapper)
10631          // Make sure we're not appending to a hidden parent
10632          listItem.appendChild(nestedList);
10633          
10634          // Immediately set the nested list to be visible since we're opening it
10635          nestedList.style.setProperty('display', 'block', 'important');
10636          nestedList.style.setProperty('visibility', 'visible', 'important');
10637          nestedList.style.setProperty('opacity', '1', 'important');
10638          nestedList.style.setProperty('height', 'auto', 'important');
10639          nestedList.style.setProperty('max-height', 'none', 'important');
10640          
10641          // Force immediate reflow after appending
10642          void nestedList.offsetHeight;
10643          void listItem.offsetHeight;
10644        }
10645    }
10646    
10647    if (nestedList) {
10648    if (!gallery.isCollapsed) {
10649      // Show the nested list - ensure parent has expanded class FIRST
10650      // CRITICAL: Add expanded class BEFORE any style operations
10651      listItem.classList.add('expanded');
10652      
10653      // Verify expanded class was added
10654      if (!listItem.classList.contains('expanded')) {
10655        listItem.className = listItem.className + ' expanded';
10656      }
10657      
10658      // CRITICAL: Ensure parent li is visible (it might be hidden by CSS)
10659      const parentVisibility = window.getComputedStyle(listItem).visibility;
10660      if (parentVisibility === 'hidden') {
10661        listItem.style.setProperty('visibility', 'visible', 'important');
10662      }
10663      
10664      // Force a reflow to ensure CSS recalculates after adding expanded class
10665      void listItem.offsetHeight;
10666      void nestedList.offsetHeight;
10667      
10668      // Show the nested list with !important to override CSS
10669      // Set these BEFORE removing any existing inline styles to avoid flicker
10670      nestedList.style.setProperty('display', 'block', 'important');
10671      nestedList.style.setProperty('visibility', 'visible', 'important');
10672      nestedList.style.setProperty('opacity', '1', 'important');
10673      nestedList.style.setProperty('height', 'auto', 'important');
10674      nestedList.style.setProperty('max-height', 'none', 'important');
10675      nestedList.style.setProperty('overflow', 'visible', 'important');
10676      
10677      // Also ensure the nested list is not clipped by parent overflow
10678      // BUT: Don't modify .horizontal-menu-container overflow - that's the entire horizontal menu!
10679      let checkParent = nestedList.parentElement;
10680      let checkDepth = 0;
10681      while (checkParent && checkDepth < 5) {
10682        // Skip .horizontal-menu-container - we don't want to modify the entire horizontal menu
10683        if (checkParent.classList.contains('horizontal-menu-container')) {
10684          checkParent = checkParent.parentElement;
10685          checkDepth++;
10686          continue;
10687        }
10688        
10689        const parentOverflow = window.getComputedStyle(checkParent).overflow;
10690        if (parentOverflow === 'hidden') {
10691          checkParent.style.setProperty('overflow', 'visible', 'important');
10692        }
10693        if (checkParent.classList.contains('sidebar') || checkParent.id === 'galleryTree') {
10694          break;
10695        }
10696        checkParent = checkParent.parentElement;
10697        checkDepth++;
10698      }
10699      
10700      // Force another reflow after setting styles
10701      void nestedList.offsetHeight;
10702      
10703      const finalRect = nestedList.getBoundingClientRect();
10704      
10705      // If the nested list has zero dimensions, it's not actually visible
10706      if (finalRect.width === 0 || finalRect.height === 0) {
10707        // Force all parent elements to be visible
10708        // BUT: Don't modify .horizontal-menu-container - that's the entire horizontal menu!
10709        let forceParent = nestedList.parentElement;
10710        while (forceParent && forceParent !== document.body) {
10711          // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu
10712          if (forceParent.classList.contains('horizontal-menu-container')) {
10713            forceParent = forceParent.parentElement;
10714            continue;
10715          }
10716          
10717          forceParent.style.setProperty('display', 'block', 'important');
10718          forceParent.style.setProperty('visibility', 'visible', 'important');
10719          forceParent.style.setProperty('opacity', '1', 'important');
10720          forceParent.style.setProperty('overflow', 'visible', 'important');
10721          if (forceParent.classList.contains('sidebar') || forceParent.id === 'galleryTree') {
10722            break;
10723          }
10724          forceParent = forceParent.parentElement;
10725        }
10726        void nestedList.offsetHeight;
10727      }
10728      
10729      // Check for any parent elements that might be hiding the nested list and fix them
10730      let parent = nestedList.parentElement;
10731      let depth = 0;
10732      while (parent && depth < 5) {
10733        const parentStyles = window.getComputedStyle(parent);
10734        if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') {
10735          
10736          // Fix hidden parent elements
10737          if (parentStyles.display === 'none') {
10738            parent.style.setProperty('display', 'block', 'important');
10739          }
10740          if (parentStyles.visibility === 'hidden') {
10741            parent.style.setProperty('visibility', 'visible', 'important');
10742          }
10743          if (parentStyles.opacity === '0') {
10744            parent.style.setProperty('opacity', '1', 'important');
10745          }
10746        }
10747        parent = parent.parentElement;
10748        depth++;
10749      }
10750      
10751    } else {
10752      // Hide the nested list
10753      nestedList.style.setProperty('display', 'none', 'important');
10754      nestedList.style.setProperty('visibility', 'hidden', 'important');
10755      nestedList.style.setProperty('opacity', '0', 'important');
10756    }
10757    
10758    // Force multiple reflows to ensure the browser applies all changes
10759    void listItem.offsetHeight;
10760    void nestedList.offsetHeight;
10761    void listItem.offsetWidth;
10762    void nestedList.offsetWidth;
10763    
10764    // Force a more aggressive repaint by temporarily toggling a class
10765    // This is especially important for sidebar layout where CSS rules might be interfer
10765ing
10766    listItem.classList.add('force-reflow');
10767    void listItem.offsetHeight;
10768    listItem.classList.remove('force-reflow');
10769    
10770    // Force reflow again after class manipulation
10771    void listItem.offsetHeight;
10772    void nestedList.offsetHeight;
10773    
10774    // Use requestAnimationFrame with multiple frames to ensure visual update
10775    // This ensures the browser has time to recalculate layout and repaint
10776    requestAnimationFrame(() => {
10777      void nestedList.offsetHeight;
10778      // Second frame to ensure all CSS has been recalculated
10779      requestAnimationFrame(() => {
10780        void nestedList.offsetHeight;
10781        // Verify the final state
10782        const finalDisplay = window.getComputedStyle(nestedList).display;
10783        const finalVisibility = window.getComputedStyle(nestedList).visibility;
10784        
10785        // If still not visible, force one more repaint cycle
10786        if (!gallery.isCollapsed && finalDisplay === 'none') {
10787          nestedList.style.setProperty('display', 'block', 'important');
10788          nestedList.style.setProperty('visibility', 'visible', 'important');
10789          void nestedList.offsetHeight;
10790          requestAnimationFrame(() => {
10791            void nestedList.offsetHeight;
10792          });
10793        }
10794      });
10795    });
10796    }
10797  
10798  // Find the submenu toggle icon and rotate it
10799  const toggleIcon = listItem.querySelector('.toggle-icon');
10800  if (toggleIcon) {
10801    toggleIcon.classList.toggle('rotated', !gallery.isCollapsed);
10802  }
10803  
10804  
10805  // Folder open/closed is a local UI preference — do not POST the full site config.
10806  try {
10807    const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}');
10808    folderStates[id] = !gallery.isCollapsed;
10809    localStorage.setItem('folder_states', JSON.stringify(folderStates));
10810  } catch (e) {
10811    console.error('Error saving folder state to localStorage:', e);
10812  }
10813}
10814
10815window.toggleFolder = toggleFolder;
10816
10817/**
10818 * Toggle password protection for a gallery/page
10819 * @param {number} id - The gallery ID
10820 * @param {Event} event - The click event
10821 */
10822function toggleGalleryPasswordProtection(id, event) {
10823  event.stopPropagation();
10824  
10825  const gallery = findGalleryById(galleries, id);
10826  if (!gallery) {
10827    console.error("Could not find gallery for password protection toggle, ID:", id);
10828    return;
10829  }
10830
10831  if (gallery.passwordProtected) {
10832    // Remove password protection
10833    if (confirm('Remove password protection from this page?')) {
10834      gallery.passwordProtected = false;
10835      delete gallery.password;
10836      saveGalleries();
10837      renderGalleries();
10838    }
10839  } else {
10840    // Add password protection
10841    const password = prompt('Enter a password for this page:');
10842    if (password && password.trim()) {
10843      gallery.passwordProtected = true;
10844      gallery.password = password.trim();
10845      saveGalleries();
10846      renderGalleries();
10847    }
10848  }
10849}
10850
10851window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection;
10852
10853function onPageMenuChromeToggle(checkbox) {
10854  if (!checkbox || !checkbox.checked) return;
10855  const root = checkbox.closest ? checkbox.closest('li') : null;
10856  if (!root) return;
10857  const siblingClass = checkbox.classList.contains('edit-hide-menu') ? '.edit-overlay-menu' : '.edit-hide-menu';
10858  const sibling = root.querySelector(siblingClass);
10859  if (sibling && sibling !== checkbox) sibling.checked = false;
10860}
10861window.onPageMenuChromeToggle = onPageMenuChromeToggle;
10862
10863/**
10864 * Saves the edited gallery item details.
10865 * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic)
10866 * @param {number} id - The ID of the gallery item.
10867 * @param {Event} event - The click event.
10868 */
10869function saveEdit(id, event) {
10870  event.stopPropagation();
10871  const li = getGalleryTreeListItem(id, event);
10872  if (!li) {
10873    console.error("Could not find list item for saveEdit, ID:", id);
10874    return;
10875  }
10876
10877  const titleInput = li.querySelector('.edit-title');
10878  const slugInput = li.querySelector('.edit-slug');
10879  const externalUrlInput = li.querySelector('.edit-external-url');
10880  const metaTitleInput = li.querySelector('.edit-meta-title');
10881  const metaDescriptionInput = li.querySelector('.edit-meta-description');
10882  const hideMenuInput = li.querySelector('.edit-hide-menu');
10883  const overlayMenuInput = li.querySelector('.edit-overlay-menu');
10884  const actsAsMenuParentInput = li.querySelector('.edit-acts-as-menu-parent');
10885  const passwordProtectedInput = li.querySelector('.edit-password-protected');
10886  const passwordInput = li.querySelector('.edit-password');
10887  const noIndexInput = li.querySelector('.edit-no-index');
10888
10889  const title = titleInput ? titleInput.value.trim() : '';
10890  const newSlugRaw = slugInput ? slugInput.value.trim() : null;
10891  const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : '';
10892  const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : '';
10893  const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : '';
10894  const hideMenu = hideMenuInput ? hideMenuInput.checked : false;
10895  const overlayMenu = overlayMenuInput ? overlayMenuInput.checked : false;
10896  const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false;
10897  const password = passwordInput ? passwordInput.value.trim() : '';
10898  // noIndex is stored explicitly so it survives visibility changes without reverting
10899  const noIndex = noIndexInput ? noIndexInput.checked : undefined;
10900
10901  const gallery = findGalleryById(galleries, id);
10902  if (!gallery) {
10903    console.error("Could not find gallery object for saveEdit, ID:", id);
10904    renderGalleries();
10905    return;
10906  }
10907
10908  if (!title && !gallery.isSpacer) {
10909    alert('Please enter a title');
10910    return;
10911  }
10912
10913  // Validate password protection
10914  if (passwordProtected && !password) {
10915    alert('Please enter a password when password protection is enabled');
10916    return;
10917  }
10918
10919  // Only mutate the opt-in property when the user explicitly changes this checkbox.
10920  // This preserves the serialized shape and behavior of every existing item.
10921  if (actsAsMenuParentInput) {
10922    const wasNavigableMenuParent = gallery.actsAsMenuParent === true;
10923    const wantsNavigableMenuParent = actsAsMenuParentInput.checked === true;
10924    if (wasNavigableMenuParent && !wantsNavigableMenuParent) {
10925      const hasNestedItems = galleries.some(item => item.parentId === gallery.id);
10926      if (hasNestedItems) {
10927        alert('Move all nested menu items out of this item before turning off “Allow nested menu items”.');
10928        return;
10929      }
10930      delete gallery.actsAsMenuParent;
10931    } else if (!wasNavigableMenuParent && wantsNavigableMenuParent) {
10932      gallery.actsAsMenuParent = true;
10933    }
10934  }
10935
10936  gallery.title = title;
10937  // Sidebar SEO fields are only shown for galleries; do not overwrite stored values when saving a page.
10938  if (!gallery.isPage) {
10939    gallery.metaTitle = metaTitle;
10940    gallery.metaDescription = metaDescription;
10941  }
10942  // Hide-menu wins if both are somehow checked; overlay is opt-in and omitted when off.
10943  if (hideMenu) {
10944    gallery.hideMenuOnPage = true;
10945    delete gallery.overlayMenuOnPage;
10946  } else {
10947    gallery.hideMenuOnPage = hideMenu;
10948    if (overlayMenu) {
10949      gallery.overlayMenuOnPage = true;
10950    } else {
10951      delete gallery.overlayMenuOnPage;
10952    }
10953  }
10954  // Persist the explicit noIndex choice so it survives future visibility toggles
10955  if (noIndex !== undefined) gallery.noIndex = noIndex;
10956
10957  // Save password protection settings
10958  gallery.passwordProtected = passwordProtected;
10959  if (passwordProtected) {
10960    gallery.password = password;
10961  } else {
10962    delete gallery.password; // Remove password if protection is disabled
10963  }
10964
10965  if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) {
10966    const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : '');
10967    let sanitizedSlug = slugify(newSlugRaw);
10968
10969    if (!sanitizedSlug) {
10970      sanitizedSlug = slugify(gallery.title);
10971    }
10972
10973    if (sanitizedSlug !== originalSlug) {
10974      const otherGalleries = galleries.filter(g => g.id !== id);
10975      gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries);
10976      gallery.url = `/${gallery.slug}`;
10977    } else {
10978      if (gallery.url !== `/${gallery.slug}`) {
10979        gallery.url = `/${gallery.slug}`;
10980      }
10981    }
10982  }
10983
10984  // Update external URL if this is an external link
10985  if (gallery.isExternal && externalUrl) {
10986    gallery.url = externalUrl;
10987  }
10988
10989  saveGalleries();
10990  renderGalleries();
10991
10992  // If the currently active gallery was just edited, re-apply menu visibility
10993    if (gallery.id === activeGalleryId) {
10994    const bodyEl = document.body;
10995    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
10996        bodyEl.classList.add('menu-hidden-on-page');
10997    } else {
10998        bodyEl.classList.remove('menu-hidden-on-page');
10999    }
11000    applyMenuOverlayClassForGallery(gallery);
11001  }
11002}
11003// Helper function to check if a gallery is an ancestor of another
11004function hasAncestor(galleryId, ancestorId) {
11005if (galleryId === ancestorId) return true;
11006
11007const gallery = galleries.find(g => g.id === galleryId);
11008if (!gallery || !gallery.parentId) return false;
11009
11010if (gallery.parentId === ancestorId) return true;
11011
11012return hasAncestor(gallery.parentId, ancestorId);
11013}
11014
11015function deleteGallery(id, event) {
11016  event.stopPropagation();
11017  const gallery = galleries.find(g => g.id === id);
11018  const title = gallery && (gallery.title || gallery.slug || 'this item') || 'this item';
11019  if (!confirm('Are you sure you want to delete "' + title + '"?')) {
11020    return;
11021  }
11022  const overlay = showDeletionOverlay();
11023  overlay.updateMessage('Saving...');
11024  galleries = galleries.filter(g => g.id !== id);
11025  if (typeof window !== 'undefined') {
11026    window.galleries = galleries;
11027  }
11028  if (activeGalleryId === id) {
11029    activeGalleryId = null;
11030    const frame = document.getElementById('galleryFrame');
11031    if (frame) frame.src = '';
11032  }
11033  saveGalleries()
11034    .then(function(success) {
11035      renderGalleries();
11036      overlay.hide();
11037      if (success === false) {
11038        alert('Failed to delete. Please try again.');
11039      }
11040    })
11041    .catch(function(err) {
11042      overlay.hide();
11043      console.error('Error deleting item:', err);
11044      alert('Failed to delete. Please try again.');
11045    });
11046}
11047
11048function debugNestedStructure() {
11049console.group('Current Gallery Structure');
11050
11051function printGallery(gallery, level = 0) {
11052const indent = '  '.repeat(level);
11053console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`);
11054
11055if (gallery.children && gallery.children.length > 0) {
11056  gallery.children.forEach(child => printGallery(child, level + 1));
11057}
11058}
11059  // Create a tree structure for debugging
11060const galleryTree = createGalleryTree(galleries);
11061galleryTree.forEach(gallery => printGallery(gallery));
11062
11063console.groupEnd();
11064}
11065
11066function toggleHome(id, event) {
11067  event.stopPropagation();
11068  
11069  console.log('Setting home page to gallery:', id);
11070  
11071  // Find gallery
11072  const gallery = galleries.find(g => g.id === id);
11073  if (!gallery) return;
11074  
11075  // Check if this gallery is already set as home
11076  const alreadyHome = gallery.isHomePage === true;
11077  
11078  // Set all galleries to not be the home page
11079  galleries.forEach(g => {
11080    g.isHomePage = false;
11081  });
11082  
11083  // Toggle this gallery's home status - if it was already home, we're unsetting it
11084  gallery.isHomePage = !alreadyHome;
11085  
11086  // Save changes
11087  saveGalleries();
11088  
11089  // Update UI
11090  renderGalleries();
11091  
11092  // If this gallery is now the home page, display a message
11093  if (gallery.isHomePage) {
11094    const title = gallery.title || 'This page';
11095    showSuccessMessage(`${title} is now set as the Home page`);
11096  }
11097}
11098
11099// Function to toggle gallery visibility
11100function toggleVisibility(id, event) {
11101  event.stopPropagation();
11102  
11103  const gallery = galleries.find(g => g.id === id);
11104  if (gallery) {
11105    // Toggle the visibility
11106    gallery.visible = gallery.visible === false ? true : false;
11107    
11108    // Save the change to server
11109    saveGalleries();
11110    
11111    // Update UI - important to toggle the class on the button itself
11112    const toggle = event.currentTarget;
11113    if (toggle) {
11114      if (gallery.visible === false) {
11115        toggle.classList.add('hidden');
11116        
11117        // Update the SVG icon
11118        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>';
11119      } else {
11120        toggle.classList.remove('hidden');
11121        
11122        // Update the SVG icon
11123        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>';
11124      }
11125    }
11126    
11127    // Update parent row class (#galleryTree row, not horizontal submenu li)
11128    const listItem = getGalleryTreeListItem(id, event);
11129    if (listItem) {
11130      if (gallery.visible === false) {
11131        listItem.classList.add('hidden-gallery');
11132      } else {
11133        listItem.classList.remove('hidden-gallery');
11134      }
11135    }
11136    
11137    // Only clear iframe if not in edit mode
11138    if (id === activeGalleryId && gallery.visible === false && !isEditing) {
11139      document.getElementById('galleryFrame').src = '';
11140    }
11141    
11142    // Re-render menus based on current layout
11143    const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 
11144                         document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar';
11145    
11146    if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') {
11147      window.renderHorizontalMenu();
11148    }
11149    
11150    // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied
11151    // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode
11152    if (typeof window.renderGalleries === 'function') {
11153      window.renderGalleries();
11154    } else if (typeof renderGalleries === 'function') {
11155      renderGalleries();
11156    }
11157  }
11158}
11159
11160
11161
11162/**
11163* Simplified loadGallery function that follows the same content swap pattern
11164* @param {number} id - The gallery ID
11165* @param {Event} event - Optional event object
11166*/
11167// Enhanced loadGallery function with script cleanup
11168async function loadGallery(id, event) {
11169
11170// Stop event propagation if provided; preventDefault keeps <a href> SPA navigation
11171if (event) {
11172  if (typeof event.preventDefault === 'function') event.preventDefault();
11173  event.stopPropagation();
11174}
11175
11176const galleryContainer = document.querySelector('.gallery-container');
11177if (galleryContainer) {
11178  // Apply immediate hiding styles
11179  galleryContainer.style.opacity = '0';
11180}
11181
11182console.log('Loading gallery with ID:', id);
11183
11184if (typeof dismissGalleryPasswordOverlay === 'function') {
11185  dismissGalleryPasswordOverlay();
11186}
11187
11188// Find gallery by ID
11189const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
11190
11191if (!gallery) {
11192  console.warn('No gallery found with ID:', id);
11193  if (galleryContainer) {
11194  galleryContainer.style.opacity = '1'; 
11195  } 
11196  return;
11197}
11198
11199// Check if this is a page, if so use loadPage instead
11200if (isPageMenuItem(gallery)) {
11201  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
11202  return loadPage(id, event);
11203}
11204
11205// Skip submenu items
11206if (gallery.isSubmenu) {
11207  console.log('Gallery is submenu, skipping URL update');
11208  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
11209  if (galleryContainer) {
11210  galleryContainer.style.opacity = '1';
11211  } // Show container
11212  return;
11213}
11214
11215console.log('Found gallery:', gallery.title);
11216
11217window._activeContentType = 'gallery';
11218beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
11219if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
11220  window.PageManager.clearCurrentPage();
11221}
11222
11223// CRITICAL: Set active gallery ID consistently in both global and window scope
11224console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
11225window.activeGalleryId = id;
11226activeGalleryId = id; // Ensure both variables are synchronized
11227
11228// Save reference to which gallery we're loading (for later verification)
11229window.currentLoadingGalleryId = id;
11230
11231// CRITICAL: Thorough cleanup with await to ensure completion
11232if (typeof window.removeGalleryScriptsWithPause === 'function') {
11233  await window.removeGalleryScriptsWithPause();
11234}
11235
11236
11237// Clear all existing content from gallery container
11238if (!galleryContainer) {
11239  console.error('Gallery container not found');
11240  return;
11241}
11242
11243// Force clear all content
11244galleryContainer.innerHTML = '';
11245
11246
11247  // CRITICAL: Double-check active gallery ID before updating UI
11248  if (window.activeGalleryId !== id || activeGalleryId !== id) {
11249    console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
11250    window.activeGalleryId = id;
11251    activeGalleryId = id;
11252  }
11253
11254// Apply menu visibility based on the gallery's setting and edit mode
11255const bodyEl = document.body;
11256if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
11257    bodyEl.classList.add('menu-hidden-on-page');
11258} else {
11259    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
11260}
11261applyMenuOverlayClassForGallery(gallery);
11262
11263// Update UI state using explicit window function references or local fallbacks
11264if (typeof window.updateActiveStates === 'function') {
11265  window.updateActiveStates();
11266} else if (typeof updateActiveStates === 'function') {
11267  updateActiveStates();
11268}
11269
11270if (typeof window.updateMobileTitle === 'function') {
11271  window.updateMobileTitle();
11272} else if (typeof updateMobileTitle === 'function') {
11273  updateMobileTitle();
11274}
11275
11276if (typeof window.closeMobileMenu === 'function') {
11277  window.closeMobileMenu();
11278} else if (typeof closeMobileMenu === 'function') {
11279  closeMobileMenu();
11280}
11281
11282// Update URL with gallery slug
11283if (typeof window.updateURLWithGallerySlug === 'function') {
11284  window.updateURLWithGallerySlug(gallery);
11285} else if (typeof updateURLWithGallerySlug === 'function') {
11286  updateURLWithGallerySlug(gallery);
11287}
11288
11289  // Create gallery container and load content
11290const neonGalleryContainer = document.createElement('div');
11291neonGalleryContainer.id = 'neon-gallery-container';
11292neonGalleryContainer.className = 'gallery-direct-content';
11293neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
11294neonGalleryContainer.style.width = '100%';
11295neonGalleryContainer.style.height = '100%';
11296galleryContainer.appendChild(neonGalleryContainer);
11297
11298// Add a cache-busting parameter to ensure script is freshly loaded
11299const timestamp = Date.now();
11300
11301// Set up gallery parameters
11302window.Parameters = {
11303  SiteAlias: window.location.hostname,
11304  InitialPageUuid: gallery.pageId,
11305  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
11306  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
11307  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11308  isHydra: true,
11309  galleryInstanceId: timestamp,
11310  // Added loadedGalleryId as per your original function
11311  loadedGalleryId: gallery.id
11312};
11313
11314// Debug: Check what galleryOptions contains
11315console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
11316
11317// Set up gallery config
11318window.neonGalleryConfig = {
11319  useData: true,
11320  useCDN: true,
11321  version: 'live',
11322  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
11323  layoutType: gallery.galleryOptions?.layoutType || "grid",
11324  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
11325  // This ensures that only properties from the current gallery are included, not from previous galleries
11326  ...(gallery.galleryOptions || {}),
11327  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
11328  // These fields are required for the gallery to determine if it should use Load Network racing
11329  loadTxId: gallery.galleryOptions?.loadTxId || null,
11330  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
11331  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
11332  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
11333  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
11334  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
11335  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
11336  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
11337  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
11338  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
11339  galleryId: gallery.id,
11340  galleryInstanceId: timestamp
11341};
11342
11343console.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);
11344
11345// Create and add the gallery script with cache-busting
11346// Use Worker route (avoids CDN SSL issues)
11347const script = document.createElement('script');
11348const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
11349script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
11350script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
11351script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
11352console.log('Loading gallery-main script from Worker:', script.src);
11353script.onerror = function() {
11354  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
11355  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
11356  script.src = proxyUrl;
11357  script.onerror = function() {
11358    console.error('❌ Proxy failed, trying storage:', proxyUrl);
11359    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
11360  };
11361};
11362
11363// Added script.onload from your original function
11364script.onload = function() {
11365  if (window.currentLoadingGalleryId !== gallery.id) {
11366    console.warn('Gallery ID mismatch! Expected:', gallery.id,
11367                 'Current:', window.currentLoadingGalleryId);
11368  }
11369  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
11370};
11371
11372
11373if (galleryContainer) {
11374    setTimeout(() => {
11375      galleryContainer.style.opacity = '1';
11376    }, 50); // Small delay to ensure content is ready
11377  }
11378
11379console.log(`Loading fresh gallery script for: ${gallery.title}`);
11380document.body.appendChild(script);
11381}
11382
11383
11384
11385function removeGalleryScriptsWithPause() {
11386  return new Promise(resolve => {
11387    console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)');
11388    
11389    // 1. Capture and clear any gallery data in localStorage (remains the same)
11390    try {
11391      const localStorageKeys = Object.keys(localStorage);
11392      const galleryStorageKeys = localStorageKeys.filter(key => 
11393        key.includes('gallery') || 
11394        key.includes('neon') || 
11395        key.includes('lightbox')
11396      );
11397      galleryStorageKeys.forEach(key => {
11398        localStorage.removeItem(key);
11399      });
11400    } catch (e) {
11401      console.warn('Error clearing localStorage:', e);
11402    }
11403    
11404    // @ts-ignore
11405    window._galleryPageContext = null; // (remains the same)
11406    
11407    // Keep HP gallery CSS. Page-element edit/view remounts skip HP reload;
11408    // stripping neon-gallery-hp-hydra.css zeros masonry-item / container margins.
11409    const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]');
11410    galleryStyles.forEach(style => {
11411      const href = (style && style.href) ? String(style.href) : '';
11412      if (style && style.id === 'neon-gallery-css') return;
11413      if (href.indexOf('neon-gallery-hp-hydra') !== -1) return;
11414      if (style && style.parentNode) {
11415        style.parentNode.removeChild(style);
11416      }
11417    });
11418    
11419    // 4. Remove lightbox elements (more specific selectors now)
11420    // These are specific IDs and classes for lightbox components.
11421    const lightboxComponentSelectors = [
11422      '#neon-lightbox',      // Main lightbox container by ID
11423      '.neon-lightbox',     // Alternative class for main container
11424      '.lightbox-container',// General container class if used
11425      // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements
11426    ];
11427    const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', '));
11428    lightboxElements.forEach(lightboxEl => {
11429      if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) {
11430        // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className);
11431        lightboxEl.parentNode.removeChild(lightboxEl);
11432      }
11433    });
11434    
11435    // 5. Remove other gallery-specific DOM elements
11436    // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list
11437    // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body.
11438    const galleryElementSelectors = [
11439      '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay',
11440      '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu',
11441      '.neon-thumbnails', '.neon-image', '.neon-caption',
11442      '.neon-controls', '.neon-pagination',
11443      '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues.
11444      '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry'
11445      // Ensure none of these selectors inadvertently match document.body or critical layout containers
11446    ];
11447    const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', '));
11448    
11449    if (galleryElements.length > 0) {
11450      galleryElements.forEach(el => {
11451        // CRITICAL FIX: Add checks to ensure we don't remove body or html
11452        if (el && el.parentNode && el !== document.body && el !== document.documentElement) {
11453          // console.log('Removing gallery-specific DOM element:', el.id || el.className);
11454          el.parentNode.removeChild(el);
11455        } else if (el === document.body || el === document.documentElement) {
11456          console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`);
11457        }
11458      });
11459    }
11460    
11461    // 6. Find and remove ALL gallery scripts (remains the same)
11462    const scripts = document.querySelectorAll(
11463      'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11464      'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11465    );
11466    scripts.forEach(script => {
11467      if (script && script.parentNode) {
11468        // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id'));
11469        script.parentNode.removeChild(script);
11470      }
11471    });
11472
11473    // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries)
11474    const optionsScripts = document.querySelectorAll(
11475      'script[src*="galleryOptions"], script[data-options-js]'
11476    );
11477    optionsScripts.forEach(script => {
11478      if (script && script.parentNode) {
11479        console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js'));
11480        script.parentNode.removeChild(script);
11481      }
11482    });
11483
11484    // NOTE: We intentionally do NOT remove gallery options CSS here
11485    // The CSS is needed across galleries and doesn't cause conflicts like scripts do
11486
11487    setTimeout(() => {
11488      const remainingScripts = document.querySelectorAll(
11489        'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' +
11490        'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]'
11491      );
11492      if (remainingScripts.length > 0) {
11493        // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`);
11494      }
11495    }, 50);
11496        
11497    // 7. Reset ALL known global state variables used by the gallery (remains the same)
11498    const resetGlobals = [
11499      'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
11500      'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState',
11501      'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings',
11502      'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance',
11503      'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache',
11504    ];
11505    
11506    resetGlobals.forEach(prop => {
11507      // @ts-ignore
11508      if (window[prop] !== undefined) {
11509        // @ts-ignore
11510        window[prop] = null;
11511      }
11512    });
11513    // Keep the bootstrap wipe flag. Clearing it let a re-injected main script
11514    // replace PageManager embed config with the default "mod" collection.
11515    if (typeof window.resetNeonGalleryInitGates === 'function') {
11516      window.resetNeonGalleryInitGates('removeGalleryScriptsWithPause');
11517    }
11518
11519    // 7b. Reset gallery options panel state
11520    if (window._editorButtonVisibilityInterval) {
11521      clearInterval(window._editorButtonVisibilityInterval);
11522      window._editorButtonVisibilityInterval = null;
11523    }
11524    const editorBtn = document.getElementById('editorButton');
11525    if (editorBtn) {
11526      editorBtn.classList.add('gallery-options-force-hidden');
11527      editorBtn.style.setProperty('display', 'none', 'important');
11528      editorBtn.style.pointerEvents = '';
11529    }
11530    if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
11531      window.syncGalleryOptionsButtonVisibility();
11532    }
11533    // Reset optionsVisible flag if it exists on window (for cross-module access)
11534    // @ts-ignore
11535    if (window.optionsVisible !== undefined) {
11536      // @ts-ignore
11537      window.optionsVisible = false;
11538    }
11539    
11540    // Reset options-related globals
11541    const optionsGlobals = [
11542      'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 
11543      'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible'
11544    ];
11545    optionsGlobals.forEach(prop => {
11546      // @ts-ignore
11547      if (window[prop] !== undefined && typeof window[prop] !== 'function') {
11548        // Only reset non-function properties (like flags)
11549        // Functions should remain available
11550        // @ts-ignore
11551        if (prop === 'optionsVisible') {
11552          // @ts-ignore
11553          window[prop] = false;
11554        }
11555      }
11556    });
11557    
11558    // Reset EnhancedGallerySystem so a pending initPromise cannot hang forever
11559    // after scripts it was waiting on were just removed from the DOM.
11560    if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') {
11561      window.EnhancedGallerySystem.resetInitializationState();
11562    }
11563
11564    // Reset PageManager page-embedded gallery tracking
11565    window.neonGalleryScriptLoaded = false;
11566    window.pageManagerGalleryInitBlocked = false;
11567    if (window.pageManagerGalleryContainers) {
11568      window.pageManagerGalleryContainers.clear();
11569    }
11570
11571    // Remove orphaned lightbox keyboard listeners (survive script tag removal)
11572    if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
11573      window.removeAllLightboxKeyboardListeners();
11574    } else if (window.currentLightboxKeyboardListener) {
11575      document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
11576      window.currentLightboxKeyboardListener = null;
11577      window.lightboxKeyboardListenerActive = false;
11578    }
11579
11580    // Short pause before resolving
11581    setTimeout(resolve, 150);
11582  });
11583}
11584
11585
11586// Fixed clearAddForm function with proper variable names
11587function clearAddForm() {
11588// Clear the title input
11589const titleInput = document.getElementById('galleryTitle');
11590if (titleInput) {
11591titleInput.value = '';
11592}
11593
11594// Clear the URL input
11595const urlInput = document.getElementById('galleryUrl');
11596if (urlInput) {
11597urlInput.value = '';
11598}
11599
11600// Reset the parent selection
11601const parentSelect = document.getElementById('galleryParent');
11602if (parentSelect) {
11603parentSelect.value = '';
11604}
11605
11606// Reset radio buttons to Gallery (integrated) as default
11607const galleryRadio = document.querySelector('input[name="galleryType"][value="integrated"]');
11608if (galleryRadio) {
11609galleryRadio.checked = true;
11610}
11611
11612// Hide URL input container when default is Gallery
11613const urlInputContainer = document.getElementById('urlInputContainer');
11614if (urlInputContainer) {
11615urlInputContainer.style.display = 'none';
11616} else {
11617console.log('URL input container not found');
11618}
11619}
11620
11621/**
11622 * Ensures window.galleries is synchronized with the global galleries variable
11623 * Call this function after any modification to gallery settings
11624 * 
11625 * @returns {boolean} True if synchronization was successful
11626 */
11627/**
11628 * Enhanced synchronizeGalleries function that checks ALL galleries
11629 * not just imported ones, and ensures settings are preserved
11630 * @returns {boolean} Success indicator
11631 */
11632function synchronizeGalleries() {
11633  console.log('Starting enhanced gallery synchronization...');
11634  
11635  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
11636    // First check if window.galleries exists and is different from galleries
11637    const needsSync = !window.galleries || 
11638                     !Array.isArray(window.galleries) || 
11639                     window.galleries.length !== galleries.length;
11640    
11641    // ENHANCED: Check ALL galleries for options preservation, not just imported ones
11642    if (!needsSync && window.galleries.length > 0) {
11643      // Find all galleries with galleryOptions
11644      const galleriesWithOptions = galleries.filter(g => 
11645        g.galleryOptions && Object.keys(g.galleryOptions).length > 4
11646      );
11647      
11648      if (galleriesWithOptions.length > 0) {
11649        // Check ALL galleries with substantial options, not just a sample
11650        let syncNeeded = false;
11651        
11652        // Use for loop so we can break early if we find a mismatch
11653        for (let i = 0; i < galleriesWithOptions.length; i++) {
11654          const gallery = galleriesWithOptions[i];
11655          const windowGallery = window.galleries.find(g => g.id === gallery.id);
11656          
11657          if (windowGallery) {
11658            // Get options depth
11659            const galleryOptionsDepth = gallery.galleryOptions ? 
11660              Object.keys(gallery.galleryOptions).length : 0;
11661              
11662            const windowOptionsDepth = windowGallery.galleryOptions ? 
11663              Object.keys(windowGallery.galleryOptions).length : 0;
11664            
11665            // If options depth differs significantly, we need to sync
11666            if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) {
11667              console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`);
11668              console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`);
11669              
11670              syncNeeded = true;
11671              break; // No need to check more galleries
11672            }
11673            
11674            // Additional check: Look for specific important gallery settings that should be preserved
11675            // Only do this check for galleries with substantial settings
11676            if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) {
11677              // Critical keys that indicate gallery customization
11678              const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 
11679                                  'autoplaySingles', 'autoplayRandomStart', 'lightboxBgColor'];
11680              
11681              // Check if any critical keys are missing in window.galleries
11682              const missingKeys = criticalKeys.filter(key => 
11683                gallery.galleryOptions[key] !== undefined && 
11684                windowGallery.galleryOptions[key] === undefined
11685              );
11686              
11687              if (missingKeys.length > 0) {
11688                console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`);
11689                console.log(`Missing keys: ${missingKeys.join(', ')}`);
11690                
11691                syncNeeded = true;
11692                break; // No need to check more galleries
11693              }
11694            }
11695          }
11696        }
11697        
11698        // If we detected a need to sync, do it now
11699        if (syncNeeded) {
11700          // Ensure we preserve rich gallery settings and per-page custom HTML fields
11701          window.galleries = galleries.map(gallery => {
11702            const windowGallery = window.galleries.find(g => g.id === gallery.id);
11703            if (!windowGallery) return gallery;
11704
11705            let merged = { ...gallery };
11706
11707            if (windowGallery.galleryOptions && gallery.galleryOptions) {
11708              const globalOptionsDepth = Object.keys(gallery.galleryOptions).length;
11709              const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length;
11710              if (windowOptionsDepth > globalOptionsDepth) {
11711                merged = {
11712                  ...merged,
11713                  galleryOptions: {
11714                    ...windowGallery.galleryOptions,
11715                    ...gallery.galleryOptions
11716                  }
11717                };
11718              }
11719            }
11720
11721            if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11722              merged = {
11723                ...merged,
11724                customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11725                customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11726                customPageHtmlFilename: windowGallery.customPageHtmlFilename
11727              };
11728            }
11729
11730            return merged;
11731          });
11732          
11733          console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11734          return true;
11735        }
11736      }
11737    }
11738    
11739    // If we determined sync is needed (or as a precaution), update window.galleries
11740    if (needsSync || !window.galleries) {
11741      if (window.galleries && Array.isArray(window.galleries)) {
11742        galleries = galleries.map(function(gallery) {
11743          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11744          if (!windowGallery) return gallery;
11745          if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) {
11746            return {
11747              ...gallery,
11748              customPageHtmlEnabled: windowGallery.customPageHtmlEnabled,
11749              customPageHtmlR2Key: windowGallery.customPageHtmlR2Key,
11750              customPageHtmlFilename: windowGallery.customPageHtmlFilename
11751            };
11752          }
11753          return gallery;
11754        });
11755      }
11756      window.galleries = [...galleries]; // Create a shallow copy to ensure different reference
11757      console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`);
11758    } else {
11759      console.log('Gallery synchronization check: No sync needed');
11760    }
11761    
11762    // Verify that all galleries with rich settings have their full options preserved
11763    const richGalleries = galleries.filter(g => 
11764      g.galleryOptions && Object.keys(g.galleryOptions).length > 5
11765    );
11766    
11767    console.log(`Status: Found ${richGalleries.length} galleries with rich settings`);
11768    
11769    // Final verification - keep both arrays aligned without dropping page-level fields
11770    if (window.galleries !== galleries) {
11771      console.warn('References not identical after sync - merging page fields into global galleries');
11772      if (window.galleries && Array.isArray(window.galleries)) {
11773        galleries = galleries.map(function(gallery) {
11774          const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; });
11775          if (!windowGallery) return gallery;
11776          return {
11777            ...gallery,
11778            customPageHtmlEnabled: windowGallery.customPageHtmlEnabled ?? gallery.customPageHtmlEnabled,
11779            customPageHtmlR2Key: windowGallery.customPageHtmlR2Key ?? gallery.customPageHtmlR2Key,
11780            customPageHtmlFilename: windowGallery.customPageHtmlFilename ?? gallery.customPageHtmlFilename,
11781            hideMenuOnPage: windowGallery.hideMenuOnPage ?? gallery.hideMenuOnPage,
11782            overlayMenuOnPage: windowGallery.overlayMenuOnPage ?? gallery.overlayMenuOnPage
11783          };
11784        });
11785      }
11786      window.galleries = galleries;
11787      return true;
11788    }
11789    
11790    return true;
11791  } else {
11792    console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array');
11793    
11794    // If we have window.galleries but no global galleries, try to reverse-sync
11795    if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) {
11796      console.log('Attempting reverse sync: updating global galleries from window.galleries');
11797      try {
11798        // This is risky and should only happen in unusual circumstances
11799        galleries = window.galleries;
11800        return true;
11801      } catch (e) {
11802        console.error('Could not reverse-sync galleries:', e);
11803      }
11804    }
11805    
11806    return false;
11807  }
11808}
11809
11810
11811
11812/**
11813 * Loads NDJSON content from the original gallery
11814 * @param {string} siteId - The site ID
11815 * @param {string} pageId - The page ID of the original gallery
11816 * @returns {Promise<string|null>} - The NDJSON content or null if not found/error
11817 */
11818async function loadGalleryNDJSON(siteId, pageId) {
11819  console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId);
11820  
11821  if (!siteId || !pageId) {
11822    console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId });
11823    return null;
11824  }
11825
11826  try {
11827    const ndjsonFilename = `${siteId}_${pageId}.ndjson`;
11828    const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`;
11829    
11830    console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url);
11831    console.error('[NDJSON Duplication] Filename:', ndjsonFilename);
11832    
11833    const response = await fetch(url);
11834    
11835    console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText);
11836    
11837    if (!response.ok) {
11838      if (response.status === 404) {
11839        console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet');
11840        console.error('[NDJSON Duplication] Attempted URL:', url);
11841        return null;
11842      }
11843      const errorText = await response.text().catch(() => 'Could not read error text');
11844      console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText);
11845      throw new Error(`Failed to load NDJSON: ${response.statusText}`);
11846    }
11847    
11848    const ndjsonContent = await response.text();
11849    console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters');
11850    console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200));
11851    return ndjsonContent;
11852  } catch (error) {
11853    console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error);
11854    console.error('[NDJSON Duplication] Error message:', error.message);
11855    console.error('[NDJSON Duplication] Error stack:', error.stack);
11856    return null;
11857  }
11858}
11859
11860/**
11861 * Duplicates and publishes NDJSON for a gallery
11862 * @param {Object} originalGallery - The original gallery object
11863 * @param {Object} newGallery - The new gallery object with new pageId
11864 * @returns {Promise<boolean>} - Success indicator
11865 */
11866async function duplicateGalleryNDJSON(originalGallery, newGallery) {
11867  console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON =====');
11868  console.error('[NDJSON Duplication] Original gallery:', {
11869    id: originalGallery.id,
11870    title: originalGallery.title,
11871    pageId: originalGallery.pageId,
11872    classicGuid: originalGallery.classicGuid,
11873    hasGalleryOptions: !!originalGallery.galleryOptions,
11874    manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11875    isClassicCollection: originalGallery.galleryOptions?.isClassicCollection
11876  });
11877  console.error('[NDJSON Duplication] New gallery:', {
11878    id: newGallery.id,
11879    title: newGallery.title,
11880    pageId: newGallery.pageId
11881  });
11882  
11883  try {
11884    // Check if this is an NDJSON-based gallery (not classic collection)
11885    // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files
11886    // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON)
11887    // Having a pageId means it uses NDJSON files, so we should duplicate them
11888    const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true;
11889    
11890    console.error('[NDJSON Duplication] Classic collection check:', {
11891      hasClassicGuid: !!originalGallery.classicGuid,
11892      manualCollectionName: originalGallery.galleryOptions?.manualCollectionName,
11893      startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('G
11893UID='),
11894      isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection,
11895      hasPageId: !!originalGallery.pageId,
11896      isClassicCollection: isClassicCollection,
11897      willSkip: isClassicCollection && !originalGallery.pageId
11898    });
11899    
11900    // Only skip if explicitly marked as classic collection AND has no pageId
11901    // If it has a pageId, it uses NDJSON files and we should duplicate them
11902    if (isClassicCollection && !originalGallery.pageId) {
11903      console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication');
11904      return true; // Not an error, just not applicable
11905    }
11906
11907    // Check if original gallery has a pageId
11908    if (!originalGallery.pageId) {
11909      console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication');
11910      console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery));
11911      return true; // Not an error, just not applicable
11912    }
11913
11914    // Get siteId
11915    const siteId = window.siteId || originalGallery.siteId;
11916    console.error('[NDJSON Duplication] SiteId resolution:', {
11917      windowSiteId: window.siteId,
11918      gallerySiteId: originalGallery.siteId,
11919      resolvedSiteId: siteId
11920    });
11921    
11922    if (!siteId) {
11923      console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId');
11924      console.error('[NDJSON Duplication] window.siteId:', window.siteId);
11925      console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId);
11926      return false;
11927    }
11928
11929    // Load original NDJSON
11930    console.error('[NDJSON Duplication] Loading original NDJSON for duplication...');
11931    console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId);
11932    const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId);
11933    
11934    if (!originalNdjson) {
11935      console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet');
11936      console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found');
11937      return false;
11938    }
11939
11940    // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher
11941    console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId);
11942    const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`;
11943    console.error('[NDJSON Duplication] New filename:', dataFileName);
11944    console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length);
11945    
11946    try {
11947      // Hydra sites require Bearer auth (same token resolution as Edition Publisher)
11948      const publishHeaders = { 'Content-Type': 'application/json' };
11949      let authToken = null;
11950      try {
11951        if (window.TokenManager) {
11952          if (typeof window.TokenManager.getHydraAuthToken === 'function') {
11953            try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {}
11954          }
11955          if (!authToken && typeof window.TokenManager.getBestToken === 'function') {
11956            try {
11957              const tokenWithBearer = window.TokenManager.getBestToken();
11958              authToken = tokenWithBearer && tokenWithBearer.startsWith('Bearer ')
11959                ? tokenWithBearer.substring(7)
11960                : tokenWithBearer;
11961            } catch (e) {}
11962          }
11963        }
11964        if (!authToken) authToken = localStorage.getItem('hydra_auth_token');
11965        if (!authToken) authToken = window.authToken || window.didToken;
11966        if (authToken) {
11967          publishHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:')
11968            ? 'Bearer ' + authToken
11969            : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken);
11970          console.error('[NDJSON Duplication] Auth token attached for publish');
11971        } else {
11972          console.error('[NDJSON Duplication] WARNING: No auth token — publish will likely fail with 401');
11973        }
11974      } catch (authErr) {
11975        console.error('[NDJSON Duplication] Auth token lookup failed:', authErr.message);
11976      }
11977
11978      // Call the publish endpoint (same as edition publisher)
11979      console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish');
11980      const response = await fetch('https://hydra-press-v2.fly.dev/publish', {
11981        method: 'POST',
11982        headers: publishHeaders,
11983        body: JSON.stringify({
11984          ndjsonContent: originalNdjson,
11985          dataFileName: dataFileName
11986        }),
11987      });
11988      
11989      console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText);
11990      console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries()));
11991      
11992      if (!response.ok) {
11993        const errorText = await response.text().catch(() => 'Unknown error');
11994        console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText);
11995        console.error('[NDJSON Duplication] Response status:', response.status);
11996        console.error('[NDJSON Duplication] Response statusText:', response.statusText);
11997        return false;
11998      }
11999      
12000      const result = await response.json();
12001      console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON');
12002      console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2));
12003      console.error('[NDJSON Duplication] Publish result URLs:', result.urls);
12004      
12005      // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions
12006      // with txId/permaURL here. If Irys upload is needed, it would need to be done separately.
12007      
12008      console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY =====');
12009      return true;
12010    } catch (error) {
12011      console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error);
12012      console.error('[NDJSON Duplication] Error message:', error.message);
12013      console.error('[NDJSON Duplication] Error stack:', error.stack);
12014      return false;
12015    }
12016  } catch (error) {
12017    console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error);
12018    console.error('[NDJSON Duplication] Error message:', error.message);
12019    console.error('[NDJSON Duplication] Error stack:', error.stack);
12020    return false;
12021  }
12022}
12023
12024/**
12025 * Creates and shows a loading overlay for gallery deletion (same pattern as duplication).
12026 * @returns {Object} Object with hide() and updateMessage() to control the overlay
12027 */
12028function showDeletionOverlay() {
12029    const existingOverlay = document.getElementById('deletion-overlay');
12030    if (existingOverlay) existingOverlay.remove();
12031    if (!document.querySelector('#deletion-overlay-styles')) {
12032        const style = document.createElement('style');
12033        style.id = 'deletion-overlay-styles';
12034        style.textContent = `
12035            #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; }
12036            #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; }
12037            #deletion-overlay .deletion-spinner { width: 80px; height: 80px; color: #444; }
12038        `;
12039        document.head.appendChild(style);
12040    }
12041    const overlayEl = document.createElement('div');
12042    overlayEl.id = 'deletion-overlay';
12043    const messageEl = document.createElement('div');
12044    messageEl.className = 'deletion-message';
12045    const spinnerEl = document.createElement('div');
12046    spinnerEl.className = 'deletion-spinner';
12047    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>`;
12048    const textEl = document.createElement('div');
12049    textEl.textContent = 'Deleting...';
12050    textEl.style.textAlign = 'center';
12051    messageEl.appendChild(spinnerEl);
12052    messageEl.appendChild(textEl);
12053    overlayEl.appendChild(messageEl);
12054    document.body.appendChild(overlayEl);
12055    return {
12056        hide: function() { if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl); },
12057        updateMessage: function(msg) { textEl.textContent = msg; }
12058    };
12059}
12060
12061/**
12062 * Creates and shows a loading overlay for gallery duplication
12063 * @returns {Object} Object with hide function to remove the overlay
12064 */
12065function showDuplicationOverlay() {
12066    // Remove any existing overlay first
12067    const existingOverlay = document.getElementById('duplication-overlay');
12068    if (existingOverlay) {
12069        existingOverlay.remove();
12070    }
12071    
12072    // Add styles if not already present
12073    if (!document.querySelector('#duplication-overlay-styles')) {
12074        const style = document.createElement('style');
12075        style.id = 'duplication-overlay-styles';
12076        style.textContent = `
12077            #duplication-overlay {
12078                position: fixed;
12079                top: 0;
12080                left: 0;
12081                right: 0;
12082                bottom: 0;
12083                background: rgba(0, 0, 0, 0.5);
12084                z-index: 99999999;
12085                display: flex;
12086                align-items: center;
12087                justify-content: center;
12088                cursor: not-allowed;
12089                pointer-events: all;
12090            }
12091            #duplication-overlay .duplication-message {
12092                background: rgba(255, 255, 255, 0.95);
12093                padding: 30px 50px;
12094                font-family: Arial, sans-serif;
12095                font-size: 16px;
12096                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
12097                display: flex;
12098                flex-direction: column;
12099                align-items: center;
12100                gap: 20px;
12101            }
12102            #duplication-overlay .duplication-spinner {
12103                width: 80px;
12104                height: 80px;
12105                color: #444444;
12106            }
12107        `;
12108        document.head.appendChild(style);
12109    }
12110    
12111    const overlay = document.createElement('div');
12112    overlay.id = 'duplication-overlay';
12113    
12114    const message = document.createElement('div');
12115    message.className = 'duplication-message';
12116    
12117    const spinner = document.createElement('div');
12118    spinner.className = 'duplication-spinner';
12119    spinner.innerHTML = `
12120        <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24">
12121          <defs>
12122            <filter id="svgSpinnersGooeyBalls20">
12123              <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/>
12124              <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"/>
12125              <feBlend in="SourceGraphic" in2="z"/>
12126            </filter>
12127          </defs>
12128          <g filter="url(#svgSpinnersGooeyBalls20)">
12129            <circle cx="5" cy="12" r="4" fill="currentColor">
12130              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/>
12131            </circle>
12132            <circle cx="19" cy="12" r="4" fill="currentColor">
12133              <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/>
12134            </circle>
12135            <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
12136          </g>
12137        </svg>
12138    `;
12139    
12140    const text = document.createElement('div');
12141    text.textContent = 'Duplicating gallery...';
12142    text.style.textAlign = 'center';
12143    
12144    message.appendChild(spinner);
12145    message.appendChild(text);
12146    overlay.appendChild(message);
12147    document.body.appendChild(overlay);
12148    
12149    return {
12150        hide: function() {
12151            if (overlay && overlay.parentNode) {
12152                overlay.parentNode.removeChild(overlay);
12153            }
12154        },
12155        updateMessage: function(newMessage) {
12156            text.textContent = newMessage;
12157        }
12158    };
12159}
12160
12161/**
12162 * Duplicates a gallery or page item.
12163 * @param {number} id - The ID of the item to duplicate.
12164 * @param {Event} event - The click event.
12165 */
12166async function duplicateGalleryItem(id, event) {
12167    event.stopPropagation();
12168    console.log(`Duplicating item with ID: ${id}`);
12169
12170    // Show loading overlay
12171    const overlay = showDuplicationOverlay();
12172
12173    // Find the original item in the global galleries array
12174    const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find
12175
12176    if (!originalItem) {
12177        console.error(`Cannot duplicate: Item with ID ${id} not found.`);
12178        overlay.hide();
12179        alert('Error: Could not find the item to duplicate.');
12180        return;
12181    }
12182
12183    if (isPageMenuItem(originalItem)) {
12184        overlay.updateMessage('Duplicating page...');
12185    }
12186
12187    try {
12188        // --- Create a Deep Copy ---
12189        // Using JSON.parse/stringify is a common way for simple object structures
12190        // Be cautious if your objects have Dates, functions, Maps, Sets, etc.
12191        const newItem = JSON.parse(JSON.stringify(originalItem));
12192
12193        // --- Modify Copied Item ---
12194        newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID
12195        newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title
12196
12197        // Generate new pageId if it's a page or integrated gallery
12198        if (newItem.isPage || newItem.isIntegrated) {
12199            newItem.pageId = generatePageId(); // Assign a new unique pageId
12200        }
12201
12202        // Generate new slug and URL
12203        const newBaseSlug = slugify(newItem.title); // Use the new title
12204        newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness
12205        newItem.url = `/${newItem.slug}`; // Update URL based on new slug
12206
12207        // Reset children (don't duplicate children for simplicity)
12208        newItem.children = [];
12209
12210        // Deep clone galleryOptions and pageElements, update pageId if needed
12211        if (newItem.galleryOptions) {
12212            newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions));
12213            // Update pageId within options if it exists and matches the old one
12214            if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) {
12215                newItem.galleryOptions.pageId = newItem.pageId;
12216            }
12217            // Clear permanent-storage pointers so duplicate loads its own Tigris NDJSON, not the original
12218            ['txId', 'permaURL', 'loadTxId', 'loadPermaURL', 'loadNetworkUrl', 'r2StorageUrl',
12219             'cdnNdjsonId', 'cdnNdjsonUrl', 'cdnNdjsonStableUrl', 'embedPointerUrl', 'cdnSaveTimestamp',
12220             'flyIoSaveTimestamp', 'loadNetworkSaveTimestamp', 'irysSaveTimestamp', 'timestampsMatch', 'isPerma']
12221              .forEach(function(field) { delete newItem.galleryOptions[field]; });
12222        }
12223        if (newItem.pageElements) {
12224            newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements));
12225             // Give new IDs to duplicated page elements
12226             newItem.pageElements.forEach(el => {
12227                 el.id = Date.now() + Math.floor(Math.random() * 10000);
12228             });
12229        }
12230
12231        // Reset home page status
12232        newItem.isHomePage = false;
12233
12234        // --- Duplicate NDJSON if applicable (galleries only; pages copy content via pageElements) ---
12235        const isDuplicatingPage = isPageMenuItem(originalItem);
12236        console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...', {
12237            isDuplicatingPage,
12238            newItemPageId: newItem.pageId,
12239            originalItemPageId: originalItem.pageId
12240        });
12241
12242        if (!isDuplicatingPage && newItem.pageId && originalItem.pageId) {
12243            overlay.updateMessage('Duplicating gallery files...');
12244            console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...');
12245            const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem);
12246            if (!ndjsonDuplicated) {
12247                console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication');
12248                console.error('[Gallery Duplication] User can republish the gallery later if needed');
12249                alert('Gallery menu item was created, but gallery images could not be copied. Open Edition Publisher on this gallery and publish to load images.');
12250                // Don't block the duplication if NDJSON fails - user can republish later
12251            } else {
12252                console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully');
12253            }
12254        } else {
12255            console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', {
12256                isDuplicatingPage,
12257                hasNewPageId: !!newItem.pageId,
12258                hasOriginalPageId: !!originalItem.pageId,
12259                reason: isDuplicatingPage ? 'Page items use pageElements, not NDJSON' :
12260                    !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId'
12261            });
12262        }
12263
12264        overlay.updateMessage(isDuplicatingPage ? 'Saving duplicated page...' : 'Saving duplicated gallery...');
12265
12266        // --- Insert into Galleries Array ---
12267        // Find the index of the original item
12268        const originalIndex = galleries.findIndex(item => item.id === id);
12269
12270        if (originalIndex > -1) {
12271            // Insert the new item right after the original
12272            galleries.splice(originalIndex + 1, 0, newItem);
12273            console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`);
12274
12275             // Update positions for all items after the insertion point
12276             for (let i = originalIndex + 1; i < galleries.length; i++) {
12277                 galleries[i].position = i; // Re-assign position based on new index
12278             }
12279
12280        } else {
12281            // Fallback: Add to the end if original couldn't be found (shouldn't happen)
12282            console.warn(`Original item ${id} not found in array, adding duplicate to the end.`);
12283            newItem.position = galleries.length;
12284            galleries.push(newItem);
12285        }
12286
12287        console.log('Rendering galleries immediately after duplication...');
12288        renderGalleries();
12289        // Update active states if necessary (though unlikely needed here)
12290        updateActiveStates();
12291
12292        // --- Save and Refresh ---
12293        console.log('Saving duplicated item...');
12294        saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh
12295            .then(success => {
12296                overlay.hide(); // Hide overlay before page refresh
12297                if (success) {
12298                    console.log('Duplicate saved successfully. Page will refresh.');
12299                    // No need to call renderGalleries() here because saveGalleries will trigger a reload.
12300                } else {
12301                    console.error('Failed to save the duplicated item.');
12302                    alert('Error: Could not save the duplicated item.');
12303                    // Optionally try to revert the galleries array change here if save fails
12304                }
12305            })
12306            .catch(error => {
12307                 overlay.hide(); // Hide overlay on error
12308                 console.error('Error saving duplicated item:', error);
12309                 alert('Error saving duplicated item.');
12310            });
12311
12312    } catch (error) {
12313        overlay.hide(); // Hide overlay on error
12314        console.error('Error duplicating item:', error);
12315        alert('An error occurred while duplicating the item.');
12316    }
12317}
12318
12319/**
12320 * saveGalleries function that always saves FULL data
12321 * Ensures gallery settings are preserved in both localStorage and server
12322 * 
12323 * @param {Object} customData - Optional custom data to save instead of galleries
12324 * @returns {Promise<boolean>} - Success indicator
12325 */
12326
12327function attachLiveSidebarElements(data) {
12328  if (!data || typeof data !== 'object') return data;
12329  var hasGetter = !!(window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function');
12330  if (hasGetter) {
12331    var live = window.SidebarManager.getElementsForSave();
12332    if (Array.isArray(live) && live.length > 0) {
12333      data.sidebarElements = live;
12334      return data;
12335    }
12336    delete data.sidebarElements;
12337    return data;
12338  }
12339  if (Array.isArray(data.sidebarElements) && data.sidebarElements.length === 0) {
12340    delete data.sidebarElements;
12341  }
12342  return data;
12343}
12344
12345async function saveGalleries(customData) {
12346  if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') {
12347    alert('You must be logged in as an admin to save changes');
12348    return false;
12349  }
12350
12351  if (window.HelpSystem && typeof window.HelpSystem.isSandboxActive === 'function' && window.HelpSystem.isSandboxActive()) {
12352    console.warn('Save blocked: help sandbox is active');
12353    return false;
12354  }
12355
12356  function _hasHelpDemoInTree(items) {
12357    if (!Array.isArray(items)) return false;
12358    for (var i = 0; i < items.length; i++) {
12359      var g = items[i];
12360      if (!g) continue;
12361      if (g.isHelpDemo || Number(g.id) < 0) return true;
12362      if (g.children && _hasHelpDemoInTree(g.children)) return true;
12363    }
12364    return false;
12365  }
12366
12367  if (window.activeGalleryId != null && Number(window.activeGalleryId) < 0) {
12368    console.warn('Save blocked: active help demo gallery');
12369    return false;
12370  }
12371
12372  var savePayload = customData && customData.galleries ? customData.galleries : galleries;
12373  if (_hasHelpDemoInTree(savePayload)) {
12374    console.warn('Save blocked: payload contains help demo records');
12375    return false;
12376  }
12377
12378  // Prevent multiple simultaneous saves — queue the latest payload instead of dropping it
12379  if (window._saveInProgress) {
12380    console.log('Save already in progress, queueing latest payload');
12381    if (customData) {
12382      window._pendingSaveGalleriesData = attachLiveSidebarElements(customData);
12383    }
12384    return new Promise(function(resolve) {
12385      window._pendingSaveResolvers = window._pendingSaveResolvers || [];
12386      window._pendingSaveResolvers.push(resolve);
12387    });
12388  }
12389
12390  window._saveInProgress = true;
12391
12392  try {
12393    // CRITICAL: Synchronize galleries before saving to ensure we use the latest data
12394    if (typeof synchronizeGalleries === 'function') {
12395      synchronizeGalleries();
12396      console.log('Galleries synchronized before saving');
12397    }
12398
12399    // Try to get a valid token from multiple sources
12400    let token = null;
12401    let tokenSource = 'none';
12402    
12403    // First try the global didToken (traditional approach)
12404    if (didToken) {
12405      console.log('Using global didToken for save operation');
12406      token = didToken;
12407      tokenSource = 'didToken';
12408    } 
12409    // Next try TokenManager if available
12410    else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
12411      token = window.TokenManager.getToken();
12412      
12413      // Remove Bearer prefix if present (we'll add it later)
12414      if (token && token.startsWith('Bearer ')) {
12415        token = token.substring(7);
12416      }
12417      
12418      console.log('Using TokenManager token for save operation, length:', token ? token.length : 0);
12419      tokenSource = 'TokenManager';
12420    }
12421    // Fallback to localStorage
12422    else if (localStorage.getItem('hydra_auth_token')) {
12423      token = localStorage.getItem('hydra_auth_token');
12424      console.log('Using localStorage token for save operation, length:', token.length);
12425      tokenSource = 'localStorage';
12426    }
12427    // Last resort: try to get a fresh token
12428    else if (magic && magic.user) {
12429      try {
12430        console.log('Attempting to get fresh token from Magic');
12431        token = await magic.user.getIdToken();
12432        console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed');
12433        tokenSource = token ? 'magic' : 'none';
12434      } catch (e) {
12435        console.error('Error getting token from Magic:', e);
12436        tokenSource = 'magic_error';
12437      }
12438    }
12439    
12440    // [AUTH_SAVE] Log for S+J download - token state and logged-in email before request
12441    const loggedInEmail = (window.userMetadata && window.userMetadata.email) ||
12442      localStorage.getItem('hydra_auth_email') ||
12443      '';
12444    console.log('[AUTH_SAVE] Token resolved:', {
12445      loggedInEmail: loggedInEmail || '(none)',
12446      tokenSource,
12447      tokenLength: token ? token.length : 0,
12448      tokenPrefix: token ? token.substring(0, 15) + '...' : '(none)',
12449      hasToken: !!token,
12450      isAuthenticated,
12451      hydraIsAdmin: localStorage.getItem('hydra_is_admin') === 'true',
12452      hasLocalStorageToken: !!localStorage.getItem('hydra_auth_token'),
12453      hasTokenManager: !!(window.TokenManager && typeof window.TokenManager.getToken === 'function')
12454    });
12455    
12456    if (!token) {
12457      console.error('[AUTH_SAVE] No token available - save will fail. Sources checked: didToken, TokenManager, localStorage, magic.');
12458      throw new Error('Could not find a valid authentication token. Please log in again.');
12459    }
12460
12461    // IMPORTANT: Try to save to localStorage with isolated error handling
12462    // This ensures localStorage failures don't prevent server saves
12463    if (!customData) { // Only try localStorage for regular gallery saves
12464      try {
12465        // FIXED: Always save the FULL galleries data to localStorage
12466        localStorage.setItem('galleries', JSON.stringify(galleries));
12467        console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`);
12468      } catch (storageError) {
12469        // Now storage errors are isolated and non-fatal
12470        console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError);
12471      }
12472    } else if (customData && customData.isImport) {
12473      // Skip localStorage for import operations - data too large
12474      console.log('Skipping localStorage for import operation - data too large for local storage');
12475    }
12476
12477    // IMPORTANT: Ensure all pages have proper URLs and slugs before saving
12478    galleries.forEach(gallery => {
12479      if (gallery.isPage === true) {
12480        const pageSlug = slugify(gallery.title);
12481        
12482        // Make sure slug and URL are set correctly
12483        if (!gallery.slug || gallery.slug === '') {
12484          gallery.slug = pageSlug;
12485        }
12486        
12487        // Make sure URL points to slug
12488        if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) {
12489          gallery.url = `/${pageSlug}`;
12490        }
12491      }
12492    });
12493
12494    // Set up the data to save - ensure we're using the complete data
12495    let siteData;
12496    console.log('saveGalleries: customData received:', JSON.stringify(customData));
12497    console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata);
12498    console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title);
12499    console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics);
12500    if (customData) {
12501      // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar
12502      // If customData contains galleries (which it should for sidebar-initiated saves),
12503      // merge them with any existing settings to ensure nothing is lost
12504      if (customData.galleries && Array.isArray(customData.galleries)) {
12505        // First, ensure we preserve all current gallery settings from the existing galleries
12506        const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || [];
12507        
12508        // Create merged galleries array with preserved settings
12509        const mergedGalleries = customData.galleries.map(gallery => {
12510          const existingGallery = existingGalleries.find(g => g.id === gallery.id);
12511          
12512          // If this gallery exists in the current dataset, ensure all galleryOptions are preserved
12513          if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) {
12514            // Calculate settings depth
12515            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12516            const serverOptionsDepth = Object.keys(gallery.galleryOptions).length;
12517            
12518            // If server has fewer settings, merge them with existing settings
12519            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12520              console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12521              
12522              // Create a merged gallery with preserved settings
12523              return {
12524                ...gallery,
12525                galleryOptions: {
12526                  ...existingGallery.galleryOptions, // Start with all existing settings
12527                  ...gallery.galleryOptions    // Override with any new server settings
12528                }
12529              };
12530            }
12531          }
12532          
12533          // If no preservation needed, use server gallery as is
12534          return gallery;
12535        });
12536        
12537        // Use the merged galleries in customData
12538        siteData = {
12539          ...customData,
12540          galleries: mergedGalleries
12541        };
12542        
12543        console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`);
12544        console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData));
12545        console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata);
12546        console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title);
12547        console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics);
12548      } else {
12549        siteData = customData;
12550      }
12551    } else {
12552      const galleriesForSave = (typeof window !== 'undefined' && window.galleries && Array.isArray(window.galleries))
12553        ? window.galleries
12554        : galleries;
12555      siteData = {
12556        galleries: galleriesForSave
12557      };
12558      
12559      if (window.siteMetadata) {
12560        siteData.siteMetadata = window.siteMetadata;
12561      }
12562
12563      if (window.SidebarManager) {
12564        const liveSidebar = typeof window.SidebarManager.getElementsForSave === 'function'
12565          ? window.SidebarManager.getElementsForSave()
12566          : null;
12567        if (Array.isArray(liveSidebar) && liveSidebar.length > 0) {
12568          siteData.sidebarElements = liveSidebar;
12569        }
12570      }
12571    }
12572
12573    attachLiveSidebarElements(siteData);
12574
12575    // Ensure galleries are never saved as pages: any item with galleryOptions is a gallery and must persist with isPage false.
12576    // (loadPage can set isPage=true in memory; sidebar/full saves would otherwise persist that and convert TEAM etc. to pages.)
12577    if (siteData.galleries && Array.isArray(siteData.galleries)) {
12578      siteData.galleries = siteData.galleries.map(g => {
12579        if (g && g.galleryOptions && typeof g.galleryOptions === 'object') {
12580          return { ...g, isPage: false };
12581        }
12582        return g;
12583      });
12584    }
12585
12586    // Get the save URL
12587    const saveUrl = typeof getApiUrl === 'function' ? 
12588      getApiUrl('/api/save-config') : 
12589      '/api/save-config';
12590    
12591    // Get user email from various sources
12592    const userEmail = (window.userMetadata && window.userMetadata.email) || 
12593                     localStorage.getItem('hydra_auth_email') || 
12594                     '';
12595    
12596    // Ensure token has proper format
12597    const formattedToken = token.startsWith('hydra:') ? token :
12598                          (token.includes('.') ? token : `hydra:${token}`);
12599    
12600    console.log('Making server save request...');
12601    console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData));
12602    console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata);
12603    console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title);
12604    console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics);
12605    
12606    // [AUTH_SAVE] Log for S+J download - request details (no full token)
12607    console.log('[AUTH_SAVE] Sending save request:', {
12608      saveUrl,
12609      authHeaderPrefix: `Bearer ${formattedToken.substring(0, 20)}...`,
12610      userEmail: userEmail || '(empty)',
12611      tokenSource,
12612      formattedTokenLength: formattedToken.length
12613    });
12614    
12615    // Make the request
12616    let response = await fetch(saveUrl, {
12617      method: 'POST',
12618      headers: {
12619        'Content-Type': 'application/json',
12620        'Authorization': `Bearer ${formattedToken}`,
12621        'X-User-Email': userEmail,
12622        'X-API-Request': 'true' // Always include this header for API requests
12623      },
12624      body: JSON.stringify(siteData)
12625    });
12626
12627    console.log('Save response status:', response.status);
12628    
12629    // Process the response
12630    let responseText = await response.text();
12631    console.log('Server response text (first 500 chars):', responseText.substring(0, 500));
12632    let responseData;
12633    
12634    try {
12635      responseData = JSON.parse(responseText);
12636        console.log('Server response parsed successfully:', responseData);
12637        if (responseData.debug) {
12638          console.log('Server debug info:', responseData.debug);
12639          console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys);
12640          console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata);
12641          console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata);
12642          console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata);
12643          console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics);
12644        }
12645    } catch (jsonError) {
12646      if (response.status === 200 && responseText.trim().startsWith('<!DOCTYPE html>')) {
12647        responseData = { success: true };
12648      } else {
12649        throw new Error('Invalid response format: ' + responseText.substring(0, 100));
12650      }
12651    }
12652
12653    if (!response.ok && !responseData.success) {
12654      // [AUTH_SAVE] Log for S+J download - capture full 401/auth failure details
12655      const authSaveFailure = {
12656        status: response.status,
12657        statusText: response.statusText,
12658        error: responseData && responseData.error,
12659        details: responseData && responseData.details,
12660        authMethod: responseData && responseData.authMethod,
12661        fullResponse: responseData,
12662        responseTextPreview: responseText.substring(0, 300)
12663      };
12664      console.error('[AUTH_SAVE] Save request failed:', authSaveFailure);
12665      
12666      // Retry once on 401: token may have expired - try refresh or fresh Magic token
12667      if (response.status === 401) {
12668        console.log('[AUTH_SAVE] 401 received - attempting token refresh before retry...');
12669        let newToken = null;
12670        if (window.TokenManager && typeof window.TokenManager.refreshToken === 'function') {
12671          newToken = await window.TokenManager.refreshToken();
12672        }
12673        if (!newToken && magic && magic.user) {
12674          try {
12675            newToken = await magic.user.getIdToken();
12676          } catch (e) {
12677            console.warn('[AUTH_SAVE] Magic getIdToken failed:', e);
12678          }
12679        }
12680        if (newToken) {
12681          if (newToken.startsWith('Bearer ')) newToken = newToken.substring(7);
12682          token = newToken;
12683          didToken = newToken;
12684          localStorage.setItem('hydra_auth_token', newToken);
12685          if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12686            window.TokenManager.storeToken(newToken.startsWith('hydra:') ? newToken : `hydra:${newToken}`, userEmail);
12687          }
12688          console.log('[AUTH_SAVE] Got fresh token, retrying save...');
12689          const retryResponse = await fetch(saveUrl, {
12690            method: 'POST',
12691            headers: {
12692              'Content-Type': 'application/json',
12693              'Authorization': `Bearer ${token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`)}`,
12694              'X-User-Email': userEmail,
12695              'X-API-Request': 'true'
12696            },
12697            body: JSON.stringify(siteData)
12698          });
12699          const retryText = await retryResponse.text();
12700          let retryData;
12701          try { retryData = JSON.parse(retryText); } catch (e) {
12702            if (retryResponse.status === 200 && retryText.startsWith('<!DOCTYPE')) retryData = { success: true };
12703            else throw new Error('Invalid retry response');
12704          }
12705          if (retryResponse.ok || retryData.success) {
12706            response = retryResponse;
12707            responseText = retryText;
12708            responseData = retryData;
12709            console.log('[AUTH_SAVE] Retry succeeded');
12710          } else {
12711            console.error('[AUTH_SAVE] Retry also failed:', retryResponse.status, retryData);
12712            throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12713          }
12714        } else {
12715          throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12716        }
12717      } else {
12718        throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`);
12719      }
12720    }
12721
12722    console.log('Configuration saved successfully');
12723
12724    // Update local lastKnownConfigTimestamp after successful save
12725    if (responseData.lastModified) {
12726      window.lastKnownConfigTimestamp = responseData.lastModified;
12727      console.error('[CONFLICT DETECT] ✅ Updated local config timestamp from save response:', window.lastKnownConfigTimestamp);
12728    } else {
12729      console.error('[CONFLICT DETECT] ⚠️ No lastModified in save response, fetching from server...');
12730      // If server doesn't return timestamp, fetch it
12731      try {
12732        const timestampUrl = typeof getApiUrl === 'function' ? 
12733          getApiUrl('/api/config-timestamp') : 
12734          '/api/config-timestamp';
12735        const timestampResponse = await fetch(timestampUrl);
12736        if (timestampResponse.ok) {
12737          const timestampData = await timestampResponse.json();
12738          if (timestampData.lastModified) {
12739            window.lastKnownConfigTimestamp = timestampData.lastModified;
12740            console.error('[CONFLICT DETECT] ✅ Fetched and updated local config timestamp:', window.lastKnownConfigTimestamp);
12741          } else {
12742            console.error('[CONFLICT DETECT] ⚠️ No lastModified in fetched timestamp data:', timestampData);
12743          }
12744        } else {
12745          console.error('[CONFLICT DETECT] ⚠️ Failed to fetch timestamp after save, status:', timestampResponse.status);
12746        }
12747      } catch (err) {
12748        console.error('[CONFLICT DETECT] ⚠️ Could not fetch timestamp after save:', err);
12749      }
12750    }
12751
12752    // Store new token if provided
12753    if (responseData.hydraToken) {
12754      console.log('Received new hydra token from server');
12755      
12756      // Update the global didToken
12757      didToken = responseData.hydraToken.startsWith('hydra:') ? 
12758        responseData.hydraToken.substring(6) : responseData.hydraToken;
12759      
12760      // Update localStorage
12761      localStorage.setItem('hydra_auth_token', didToken);
12762      
12763      // Update TokenManager if available
12764      if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') {
12765        window.TokenManager.storeToken(responseData.hydraToken, userEmail);
12766      }
12767    }
12768
12769    // Process server response data if needed
12770    if (responseData.galleries) {
12771      // If server returns updated galleries data, update the global galleries variable
12772      if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) {
12773        // CRITICAL FIX: Before replacing galleries with server response,
12774        // preserve any gallery settings that might be lost in the response
12775        const updatedGalleries = responseData.galleries.map(serverGallery => {
12776          // Find the matching gallery in our current data
12777          const existingGallery = galleries.find(g => g.id === serverGallery.id);
12778          
12779          // Check if we need to preserve gallery settings
12780          if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) {
12781            // Calculate settings depth
12782            const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length;
12783            const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length;
12784            
12785            // If server has fewer settings, merge them with existing settings
12786            if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) {
12787              console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`);
12788              
12789              // Create a merged gallery with preserved settings
12790              return {
12791                ...serverGallery,
12792                galleryOptions: {
12793                  ...existingGallery.galleryOptions, // Start with all existing settings
12794                  ...serverGallery.galleryOptions    // Override with any new server settings
12795                }
12796              };
12797            }
12798          }
12799          
12800          // If no preservation needed, use server gallery as is
12801          return serverGallery;
12802        });
12803        
12804        // Update the galleries variable with our preserved data
12805        galleries = updatedGalleries;
12806        console.log(`Updated galleries from server response: ${galleries.length} galleries`);
12807        
12808        // IMPORTANT: Synchronize again after receiving server response
12809        if (typeof synchronizeGalleries === 'function') {
12810          synchronizeGalleries();
12811          console.log('Galleries re-synchronized after server response');
12812        }
12813        
12814        // Keep sidebar UI in sync with current galleries (e.g. after delete)
12815        if (typeof renderGalleries === 'function') {
12816          renderGalleries();
12817        }
12818        
12819        // Update localStorage with full data
12820        try {
12821          localStorage.setItem('galleries', JSON.stringify(galleries));
12822          console.log('Updated localStorage with server gallery data (FULL format)');
12823        } catch (e) {
12824          console.warn('Could not update localStorage with server gallery data', e);
12825        }
12826      }
12827    }
12828
12829    // CRITICAL FIX: Clear all sessionStorage caches after successful save
12830    // This ensures that when users open the site in a new tab, they get fresh data
12831    try {
12832      console.log('Clearing sessionStorage caches after successful save...');
12833      const keysToRemove = [];
12834      for (let i = 0; i < sessionStorage.length; i++) {
12835        const key = sessionStorage.key(i);
12836        if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) {
12837          keysToRemove.push(key);
12838        }
12839      }
12840      keysToRemove.forEach(key => {
12841        sessionStorage.removeItem(key);
12842        console.log('Cleared sessionStorage key: ' + key);
12843      });
12844      console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries');
12845    } catch (cacheError) {
12846      console.warn('Error clearing sessionStorage cache:', cacheError);
12847      // Don't fail the save if cache clearing fails
12848    }
12849
12850    window._saveInProgress = false;
12851
12852    if (!customData || !Array.isArray(customData.sidebarElements)) {
12853      document.dispatchEvent(new CustomEvent('sidebar-save-requested'));
12854    }
12855
12856    // Flush any save queued while this request was in flight
12857    if (window._pendingSaveGalleriesData) {
12858      const pending = window._pendingSaveGalleriesData;
12859      window._pendingSaveGalleriesData = null;
12860      const resolvers = window._pendingSaveResolvers || [];
12861      window._pendingSaveResolvers = [];
12862      return saveGalleries(pending).then(function(result) {
12863        resolvers.forEach(function(r) { r(result); });
12864        return result;
12865      });
12866    }
12867
12868    const idleResolvers = window._pendingSaveResolvers || [];
12869    window._pendingSaveResolvers = [];
12870    idleResolvers.forEach(function(r) { r(true); });
12871
12872    return true;
12873  } catch (error) {
12874    // [AUTH_SAVE] Log for S+J download - capture exception (network, parse, or thrown after 401)
12875    console.error('[AUTH_SAVE] Save threw exception:', {
12876      message: error.message,
12877      name: error.name,
12878      stack: error.stack
12879    });
12880    console.error('Error saving configuration:', error);
12881    alert('Failed to save changes: ' + error.message);
12882    window._saveInProgress = false;
12883    const failResolvers = window._pendingSaveResolvers || [];
12884    window._pendingSaveResolvers = [];
12885    failResolvers.forEach(function(r) { r(false); });
12886    return false;
12887  }
12888}
12889
12890// Helper function to get API URL for preview sites
12891function getApiUrl(endpoint) {
12892  // Check if we're on a preview URL
12893  const isPreviewUrl = window.location.hostname === 'preview.neonsky.app';
12894  
12895  if (isPreviewUrl) {
12896    // Extract the site GUID from the path (first segment after domain)
12897    const pathParts = window.location.pathname.split('/').filter(Boolean);
12898    if (pathParts.length > 0) {
12899      const siteGuid = pathParts[0];
12900      // Include the site GUID in the API URL
12901      return `/${siteGuid}${endpoint}`;
12902    }
12903  }
12904  
12905  // Regular case - return the endpoint as is
12906  return endpoint;
12907}
12908
12909function updateActiveStates() {
12910  // Get the most current active gallery ID, ensuring consistency between both variables
12911  const currentActiveId = window.activeGalleryId || activeGalleryId;
12912  
12913  // Ensure both variables are synchronized
12914  window.activeGalleryId = currentActiveId;
12915  activeGalleryId = currentActiveId;
12916  
12917  console.log("updateActiveStates: Using active gallery ID:", currentActiveId);
12918  
12919  // Special case for invisible home page - don't try to highlight it in the menu
12920  const activeGallery = galleries.find(g => g.id === currentActiveId);
12921  if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) {
12922    console.log("Active gallery is invisible home page - not highlighting in menu");
12923    // We're not returning here, so it will still update the visibility classes
12924    // but won't try to find an element to mark as active
12925  }
12926  
12927  // Fix selector issue - look for both .tree and .sidebar
12928  document.querySelectorAll('.tree li, .sidebar li').forEach(li => {
12929    if (!li.dataset.id) return; // Skip items without data-id attribute
12930    
12931    const id = parseInt(li.dataset.id);
12932    if (isNaN(id)) return; // Skip if ID is not a number
12933    
12934    const gallery = galleries.find(g => g.id === id);
12935    
12936    // Update active state using the most current ID
12937    const isActive = id === currentActiveId;
12938    
12939    // Add or remove the active class
12940    if (isActive) {
12941      li.classList.add('active');
12942      console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`);
12943    } else {
12944      li.classList.remove('active');
12945    }
12946    
12947    // Update visibility class if gallery exists
12948    if (gallery) {
12949      li.classList.toggle('hidden-gallery', gallery.visible === false);
12950      
12951      // Update visibility toggle icon if it exists
12952      const toggleButton = li.querySelector('.visibility-toggle');
12953      if (toggleButton) {
12954        toggleButton.classList.toggle('hidden', gallery.visible === false);
12955      }
12956    }
12957  });
12958  
12959  // Verify that the active state was actually set for the current activeGalleryId
12960  const activeElements = document.querySelectorAll('.active');
12961  if (activeElements.length === 0 && currentActiveId) {
12962    // Only log a warning if we're not dealing with an invisible home page
12963    const activeGallery = galleries.find(g => g.id === currentActiveId);
12964    if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) {
12965      console.warn(`No active elements found after update for ID: ${currentActiveId}`);
12966    }
12967  }
12968  
12969  // Log active items for debugging
12970  console.log("Active gallery ID after update:", currentActiveId);
12971  activeElements.forEach(el => 
12972    console.log("Active element:", el.textContent.trim())
12973  );
12974}
12975
12976
12977function debugGalleryStructure() {
12978console.group('Gallery Structure Debug');
12979
12980// Log the raw data structure
12981console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries)));
12982
12983// Log the tree structure
12984const treeStructure = createGalleryTree(galleries);
12985
12986// Helper to print the tree
12987function printTree(items, level = 0) {
12988const indent = '  '.repeat(level);
12989items.forEach(item => {
12990  console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`);
12991  if (item.children && item.children.length > 0) {
12992    printTree(item.children, level + 1);
12993  }
12994});
12995}
12996
12997console.log('Tree structure:');
12998printTree(treeStructure);
12999
13000// Log the DOM structure
13001console.log('DOM structure:');
13002const treeEl = document.getElementById('galleryTree');
13003console.log(treeEl);
13004
13005console.groupEnd();
13006}
13007
13008function createGalleryTree(galleries) {
13009console.log("Creating gallery tree with preservation safeguards...");
13010
13011// First, create a map of galleries by id for quick lookup
13012const galleryMap = {};
13013const processedIds = new Set();
13014
13015// First pass: Create clean copies without children
13016galleries.forEach(gallery => {
13017if (!gallery || !gallery.id) return; // Skip invalid galleries
13018
13019// Create a copy of the gallery object without the children field
13020const cleanGallery = { ...gallery };
13021delete cleanGallery.children;
13022
13023// Initialize an empty children array
13024cleanGallery.children = [];
13025
13026// Add to map - even if we've seen this ID before, keep the latest version
13027if (processedIds.has(gallery.id)) {
13028  console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`);
13029}
13030
13031processedIds.add(gallery.id);
13032galleryMap[gallery.id] = cleanGallery;
13033});
13034
13035// Second pass: Assign children to their parents
13036const rootGalleries = [];
13037
13038// To detect already-processed children
13039const childrenAssigned = new Set();
13040
13041galleries.forEach(gallery => {
13042if (!gallery || !gallery.id) return; // Skip invalid galleries
13043
13044const galleryId = gallery.id;
13045
13046// Skip if this gallery isn't in our map (could be invalid)
13047if (!galleryMap[galleryId]) return;
13048
13049// Skip if we've already assigned this gallery as a child
13050if (childrenAssigned.has(galleryId)) return;
13051
13052if (gallery.parentId && galleryMap[gallery.parentId]) {
13053  // This is a child gallery, but first check it doesn't create a circular reference
13054  
13055  // Find all parents in the chain up to the root
13056  let currentParent = galleryMap[gallery.parentId];
13057  let safeToAdd = true;
13058  let chainIds = [galleryId, gallery.parentId];
13059  
13060  while (currentParent.parentId) {
13061    // If we've seen this ID in the chain, it would create a circular reference
13062    if (chainIds.includes(currentParent.parentId)) {
13063      console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`);
13064      safeToAdd = false;
13065      break;
13066    }
13067    
13068    chainIds.push(currentParent.parentId);
13069    currentParent = galleryMap[currentParent.parentId];
13070    
13071    // If parent doesn't exist in the map, break the chain
13072    if (!currentParent) break;
13073  }
13074  
13075  if (safeToAdd) {
13076    // Safe to add as child
13077    galleryMap[gallery.parentId].children.push(galleryMap[galleryId]);
13078    childrenAssigned.add(galleryId);
13079  } else {
13080    // Not safe - make it a root gallery
13081    rootGalleries.push(galleryMap[galleryId]);
13082  }
13083} else {
13084  // This is a root gallery (no parent or parent doesn't exist)
13085  if (!childrenAssigned.has(galleryId)) {
13086    rootGalleries.push(galleryMap[galleryId]);
13087  }
13088}
13089});
13090
13091// Final verification
13092const allGalleryIds = new Set(galleries.map(g => g.id));
13093const treeGalleryIds = new Set();
13094
13095// Function to collect all IDs in the tree
13096const collectIds = (items) => {
13097items.forEach(item => {
13098  treeGalleryIds.add(item.id);
13099  if (item.children && item.children.length > 0) {
13100    collectIds(item.children);
13101  }
13102});
13103};
13104
13105collectIds(rootGalleries);
13106
13107// Check if any galleries are missing from the tree
13108const missingIds = [];
13109allGalleryIds.forEach(id => {
13110if (!treeGalleryIds.has(id)) {
13111  missingIds.push(id);
13112}
13113});
13114
13115if (missingIds.length > 0) {
13116console.warn(`${missingIds.length} galleries are missing from the tree structure`);
13117
13118// Add the missing galleries as root items
13119missingIds.forEach(id => {
13120  const gallery = galleryMap[id];
13121  if (gallery && !childrenAssigned.has(id)) {
13122    console.log(`Adding missing gallery to root: ${gallery.title}`);
13123    rootGalleries.push(gallery);
13124  }
13125});
13126}
13127
13128console.log(`Created tree with ${rootGalleries.length} root galleries`);
13129return rootGalleries;
13130}
13131
13132function alignFolderStates() {
13133  console.log("Aligning folder data states with collapsed visual appearance");
13134  let changed = 0;
13135  
13136  galleries.forEach(gallery => {
13137    if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) {
13138      gallery.isCollapsed = true;
13139      changed++;
13140    }
13141  });
13142  
13143}
13144
13145
13146
13147function renderGalleryItem(gallery, level = 0, maxLevel = 10) {
13148  // Prevent infinite recursion by limiting depth
13149  if (level >= maxLevel) {
13150    console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`);
13151    return '';
13152  }
13153
13154  // Add debug info to console
13155  console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`);
13156
13157  // Skip hidden items in view mode (but not spacers)
13158  if (!isEditing && gallery.visible === false && !gallery.isSpacer) {
13159    return '';
13160  }
13161  
13162const isPage = isPageMenuItem(gallery);
13163const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu
13164const isNavigableParent = isNavigableMenuParent(gallery);
13165const isSpacer = gallery.isSpacer === true;
13166const isExternal = gallery.isExternal === true;
13167  
13168// CRITICAL FIX: Ensure children are always populated for folders
13169// If gallery.children is missing or empty, but this is a folder, build children from flat galleries array
13170// This ensures the nested <ol> is always rendered with children in the DOM, even in view mode
13171if (isFolder) {
13172  // Always ensure children array exists
13173  if (!gallery.children) {
13174    gallery.children = [];
13175  }
13176  // If children array is empty, build it from flat galleries array using parentId
13177  if (gallery.children.length === 0) {
13178    const childrenFromFlat = galleries.filter(g => g.parentId === gallery.id);
13179    if (childrenFromFlat.length > 0) {
13180      gallery.children = childrenFromFlat;
13181      console.log(`[renderGalleryItem] Built children for folder ${gallery.title}: ${gallery.children.length} children`);
13182    }
13183  }
13184}
13185
13186// Parent-capable items need their chevron and empty nested drop zone immediately,
13187// before the first child has been added.
13188const hasChildren = isFolder || isNavigableParent || gallery.children?.length > 0;
13189const hasChildrenClass = hasChildren ? 'has-children' : '';
13190
13191// Standard collapsed state check
13192const isCollapsed = gallery.isCollapsed === true;
13193
13194// Simple expanded class logic - if the folder is not collapsed, add 'expanded' class
13195const isExpandedClass = (isFolder || isNavigableParent) && !isCollapsed ? 'expanded' : '';
13196
13197// Add additional classes for special types
13198const spacerClass = isSpacer ? 'spacer-item' : '';
13199const externalClass = isExternal ? 'external-link' : '';
13200const folderClass = isFolder ? 'folder-item' : '';
13201const navigableParentClass = isNavigableParent ? 'navigable-parent-item' : '';
13202  
13203        const duplicateIconSvg = `
13204    <svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" width="24" height="24" viewBox="0 0 24 24">
13205      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
13206        <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"/>
13207        <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"/>
13208      </g>
13209    </svg>
13210  `;
13211
13212
13213  
13214  // NEW: Add nesting level as a data attribute for styling
13215  const nestingLevelAttr = `data-nesting-level="${level}"`;
13216
13217  // Accessible href for SPA items (loadPage/loadGallery still preventDefault)
13218  const menuHref = gallery.isHomePage
13219    ? '/'
13220    : (gallery.url && String(gallery.url).startsWith('/')
13221        ? gallery.url
13222        : (gallery.slug ? ('/' + gallery.slug) : '#'));
13223  const childrenListId = 'gallery-children-' + gallery.id;
13224  const folderExpandedAttr = !isCollapsed ? 'true' : 'false';
13225  const ariaCurrentAttr = (gallery.id === activeGalleryId && !isFolder && !isSpacer)
13226    ? ' aria-current="page"'
13227    : '';
13228
13229  // Create the gallery item HTML with enhanced nesting indicators
13230  const html = `
13231<li data-id="${gallery.id}" 
13232    ${nestingLevelAttr}
13233    class="${gallery.id === activeGalleryId ? 'active' : ''} 
13234          ${gallery.visible === false ? 'hidden-gallery' : ''} 
13235          ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass} ${navigableParentClass}">
13236  <div class="gallery-item-content nested-sortable-handle">
13237    ${hasChildren ? `
13238      <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;')}">
13239        <svg class="icon toggle-icon ${!isCollapsed ? 'rotated' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
13240          <polyline points="9 6 15 12 9 18"></polyline>
13241        </svg>
13242      </button>
13243    ` : ''}
13244    
13245    <!-- Menu item with different rendering based on type -->
13246    ${isSpacer ? `
13247      <!-- Spacer with non-breaking space to ensure height -->
13248      <span class="menu-item spacer" aria-hidden="true">&nbsp;</span>
13249    ` : isExternal ? `
13250      <!-- External URL uses an anchor tag with target="_blank" -->
13251      <a href="${gallery.url}" class="menu-item external-link" target="_blank" rel="noopener noreferrer"${ariaCurrentAttr}>
13252        ${gallery.title}
13253      </a>
13254    ` : isFolder ? `
13255      <!-- Folder item with toggle functionality (button for keyboard / SR) -->
13256      <button type="button" class="menu-item folder" data-folder="true"
13257            onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}">
13258        ${gallery.title}
13259      </button>
13260    ` : `
13261      <!-- Real link for SR/keyboard; SPA handlers keep click behavior -->
13262      <a href="${menuHref}" class="menu-item" onclick="${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}"${ariaCurrentAttr}>
13263        ${gallery.title}
13264      </a>
13265    `}
13266    
13267<div class="controls">
13268      ${!isSpacer ? `
13269      <button class="btn visibility-toggle ${gallery.visible === false ? 'hidden' : ''}" onclick="toggleVisibility(${gallery.id}, event)">
13270        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13271          ${gallery.visible !== false ? 
13272            '<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>' :
13273            '<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>'
13274          }
13275        </svg>
13276      </button>
13277      
13278      <button class="btn password-toggle ${gallery.passwordProtected ? 'protected' : ''}" onclick="toggleGalleryPasswordProtection(${gallery.id}, event)" title="${gallery.passwordProtected ? 'Remove Password Protection' : 'Add Password Protection'}">
13279        <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24">
13280          ${gallery.passwordProtected ? 
13281            '<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>' :
13282            '<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"/>'
13283          }
13284        </svg>
13285      </button>
13286      
13287      <button class="btn edit-btn" onclick="editGallery(${gallery.id}, event)">
13288        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13289          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
13290          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
13291        </svg>
13292      </button>
13293      ` : ''}
13294
13295        <button class="btn duplicate-btn" onclick="duplicateGalleryItem(${gallery.id}, event)" title="Duplicate">
13296         ${duplicateIconSvg}
13297      </button>
13298
13299      
13300      <button class="btn delete-btn" onclick="deleteGallery(${gallery.id}, event)">
13301        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13302          <polyline points="3 6 5 6 21 6"></polyline>
13303          <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>
13304        </svg>
13305      </button>
13306${!isSpacer ? `
13307<!-- Add home toggle button -->
13308<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'}">
13309  <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
13310    ${gallery.isHomePage ? 
13311      '<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"/>' :
13312      '<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>'
13313    }
13314  </svg>
13315</button>
13316` : ''}
13317    </div>
13318  </div>
13319  <ol id="${childrenListId}" class="nested-sortable ${level > 0 ? 'nested-level-' + level : ''}">
13320    ${gallery.children && gallery.children.length > 0 ? 
13321      gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''}
13322  </ol>
13323</li>
13324`;
13325
13326  return html;
13327}
13328
13329// Additional helper function to add visual indicators during drag operations
13330function addDragVisualFeedback() {
13331  // Add event listener for drag operations
13332  document.addEventListener('dragover', function(e) {
13333    // Find the closest nested sortable list
13334    const nearestList = e.target.closest('.nested-sortable');
13335    if (!nearestList) return;
13336    
13337    // Remove active-drop-target class from all lists
13338    document.querySelectorAll('.active-drop-target').forEach(el => {
13339      el.classList.remove('active-drop-target');
13340    });
13341    
13342    // Add it to the current list
13343    nearestList.classList.add('active-drop-target');
13344    
13345    // Calculate if this would be a nested position
13346    // This is a simplified calculation - SortableJS has its own logic,
13347    // but this helps provide additional visual feedback
13348    const rect = nearestList.getBoundingClientRect();
13349    const distanceFromLeft = e.clientX - rect.left;
13350    
13351    // If we're close to the left edge of a nested list, add a class
13352    if (distanceFromLeft < 30) {
13353      nearestList.classList.add('potential-parent');
13354    } else {
13355      nearestList.classList.remove('potential-parent');
13356    }
13357  });
13358  
13359  // Remove classes when drag ends
13360  document.addEventListener('dragend', function() {
13361    document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => {
13362      el.classList.remove('active-drop-target', 'potential-parent');
13363    });
13364  });
13365}
13366
13367// Make sure the addDragVisualFeedback function is called when the document is ready
13368document.addEventListener('DOMContentLoaded', function() {
13369  // Initialize the visual feedback helper
13370  addDragVisualFeedback();
13371  
13372  // If we're in edit mode, make sure the nested sortables are initialized
13373  if (document.body.classList.contains('edit-mode-active')) {
13374    setTimeout(function() {
13375      initializeNestedSortables();
13376    }, 300);
13377  }
13378});
13379
13380// Update the toggleEditMode function to ensure proper initialization of sortables
13381const originalToggleEditMode = window.toggleEditMode;
13382window.toggleEditMode = function() {
13383    originalToggleEditMode.apply(this, arguments); // Call original function
13384
13385    stopAutoAdvanceTimer();
13386    
13387    const sidebar = document.querySelector('.sidebar');
13388    const editControls = document.querySelector('.edit-controls');
13389
13390    if (currentMenuLayout === 'horizontal') {
13391        if (document.body.classList.contains('edit-mode-active')) {
13392            // In horizontal layout and edit mode is ON: show sidebar and edit controls
13393            if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container
13394            if(editControls) editControls.style.display = 'flex';
13395             // Re-render the sidebar menu for editing
13396            renderGalleries();
13397        } else {
13398            // In horizontal layout and edit mode is OFF: hide sidebar
13399            if(sidebar) sidebar.style.display = 'none';
13400             // Re-render the horizontal menu for viewing
13401            renderHorizontalMenu();
13402        }
13403    } else {
13404        // For other layouts (sidebar, top), ensure sidebar is visible
13405        if(sidebar) sidebar.style.display = 'block'; // Or 'flex'
13406    }
13407};
13408
13409function verifyGalleryStructure() {
13410console.log("Verifying gallery structure integrity...");
13411
13412// Count number of galleries
13413const galleryCount = galleries.length;
13414console.log(`Total galleries in flat array: ${galleryCount}`);
13415
13416// Create tree and count galleries in tree
13417const tree = createGalleryTree([...galleries]);
13418let treeCount = 0;
13419
13420function countGalleriesInTree(items) {
13421items.forEach(item => {
13422  treeCount++;
13423  if (item.children && item.children.length > 0) {
13424    countGalleriesInTree(item.children);
13425  }
13426});
13427}
13428
13429countGalleriesInTree(tree);
13430console.log(`Total galleries in tree structure: ${treeCount}`);
13431
13432if (treeCount !== galleryCount) {
13433console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`);
13434return false;
13435} else {
13436console.log("Gallery structure is consistent");
13437return true;
13438}
13439}
13440
13441function renderGalleries() {
13442console.log("Rendering galleries...");
13443
13444  // Align with the explicit edit toggle only (not admin login / stale sidebar.editing)
13445  if (document.body.classList.contains('edit-mode-active')) {
13446    isEditing = true;
13447  } else if (typeof window.isEditing === 'boolean') {
13448    isEditing = window.isEditing;
13449  }
13450
13451  var galleriesSource = galleries;
13452  if (window.HelpSystem && typeof window.HelpSystem.getRenderGalleries === 'function') {
13453    var helpList = window.HelpSystem.getRenderGalleries();
13454    if (helpList) galleriesSource = helpList;
13455  }
13456
13457try {
13458// Only fix circular references if absolutely necessary
13459const foundCircular = fixCircularReferences();
13460
13461// Only create a new tree if we had to fix circular references
13462let galleryTree;
13463if (foundCircular) {
13464  console.log("Creating fresh gallery tree after fixing circular references");
13465  galleryTree = createGalleryTree(galleriesSource);
13466} else {
13467  console.log("Using existing structure to create gallery tree");
13468  galleryTree = createGalleryTree(galleriesSource);
13469}
13470
13471// Render the tree - IMPORTANT: Add null check
13472const tree = document.getElementById('galleryTree');
13473if (!tree) {
13474  console.warn('galleryTree element not found, cannot render galleries');
13475  return;  // Exit the function if the element doesn't exist
13476}
13477
13478// Render the tree with maximum depth protection
13479tree.innerHTML = `
13480  <ol class="nested-sortable">
13481    ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')}
13482  </ol>
13483`;
13484
13485// Initialize sortable for all nested sortable elements
13486if (isEditing) {
13487  // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables
13488  // This prevents drag issues when items are added and immediately dragged
13489  requestAnimationFrame(() => {
13490    initializeNestedSortables();
13491  });
13492}
13493
13494console.log("Galleries rendered successfully");
13495
13496// Set up folder toggle handler after rendering (works in both view and edit modes)
13497setupFolderToggleHandler();
13498
13499// Verify structure integrity after rendering
13500verifyGalleryStructure();
13501
13502if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y();
13503} catch (error) {
13504console.error("Error rendering galleries:", error);
13505}
13506}
13507
13508/** Prefer visible menu links for AT; keep SEO nav in DOM for crawlers. */
13509function syncSeoNavA11y() {
13510  const seoNav = document.querySelector('.hydra-seo-nav');
13511  if (!seoNav) return;
13512  const hasVisibleLinks =
13513    document.querySelectorAll('#galleryTree a.menu-item').length > 0 ||
13514    document.querySelectorAll('.horizontal-menu-container a.horizontal-menu-label').length > 0;
13515  const hideFromAt = hasVisibleLinks;
13516  seoNav.setAttribute('aria-hidden', hideFromAt ? 'true' : 'false');
13517  // aria-hidden alone is not enough — focusable descendants must leave the tab order
13518  seoNav.querySelectorAll('a, button, [tabindex]').forEach((el) => {
13519    if (hideFromAt) {
13520      if (!el.hasAttribute('data-seo-prev-tabindex')) {
13521        el.setAttribute('data-seo-prev-tabindex', el.hasAttribute('tabindex') ? el.getAttribute('tabindex') : '');
13522      }
13523      el.setAttribute('tabindex', '-1');
13524    } else {
13525      const prev = el.getAttribute('data-seo-prev-tabindex');
13526      if (prev === null) return;
13527      if (prev === '') el.removeAttribute('tabindex');
13528      else el.setAttribute('tabindex', prev);
13529      el.removeAttribute('data-seo-prev-tabindex');
13530    }
13531  });
13532}
13533
13534function getMenuItemHref(galleryItem) {
13535  if (!galleryItem) return '#';
13536  if (galleryItem.isExternal && galleryItem.url) return galleryItem.url;
13537  if (galleryItem.isHomePage) return '/';
13538  if (galleryItem.url && String(galleryItem.url).startsWith('/')) return galleryItem.url;
13539  if (galleryItem.slug) return '/' + galleryItem.slug;
13540  return '#';
13541}
13542
13543
13544function fixCircularReferences() {
13545console.log("Selectively checking for circular references...");
13546
13547// Only process galleries that need checking:
13548// 1. Imported galleries (they're new and might cause issues)
13549// 2. Galleries with extremely deep nesting
13550// 3. Actual circular references
13551
13552const processed = new Set();
13553
13554// First pass: Identify actual circular references without modifying structure
13555const circularIds = new Set();
13556const suspiciousIds = new Set();
13557
13558galleries.forEach(gallery => {
13559if (!gallery || !gallery.id) return;
13560
13561// Skip galleries we've already processed
13562if (processed.has(gallery.id)) return;
13563
13564// Check for self-reference - this is always wrong
13565if (gallery.parentId === gallery.id) {
13566  console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`);
13567  circularIds.add(gallery.id);
13568  return;
13569}
13570
13571// Skip galleries without parents
13572if (!gallery.parentId) {
13573  processed.add(gallery.id);
13574  return;
13575}
13576
13577// Follow the parent chain to detect circular references
13578const parentChain = [gallery.id];
13579let currentId = gallery.parentId;
13580let loopCount = 0;
13581let foundCircular = false;
13582
13583while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops
13584  loopCount++;
13585  
13586  // If we've seen this ID before, it's a circular reference
13587  if (parentChain.includes(currentId)) {
13588    console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`);
13589    circularIds.add(gallery.id);
13590    foundCircular = true;
13591    break;
13592  }
13593  
13594  parentChain.push(currentId);
13595  
13596  // Move up to the next parent
13597  const parent = galleries.find(g => g.id === currentId);
13598  currentId = parent?.parentId;
13599  
13600  // If we can't find the parent, we can stop
13601  if (!parent) break;
13602}
13603
13604// If we hit our loop limit but didn't find a circular reference,
13605// it's suspicious but not definitely circular
13606if (loopCount >= 20 && !foundCircular) {
13607  console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`);
13608  suspiciousIds.add(gallery.id);
13609}
13610
13611// Mark this gallery and all its ancestors as processed
13612parentChain.forEach(id => processed.add(id));
13613});
13614
13615// Second pass: Only fix the identified circular references
13616if (circularIds.size > 0) {
13617console.log(`Fixing ${circularIds.size} confirmed circular references`);
13618
13619galleries.forEach(gallery => {
13620  if (circularIds.has(gallery.id)) {
13621    console.log(`Breaking circular reference for gallery: ${gallery.title}`);
13622    gallery.parentId = null;
13623  }
13624});
13625} else {
13626console.log("No circular references found");
13627}
13628
13629// For suspicious galleries, don't automatically fix them
13630if (suspiciousIds.size > 0) {
13631console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`);
13632}
13633
13634return circularIds.size > 0; // Return true if we fixed anything
13635}
13636
13637// Deep linking implementation that uses the existing slug property
13638
13639// Function to get the slug for a gallery
13640function getGallerySlug(gallery) {
13641// First try to use the existing slug property
13642if (gallery.slug) {
13643console.log(`Using existing gallery slug: ${gallery.slug}`);
13644return gallery.slug;
13645}
13646
13647// If no slug exists, generate one from the title as fallback
13648if (gallery.title) {
13649const generatedSlug = gallery.title
13650  .toLowerCase()
13651  .replace(/[^ws-]/g, '')
13652  .replace(/s+/g, '-')
13653  .replace(/--+/g, '-')
13654  .trim();
13655  
13656console.log(`Generated slug from title: ${generatedSlug}`);
13657return generatedSlug || 'gallery';
13658}
13659
13660// Default fallback
13661console.warn('Gallery has no slug or title, using default');
13662return 'gallery';
13663}
13664
13665// Function to update the URL when a gallery is loaded
13666// Update the URL when a gallery is loaded
13667function updateURLWithGallerySlug(gallery) {
13668  if (!gallery) return;
13669
13670  // Keep bookmarkable /_manage URL — do not rewrite to / or /slug when loading galleries/pages
13671  const __hydraPath = window.location.pathname;
13672  if (__hydraPath === '/_manage' || __hydraPath === '/_manage/') {
13673    console.log('[updateURLWithGallerySlug] On /_manage editor path; skipping slug URL update.');
13674    return;
13675  }
13676
13677  // Get the slug for this gallery/page
13678  let slug = gallery.slug;
13679
13680  if (!slug && gallery.title) {
13681    slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim();
13682  }
13683
13684  // Check if we're in preview mode (add this new code)
13685  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13686  
13687  // Check if current history state already has this galleryId
13688  const currentState = window.history.state;
13689  const currentPath = window.location.pathname;
13690  const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && 
13691                               ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug));
13692  
13693  if (isPreviewMode) {
13694    // Extract GUID from current URL path - first segment after domain
13695    const pathParts = window.location.pathname.split('/').filter(Boolean);
13696    const siteGuid = pathParts[0];
13697    
13698    // Create history entry if we have a valid GUID, and it's not already the current state.
13699    // Home pages use /{guid}/ (canonical preview home), matching live's '/' special-case.
13700    if (siteGuid && (gallery.isHomePage || slug)) {
13701      const targetPath = gallery.isHomePage ? ('/' + siteGuid + '/') : ('/' + siteGuid + '/' + slug);
13702      const alreadyAtPreviewHome = gallery.isHomePage && (
13703        currentPath === '/' + siteGuid || currentPath === '/' + siteGuid + '/'
13704      );
13705      // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13706      if ((!alreadyAtPreviewHome && currentPath !== targetPath) || !isAlreadyCurrentPage) {
13707        console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id);
13708        
13709        // Update browser URL without reloading, preserving the GUID
13710        window.history.pushState(
13711          { galleryId: gallery.id }, 
13712          gallery.title || 'Gallery', 
13713          targetPath
13714        );
13715        
13716        // Update page title
13717        document.title = formatGalleryDocumentTitle(gallery.title);
13718      } else {
13719        console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)');
13720      }
13721    }
13722    
13723    return; // Exit early, we've handled the preview case
13724  }
13725  
13726  // Regular (non-preview) URL handling
13727  if (slug) {
13728    // If this gallery is the designated home page, keep the URL as '/' (canonical home URL)
13729    const targetPath = gallery.isHomePage ? '/' : '/' + slug;
13730    // Only push if URL is different OR if state doesn't match (allows multiple navigations)
13731    if (currentPath !== targetPath || !isAlreadyCurrentPage) {
13732      console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id);
13733      window.history.pushState(
13734        { galleryId: gallery.id }, 
13735        gallery.title || 'Gallery', 
13736        targetPath
13737      );
13738      document.title = formatGalleryDocumentTitle(gallery.title);
13739    } else {
13740      console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update');
13741    }
13742  }
13743}
13744
13745
13746function loadHomePage() {
13747  // Find a gallery marked as home page (regardless of visibility)
13748  const homePage = galleries.find(gallery => gallery.isHomePage === true);
13749  
13750  if (homePage) {
13751    console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`);
13752    
13753    // Update active gallery ID
13754    activeGalleryId = homePage.id;
13755    
13756    // Add a special flag to indicate we're loading the home page
13757    // This allows us to bypass visibility checks
13758    window._loadingHomePage = true;
13759    
13760    // Determine if this is a page or gallery and load appropriately
13761    if (homePage.isPage) {
13762      loadPage(homePage.id);
13763    } else {
13764      loadGallery(homePage.id);
13765    }
13766    
13767    // Clear the flag after loading
13768    setTimeout(() => {
13769      window._loadingHomePage = false;
13770    }, 100);
13771    
13772    // Update UI state
13773    updateActiveStates();
13774    updateMobileTitle();
13775    if (typeof closeMobileMenu === 'function') closeMobileMenu();
13776    
13777    return true;
13778  }
13779  
13780  console.log('No home page defined');
13781  return false;
13782}
13783
13784// 4. Update the handleURLNavigation function to check for home page
13785// Find the existing handleURLNavigation function and modify as follows:
13786// In index.ts
13787function handleURLNavigation() {
13788  const path = window.location.pathname.substring(1); // Remove leading slash
13789  const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
13790  let slug = path;
13791  let siteGuid = null;
13792
13793  // /_manage is the Hydra editor entry — treat like site root for slug resolution (not a gallery slug)
13794  if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
13795    console.log('[handleURLNavigation] /_manage detected — using root navigation behavior');
13796    slug = '';
13797  }
13798
13799  if (isPreviewMode && path) {
13800    const pathParts = path.split('/');
13801    if (pathParts.length >= 1) {
13802        siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug
13803    }
13804    if (pathParts.length >= 2) {
13805      slug = pathParts[1]; // Slug is the part after GUID
13806      console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`);
13807    } else {
13808      // Only GUID is present, or path is empty after GUID
13809      slug = ''; // Treat as root/home for the given GUID
13810      console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`);
13811    }
13812  }
13813
13814  if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/')
13815    console.log("No specific slug in path (root or GUID-only preview), checking for home page.");
13816    if (!loadHomePage() && galleries.length > 0) {
13817      const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13818      if (firstVisibleGallery) {
13819        console.log("No home page, loading first visible item:", firstVisibleGallery.title);
13820        if (firstVisibleGallery.isPage) {
13821            loadPage(firstVisibleGallery.id);
13822        } else {
13823            loadGallery(firstVisibleGallery.id);
13824        }
13825      } else {
13826        console.log("No home page and no visible items to load for this path.");
13827      }
13828    }
13829  } else {
13830    console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : ""));
13831    // findGalleryByPath should be able to find the item using the slug,
13832    // handling preview mode internally if necessary or by being passed the correct slug.
13833    const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid);
13834
13835    if (matchingGallery) {
13836      console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')');
13837      console.log('🔍 [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements)));
13838      
13839      if (matchingGallery.isSubmenu) {
13840        console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.');
13841        // Potentially, you might want to find and expand its parent here if the UI supports it.
13842      } else {
13843        // Set activeGalleryId *before* calling loadPage/loadGallery
13844        activeGalleryId = matchingGallery.id;
13845        window.activeGalleryId = matchingGallery.id;
13846
13847        // Determine if this is a page by checking multiple indicators
13848        // Check isPage flag, pageId, or pageElements array
13849        const isPage = matchingGallery.isPage === true || 
13850                       (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) ||
13851                       (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0);
13852        
13853        if (isPage) {
13854          console.log('🔍 [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"');
13855          const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
13856          pageLoader(matchingGallery.id);
13857        } else {
13858          console.log('🔍 [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"');
13859          const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
13860          galleryLoader(matchingGallery.id, null, { forceReload: true });
13861        }
13862      }
13863    } else {
13864      console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`);
13865      if (!loadHomePage()) { // Try loading home page
13866        const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu);
13867        if (firstVisibleGallery) {
13868          console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`);
13869           if (firstVisibleGallery.isPage) {
13870               loadPage(firstVisibleGallery.id);
13871           } else {
13872               loadGallery(firstVisibleGallery.id);
13873           }
13874        } else {
13875            console.log("No matching gallery, no home page, and no visible items to load.");
13876            // Optionally, display a 404 message in the gallery-container
13877            const galleryContainer = document.querySelector('.gallery-container');
13878            if (galleryContainer) {
13879                galleryContainer.innerHTML = '<div>Page not found.</div>';
13880                galleryContainer.style.opacity = '1';
13881            }
13882        }
13883      }
13884    }
13885  }
13886
13887  // Update UI states after initiating load.
13888  // These functions should ideally use the now-set activeGalleryId.
13889  if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree
13890  if (typeof updateMobileTitle === 'function') updateMobileTitle();   // For mobile header
13891
13892  // **** ADDED FOR DEEP LINKING HORIZONTAL MENU ****
13893  // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load)
13894  let currentLayout = 'sidebar'; // Default assumption
13895  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
13896    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
13897  } else {
13898    // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings
13899    if (document.body.classList.contains('menu-layout-horizontal')) {
13900        currentLayout = 'horizontal';
13901    }
13902  }
13903
13904  console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`);
13905
13906  if (currentLayout === 'horizontal') {
13907    if (typeof updateActiveStatesHorizontal === 'function') {
13908      console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation.");
13909      // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call.
13910      // A small delay might be needed, or ensure renderHorizontalMenu has run.
13911      // For now, call it directly. If items aren't there, it won't do anything harmful.
13912      setTimeout(() => { // Add a slight delay to allow menu rendering
13913          updateActiveStatesHorizontal();
13914      }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered.
13915    } else {
13916        console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout.");
13917    }
13918  }
13919}
13920
13921/**
13922* Enhanced loadPage function that properly cleans up gallery content
13923* @param {number} id - The page ID
13924* @param {Event} event - Optional event object
13925*/
13926window.loadPage = function(id, event) {
13927  if (event && event.target && event.target.isConnected === false) {
13928    console.log('🔍 [loadPage] Ignoring navigation from detached element');
13929    return;
13930  }
13931  if (event) {
13932    event.preventDefault();
13933    event.stopPropagation();
13934    const now = Date.now();
13935    const lastCallTime = window._lastPageLoadTime || 0;
13936    window._lastPageLoadTime = now;
13937    if (now - lastCallTime < 100) {
13938      console.log('Ignoring duplicate loadPage call');
13939      return;
13940    }
13941  }
13942  console.error('🔍 [loadPage] Loading page with gallery ID:', id);
13943
13944  stopAutoAdvanceTimer();
13945  if (typeof dismissGalleryPasswordOverlay === 'function') {
13946    dismissGalleryPasswordOverlay();
13947  }
13948
13949  let galleriesData = window.galleries || galleries;
13950  const gallery = findGalleryById(galleriesData, id);
13951
13952  if (!gallery) {
13953    console.warn('No gallery found with ID:', id, 'for page load.');
13954    return;
13955  }
13956
13957  const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`);
13958
13959  window.activeGalleryId = id;
13960  activeGalleryId = id;
13961  window._activeContentType = 'page';
13962  if (window.EnhancedGallerySystem) {
13963    window.EnhancedGallerySystem.currentGalleryId = null;
13964  }
13965
13966  if (typeof window.removeGalleryScriptsWithPause === 'function') {
13967    window.removeGalleryScriptsWithPause();
13968  }
13969
13970  const galleryContainer = document.querySelector('.gallery-container');
13971  if (galleryContainer) {
13972    // Clear the container but ensure it's visible
13973    galleryContainer.innerHTML = '';
13974    galleryContainer.style.display = 'block';
13975    galleryContainer.style.visibility = 'visible';
13976    galleryContainer.style.opacity = '1';
13977    console.log('🔍 [loadPage] Cleared and ensured gallery-container is visible');
13978  } else {
13979    console.error('🔍 [loadPage] gallery-container not found!');
13980  }
13981
13982  if (!gallery.pageId) gallery.pageId = `page_${id}`;
13983  if (!gallery.isPage) gallery.isPage = true;
13984
13985  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
13986    window.syncGalleryOptionsButtonVisibility();
13987  }
13988
13989  if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {};
13990  window._pageIdToGalleryId[gallery.pageId] = id;
13991
13992  const seedPageElementsOntoManager = function() {
13993    if (gallery.pageElements && Array.isArray(gallery.pageElements)) {
13994      if (window.PageManager && window.PageManager.elements) {
13995        window.PageManager.elements[gallery.pageId] = [...gallery.pageElements];
13996      }
13997    } else if (window.PageManager && window.PageManager.elements &&
13998               window.PageManager.elements[gallery.pageId] &&
13999               window.PageManager.elements[gallery.pageId].length > 0) {
14000      gallery.pageElements = [...window.PageManager.elements[gallery.pageId]];
14001    }
14002  };
14003  seedPageElementsOntoManager();
14004
14005  const invokePageManagerLoad = function() {
14006    seedPageElementsOntoManager();
14007    try {
14008      console.error('🔍 [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId);
14009      console.error('🔍 [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0);
14010      console.error('🔍 [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0);
14011      console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId]));
14012      
14013      // Small delay to ensure DOM is ready, especially when navigating back via history
14014      setTimeout(() => {
14015        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
14016          console.log('🔍 [loadPage] Skipping stale delayed PageManager load for:', gallery.pageId);
14017          return;
14018        }
14019
14020        // Verify container still exists
14021        const containerCheck = document.querySelector('.gallery-container');
14022        console.error('🔍 [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer);
14023        
14024        // PageManager.loadPage is async - we need to properly handle it
14025        try {
14026          const loadPromise = window.PageManager.loadPage(gallery.pageId);
14027          console.error('🔍 [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function');
14028          
14029          if (loadPromise && typeof loadPromise.then === 'function') {
14030            loadPromise.then(() => {
14031              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
14032                console.log('🔍 [loadPage] Ignoring stale PageManager completion for:', gallery.pageId);
14033                return;
14034              }
14035
14036              console.error('🔍 [loadPage] PageManager.loadPage promise resolved');
14037              if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
14038                window.syncGalleryOptionsButtonVisibility();
14039              }
14040              // Ensure container visibility after PageManager has initialized
14041              if (galleryContainer) {
14042                const pageContainer = galleryContainer.querySelector('.page-container');
14043                if (pageContainer) {
14044                  pageContainer.style.display = 'block';
14045                  pageContainer.style.visibility = 'visible';
14046                  pageContainer.style.opacity = '1';
14047                  const elementCount = pageContainer.querySelectorAll('.page-element').length;
14048                  console.error('🔍 [loadPage] Page container found and made visible, has', elementCount, 'elements');
14049                  if (elementCount === 0) {
14050                    console.error('🔍 [loadPage] WARNING: Page container exists but has no elements!');
14051                    console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]);
14052                  }
14053                } else {
14054                  console.error('❌ [loadPage] Page container not found after PageManager.loadPage');
14055                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
14056                  console.error('❌ [loadPage] Gallery container children:', galleryContainer.children.length);
14057                }
14058              } else {
14059                console.error('❌ [loadPage] Gallery container is null after PageManager.loadPage');
14060              }
14061            }).catch((error) => {
14062              console.error('❌ [loadPage] Error in PageManager.loadPage promise:', error);
14063              console.error('❌ [loadPage] Error stack:', error.stack);
14064            });
14065          } else {
14066            // If loadPage doesn't return a promise, check after a delay
14067            console.error('🔍 [loadPage] loadPage did not return a promise, checking after delay');
14068            setTimeout(() => {
14069              if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
14070                console.log('🔍 [loadPage] Ignoring stale non-promise PageManager completion for:', gallery.pageId);
14071                return;
14072              }
14073              if (galleryContainer) {
14074                const pageContainer = galleryContainer.querySelector('.page-container');
14075                if (pageContainer) {
14076                  pageContainer.style.display = 'block';
14077                  pageContainer.style.visibility = 'visible';
14078                  pageContainer.style.opacity = '1';
14079                  console.error('🔍 [loadPage] Ensured page-container is visible (non-promise path)');
14080                } else {
14081                  console.error('❌ [loadPage] Page container not found (non-promise path)');
14082                  console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500));
14083                }
14084              }
14085            }, 200);
14086          }
14087        } catch (loadError) {
14088          console.error('❌ [loadPage] Exception calling PageManager.loadPage:', loadError);
14089          console.error('❌ [loadPage] Error stack:', loadError.stack);
14090        }
14091        
14092        const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false;
14093        if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') {
14094          window.PageManager.setEditMode(isCurrentlyEditing);
14095        }
14096      }, 50); // Small delay to ensure DOM is ready
14097    } catch (error) {
14098      console.error('❌ [loadPage] Error in loadPage function:', error);
14099      console.error('❌ [loadPage] Error stack:', error.stack);
14100    }
14101  };
14102
14103  if (window.PageManager && typeof window.PageManager.loadPage === 'function') {
14104    invokePageManagerLoad();
14105  } else {
14106    console.error('❌ [loadPage] PageManager not found yet — waiting for CDN proxy script');
14107    console.error('❌ [loadPage] window.PageManager:', !!window.PageManager);
14108    console.error('❌ [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A');
14109    whenPageManagerReady(function() {
14110      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) {
14111        console.log('🔍 [loadPage] Skipping stale PageManager ready retry for:', gallery.pageId);
14112        return;
14113      }
14114      console.log('✅ [loadPage] PageManager became available — retrying page render');
14115      invokePageManagerLoad();
14116    });
14117  }
14118
14119  // Apply menu visibility
14120  const bodyEl = document.body;
14121  if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
14122      bodyEl.classList.add('menu-hidden-on-page');
14123  } else {
14124      bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
14125  }
14126  applyMenuOverlayClassForGallery(gallery);
14127
14128  if (typeof updateActiveStates === 'function') updateActiveStates();
14129  if (typeof updateMobileTitle === 'function') updateMobileTitle();
14130  if (typeof closeMobileMenu === 'function') closeMobileMenu();
14131  // Close the gallery options panel when navigating to a different page
14132  if (typeof window.closeOptionsPanel === 'function') {
14133    window.closeOptionsPanel();
14134  }
14135  if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery);
14136  if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
14137    window.syncGalleryOptionsButtonVisibility();
14138  }
14139  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
14140    window.dispatchEvent(new CustomEvent('page-loaded', {
14141      detail: { pageId: gallery.pageId, galleryId: id }
14142    }));
14143  }
14144}
14145
14146
14147
14148/**
14149* Ensures all required gallery styles are loaded
14150*/
14151function ensureGalleryStylesLoaded() {
14152window.ensureGalleryStylesLoaded = ensureGalleryStylesLoaded;
14153// Use Worker route for gallery CSS (avoids CDN SSL issues)
14154const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14155const requiredStyles = [
14156{ id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' },
14157{ id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' }
14158];
14159console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css');
14160
14161requiredStyles.forEach(style => {
14162if (!document.getElementById(style.id)) {
14163  const link = document.createElement('link');
14164  link.id = style.id;
14165  link.rel = 'stylesheet';
14166  link.href = style.href;
14167  if (style.href.includes('cdn.neonsky.app')) {
14168    link.onerror = function() {
14169      console.error('❌ Gallery CSS failed, retrying via proxy:', link.href);
14170      // Set flag if not already set
14171      if (!window.useCdnProxy) {
14172        window.useCdnProxy = true;
14173        console.error('🚩 CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests');
14174      }
14175      const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14176      link.href = proxyUrl;
14177      link.onerror = function() {
14178        console.error('❌ Proxy failed, trying storage:', proxyUrl);
14179        link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
14180      };
14181    };
14182  }
14183  document.head.appendChild(link);
14184  console.log(`Added gallery style: ${style.id}`);
14185}
14186});
14187}
14188
14189
14190/**
14191* Ensures all required gallery scripts are loaded
14192* @returns {Promise} A promise that resolves when all scripts are loaded
14193*/
14194function ensureGalleryScriptsLoaded() {
14195const requiredScripts = [
14196{ id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js' },
14197{ id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' },
14198          { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260923-002.js' }
14199];
14200
14201const promises = requiredScripts.map(script => {
14202return new Promise((resolve, reject) => {
14203  // If script is already loaded, resolve immediately
14204  if (document.getElementById(script.id)) {
14205    resolve();
14206    return;
14207  }
14208  
14209  // Create and load the script
14210  const scriptElement = document.createElement('script');
14211  scriptElement.id = script.id;
14212  scriptElement.src = script.src;
14213  scriptElement.onload = resolve;
14214  scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`));
14215  document.head.appendChild(scriptElement);
14216});
14217});
14218
14219return Promise.all(promises);
14220}
14221
14222/**
14223 * Creates a clean gallery context without using an iframe
14224 * @param {Object} gallery - The gallery object
14225 * @param {HTMLElement} container - The container to load the gallery into
14226 */
14227function createGalleryContext(gallery, container) {
14228  console.log('Creating gallery context for:', gallery.title);
14229
14230  // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script
14231  // This ensures styles are available even if NDJSON fails and falls back to classic JSON
14232  ensureGalleryStylesLoaded();
14233
14234  // Generate the page alias safely
14235  const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-');
14236
14237  // Get our gallery options
14238  const galleryOptions = gallery.galleryOptions || {};
14239
14240  // Get token data
14241  let authToken = null;
14242  // First try TokenManager if available
14243  if (window.TokenManager && typeof window.TokenManager.getToken === 'function') {
14244    authToken = window.TokenManager.getToken();
14245  } 
14246  // Then try global token
14247  else if (window.didToken) {
14248    authToken = window.didToken;
14249  }
14250
14251  // Set up Parameters for the gallery
14252  const galleryParameters = {
14253    SiteAlias: window.location.hostname,
14254    InitialPageUuid: gallery.pageId,
14255    InitialPageAlias: pageAlias,
14256    isInEditor: window.isEditing || false,
14257    siteId: window.siteId || '',
14258    isHydra: true
14259  };
14260
14261  // If we have a token, add it
14262  if (authToken) {
14263    galleryParameters.hydraAuthToken = authToken;
14264  }
14265
14266  // Preserve original window.Parameters
14267  const originalParameters = window.Parameters;
14268
14269  // REMOVED: Loading indicator - no longer showing the loading animation
14270
14271  // Load the main gallery script - Use Worker route (avoids CDN SSL issues)
14272  const galleryScript = document.createElement('script');
14273  const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14274  galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js';
14275  console.log('Loading gallery-main script from Worker:', galleryScript.src);
14276  galleryScript.onerror = function() {
14277    console.error('❌ Gallery script failed, retrying via proxy:', galleryScript.src);
14278    const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14279    galleryScript.src = proxyUrl;
14280    galleryScript.onerror = function() {
14281      console.error('❌ Proxy failed, trying storage:', proxyUrl);
14282      galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14283    };
14284  };
14285
14286  // Setup the gallery configuration
14287  const neonGalleryConfig = {
14288    useData: true,
14289    useCDN: true,
14290    version: 'live',
14291    manualCollectionName: "mod",
14292    layoutType: "grid",
14293    ...galleryOptions,
14294    siteId: window.siteId || ''
14295  };
14296
14297  // Execute in a safe way that minimizes global namespace pollution
14298  try {
14299    // Set up the global variables needed by the gallery script
14300    window.Parameters = galleryParameters;
14301    window.neonGalleryConfig = neonGalleryConfig;
14302
14303    // Monitor for script load/error
14304    galleryScript.onload = () => {
14305      console.log('Gallery script loaded successfully for:', gallery.title);
14306      
14307      // REMOVED: Don't need to remove the loading indicator since we don't add it
14308    };
14309
14310    galleryScript.onerror = () => {
14311      console.error('Failed to load gallery script for:', gallery.title);
14312      //container.innerHTML = '<div class="gallery-error">Error loading gallery. Please try again later.</div>';
14313      // Restore original parameters
14314      window.Parameters = originalParameters;
14315    };
14316
14317    // Add the script to load the gallery
14318    document.body.appendChild(galleryScript);
14319
14320    // Set up a cleanup function that will be called when another gallery is loaded
14321    container.cleanup = () => {
14322      // Restore original parameters when cleaning up
14323      window.Parameters = originalParameters;
14324      
14325      // Remove the gallery script to prevent conflicts
14326      galleryScript.remove();
14327      
14328      // Clear main gallery container
14329      if (container.parentNode) {
14330        container.innerHTML = '';
14331      }
14332    };
14333  } catch (error) {
14334    console.error('Error creating gallery context:', error);
14335    //container.innerHTML = '<div class="gallery-error">Error initializing gallery. Please try again later.</div>';
14336    // Restore original parameters
14337    window.Parameters = originalParameters;
14338  }
14339}
14340
14341/**
14342* Only used for the PageManager's initializeDOM - ensures clean content swap
14343*/
14344function cleanGalleryContainer() {
14345const galleryContainer = document.querySelector('.gallery-container');
14346if (galleryContainer) {
14347galleryContainer.innerHTML = '';
14348}
14349}
14350
14351
14352
14353/**
14354* Initializes the page using the PageManager
14355* To be used by PageManager.initializeDOM
14356*/
14357function initPageContent(pageId) {
14358// First clear the container completely
14359cleanGalleryContainer();
14360
14361// Then load the page content
14362if (window.PageManager && window.PageManager.loadPage) {
14363window.PageManager.loadPage(pageId);
14364}
14365}
14366
14367
14368/**
14369* Cleanup gallery content when changing between galleries or pages
14370*/
14371function cleanupGalleryContent() {
14372// Get the gallery container
14373const galleryContainer = document.querySelector('.gallery-container');
14374if (!galleryContainer) return;
14375
14376// Find the direct gallery content
14377const directContent = galleryContainer.querySelector('.gallery-direct-content');
14378if (directContent && typeof directContent.cleanup === 'function') {
14379// Call the cleanup function
14380directContent.cleanup();
14381}
14382
14383// Clear the container
14384galleryContainer.innerHTML = '';
14385}
14386
14387
14388
14389
14390/**
14391* Simplified loadGallery function that follows the same content swap pattern
14392* @param {number} id - The gallery ID
14393* @param {Event} event - Optional event object
14394*/
14395
14396// Override loadGallery to update URL
14397window.loadGallery = async function(id, event) {
14398
14399
14400// Stop event propagation if provided
14401if (event) {
14402  event.stopPropagation();
14403}
14404
14405const galleryContainer = document.querySelector('.gallery-container');
14406if (galleryContainer) {
14407  // Apply immediate hiding styles
14408  galleryContainer.style.opacity = '0';
14409}
14410
14411console.log('Loading gallery with ID:', id);
14412
14413stopAutoAdvanceTimer();
14414if (typeof dismissGalleryPasswordOverlay === 'function') {
14415  dismissGalleryPasswordOverlay();
14416}
14417
14418// Find gallery by ID
14419const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible
14420
14421if (!gallery) {
14422  console.warn('No gallery found with ID:', id);
14423  if (galleryContainer) {
14424  galleryContainer.style.opacity = '1';
14425  } // Show container if gallery not found
14426  return;
14427}
14428
14429// Check if this is a page, if so use loadPage instead
14430if (isPageMenuItem(gallery)) {
14431  // Use the loadPage function defined in this scope (or window.loadPage if you prefer)
14432  return loadPage(id, event);
14433}
14434
14435// Skip submenu items
14436if (gallery.isSubmenu) {
14437  console.log('Gallery is submenu, skipping URL update');
14438  toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available
14439  if (galleryContainer) {
14440  galleryContainer.style.opacity = '1';
14441  } // Show container
14442  return;
14443}
14444
14445console.log('Found gallery:', gallery.title);
14446
14447window._activeContentType = 'gallery';
14448beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
14449if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14450  window.PageManager.clearCurrentPage();
14451}
14452
14453// CRITICAL: Set active gallery ID consistently in both global and window scope
14454console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')');
14455window.activeGalleryId = id;
14456activeGalleryId = id; // Ensure both variables are synchronized
14457
14458// Save reference to which gallery we're loading (for later verification)
14459window.currentLoadingGalleryId = id;
14460
14461// CRITICAL: Thorough cleanup with await to ensure completion
14462if (typeof window.removeGalleryScriptsWithPause === 'function') {
14463  await window.removeGalleryScriptsWithPause();
14464}
14465
14466
14467// Clear all existing content from gallery container
14468if (!galleryContainer) {
14469  console.error('Gallery container not found');
14470  return;
14471}
14472
14473// Force clear all content
14474galleryContainer.innerHTML = '';
14475
14476
14477// CRITICAL: Double-check active gallery ID before updating UI
14478if (window.activeGalleryId !== id || activeGalleryId !== id) {
14479  console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id);
14480  window.activeGalleryId = id;
14481  activeGalleryId = id;
14482}
14483
14484// Apply menu visibility based on the gallery's setting and edit mode
14485const bodyEl = document.body;
14486if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
14487    bodyEl.classList.add('menu-hidden-on-page');
14488} else {
14489    bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden
14490}
14491applyMenuOverlayClassForGallery(gallery);
14492
14493// Update UI state using explicit window function references or local fallbacks
14494if (typeof window.updateActiveStates === 'function') {
14495  window.updateActiveStates();
14496} else if (typeof updateActiveStates === 'function') {
14497  updateActiveStates();
14498}
14499
14500if (typeof window.updateMobileTitle === 'function') {
14501  window.updateMobileTitle();
14502} else if (typeof updateMobileTitle === 'function') {
14503  updateMobileTitle();
14504}
14505
14506if (typeof window.closeMobileMenu === 'function') {
14507  window.closeMobileMenu();
14508} else if (typeof closeMobileMenu === 'function') {
14509  closeMobileMenu();
14510}
14511
14512// Update URL with gallery slug
14513if (typeof window.updateURLWithGallerySlug === 'function') {
14514  window.updateURLWithGallerySlug(gallery);
14515} else if (typeof updateURLWithGallerySlug === 'function') {
14516  updateURLWithGallerySlug(gallery);
14517}
14518
14519  // Create gallery container and load content
14520const neonGalleryContainer = document.createElement('div');
14521neonGalleryContainer.id = 'neon-gallery-container';
14522neonGalleryContainer.className = 'gallery-direct-content';
14523neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
14524neonGalleryContainer.style.width = '100%';
14525neonGalleryContainer.style.height = '100%';
14526galleryContainer.appendChild(neonGalleryContainer);
14527
14528// Add a cache-busting parameter to ensure script is freshly loaded
14529const timestamp = Date.now();
14530
14531// Set up gallery parameters
14532window.Parameters = {
14533  SiteAlias: window.location.hostname,
14534  InitialPageUuid: gallery.pageId,
14535  InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available
14536  isInEditor: window.isEditing || false, // Assuming window.isEditing is available
14537  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14538  isHydra: true,
14539  galleryInstanceId: timestamp,
14540  // Added loadedGalleryId as per your original function
14541  loadedGalleryId: gallery.id
14542};
14543
14544// Debug: Check what galleryOptions contains
14545console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL);
14546
14547// Set up gallery config
14548window.neonGalleryConfig = {
14549  useData: true,
14550  useCDN: true,
14551  version: 'live',
14552  manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod",
14553  layoutType: gallery.galleryOptions?.layoutType || "grid",
14554  // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence
14555  // This ensures that only properties from the current gallery are included, not from previous galleries
14556  ...(gallery.galleryOptions || {}),
14557  // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script
14558  // These fields are required for the gallery to determine if it should use Load Network racing
14559  loadTxId: gallery.galleryOptions?.loadTxId || null,
14560  loadPermaURL: gallery.galleryOptions?.loadPermaURL || null,
14561  // Cloudflare CDN NDJSON (unique id) — when present, gallery loads CDN-first (3s) then Fly
14562  cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null,
14563  cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null,
14564  cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null,
14565  embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null,
14566  cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null,
14567  siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available
14568  // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread
14569  galleryId: gallery.id,
14570  galleryInstanceId: timestamp
14571};
14572
14573console.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);
14574
14575// Create and add the gallery script with cache-busting
14576// Use Worker route (avoids CDN SSL issues)
14577const script = document.createElement('script');
14578const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
14579script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file
14580script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute
14581script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute
14582console.log('Loading gallery-main script from Worker:', script.src);
14583script.onerror = function() {
14584  console.error('❌ Gallery script failed, retrying via proxy:', script.src);
14585  const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
14586  script.src = proxyUrl;
14587  script.onerror = function() {
14588    console.error('❌ Proxy failed, trying storage:', proxyUrl);
14589    script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
14590  };
14591};
14592
14593// Added script.onload from your original function
14594script.onload = function() {
14595  if (window.currentLoadingGalleryId !== gallery.id) {
14596    console.warn('Gallery ID mismatch! Expected:', gallery.id,
14597                 'Current:', window.currentLoadingGalleryId);
14598  }
14599  console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`);
14600};
14601
14602
14603if (galleryContainer) {
14604    setTimeout(() => {
14605      galleryContainer.style.opacity = '1';
14606    }, 50); // Small delay to ensure content is ready
14607  }
14608
14609console.log(`Loading fresh gallery script for: ${gallery.title}`);
14610document.body.appendChild(script);
14611}
14612
14613// Function to ensure gallery container is visible
14614function ensureGalleryVisibility() {
14615  const galleryContainer = document.querySelector('.gallery-container');
14616  if (galleryContainer && galleryContainer.style.opacity === '0') {
14617    console.log('ensureGalleryVisibility: Setting gallery container opacity to 1');
14618    galleryContainer.style.opacity = '1';
14619  }
14620}
14621
14622// Handle browser back/forward navigation
14623window.addEventListener('popstate', async function(event) { // Make it async
14624  if (window.__hydraPopstateLock) {
14625    return;
14626  }
14627  window.__hydraPopstateLock = true;
14628  try {
14629  console.error('🔍 [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname);
14630  let galleryIdToLoad = null;
14631  let galleryToLoad = null;
14632
14633  // First, try to get galleryId from event.state
14634  if (event.state && event.state.galleryId) {
14635    galleryIdToLoad = event.state.galleryId;
14636    console.error('🔍 [Popstate] Found galleryId in event.state:', galleryIdToLoad);
14637    // Ensure galleries array is available and use findGalleryById helper
14638    const currentGalleries = window.galleries || galleries || [];
14639    galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad);
14640  }
14641
14642  if (window.__closingNestedLightbox) {
14643    window.__closingNestedLightbox = false;
14644    if (typeof updateActiveStates === 'function') updateActiveStates();
14645    if (typeof updateMobileTitle === 'function') updateMobileTitle();
14646    return;
14647  }
14648
14649  if (window.lightboxNavigationStack?.length > 0 &&
14650      typeof window.closeNestedGalleryLightbox === 'function') {
14651    const stackTop = window.lightboxNavigationStack[window.lightboxNavigationStack.length - 1];
14652    const parentId = stackTop?.parentGalleryId;
14653    if (parentId && (event.state?.galleryId === parentId || galleryToLoad?.id === parentId)) {
14654      window.closeNestedGalleryLightbox({ fromPopstate: true });
14655      if (typeof updateActiveStates === 'function') updateActiveStates();
14656      if (typeof updateMobileTitle === 'function') updateMobileTitle();
14657      return;
14658    }
14659  }
14660
14661  if (typeof dismissGalleryPasswordOverlay === 'function') {
14662    dismissGalleryPasswordOverlay();
14663  }
14664
14665  // If no gallery found from state, resolve from URL slug (handles history entries with null state)
14666  if (!galleryToLoad) {
14667    console.error('🔍 [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname);
14668    const path = window.location.pathname.substring(1);
14669    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
14670    let slug = path;
14671    let siteGuid = null;
14672    if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) {
14673      slug = '';
14674    }
14675    if (isPreviewMode && path) {
14676      const pathParts = path.split('/');
14677      if (pathParts.length >= 1) siteGuid = pathParts[0];
14678      if (pathParts.length >= 2) slug = pathParts[1];
14679      else slug = '';
14680    }
14681    if (slug && typeof findGalleryByPath === 'function') {
14682      galleryToLoad = findGalleryByPath(slug, isPreviewMode, siteGuid);
14683      if (galleryToLoad) {
14684        console.error('🔍 [Popstate] Resolved from URL slug: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14685      }
14686    }
14687  }
14688
14689  // If we have a gallery from state or URL, load it
14690  let contentLoaded = false;
14691  if (galleryToLoad) {
14692    console.error('🔍 [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')');
14693    console.error('🔍 [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements)));
14694    
14695    activeGalleryId = galleryToLoad.id;
14696    window.activeGalleryId = galleryToLoad.id;
14697
14698    // Browser history navigation is explicit user intent — load content regardless of menu visibility.
14699    // Determine if this is a page by checking multiple indicators
14700    const isPage = galleryToLoad.isPage === true || 
14701                   (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) ||
14702                   (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0);
14703    
14704    if (isPage) {
14705      console.error('🔍 [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"');
14706      const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage;
14707      if (typeof pageLoader === 'function') {
14708          console.error('🔍 [Popstate] Calling loadPage for ID:', galleryToLoad.id);
14709          pageLoader(galleryToLoad.id);
14710          contentLoaded = true;
14711      } else {
14712          console.error('❌ [Popstate] loadPage function not available');
14713      }
14714    } else {
14715      console.error('🔍 [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"');
14716      // Clear page state left over from prior page navigation
14717      if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
14718        window.PageManager.clearCurrentPage();
14719      }
14720      window._activeContentType = 'gallery';
14721      const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery;
14722      if (typeof galleryLoader === 'function') {
14723          await galleryLoader(galleryToLoad.id, null, { forceReload: true });
14724          contentLoaded = true;
14725      } else {
14726          console.error('❌ [Popstate] loadGallery function not available');
14727      }
14728    }
14729  } else {
14730    console.error('❌ [Popstate] No gallery found and handleURLNavigation not available');
14731  }
14732
14733  // Update UI states after loading (or attempting to load) content
14734  if (typeof updateActiveStates === 'function') updateActiveStates();
14735  if (typeof updateMobileTitle === 'function') updateMobileTitle();
14736  
14737  // Ensure gallery visibility after popstate navigation
14738  setTimeout(ensureGalleryVisibility, 1000);
14739
14740  let currentLayout = 'sidebar';
14741  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) {
14742    currentLayout = window.MenuStyleCustomizer.settings.menuLayout;
14743  } else if (document.body.classList.contains('menu-layout-horizontal')) {
14744    currentLayout = 'horizontal';
14745  }
14746
14747  if (currentLayout === 'horizontal') {
14748    if (typeof updateActiveStatesHorizontal === 'function') {
14749      console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay.");
14750      setTimeout(() => {
14751          console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal.");
14752          updateActiveStatesHorizontal();
14753      }, 150); // Slightly longer delay for popstate, as full content might be re-initializing
14754    }
14755  }
14756  } finally {
14757    setTimeout(function() { window.__hydraPopstateLock = false; }, 300);
14758  }
14759});
14760
14761// Check for direct URL access on page load (fallback when bootstrap did not route)
14762document.addEventListener('DOMContentLoaded', function() {
14763setTimeout(function() {
14764console.log('Checking for deep links after DOM content loaded');
14765var __p = window.location.pathname;
14766if (__p !== '/' && __p !== '/_manage' && __p !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14767  handleURLNavigation();
14768}
14769// Ensure gallery visibility after URL navigation
14770setTimeout(ensureGalleryVisibility, 1000);
14771}, 500);
14772});
14773
14774document.addEventListener('page-save-requested', function(e) {
14775const data = e.detail;
14776if (data && data.pageId && data.elements) {
14777// Find the gallery/page that corresponds to this pageId
14778const page = galleries.find(g => g.pageId === data.pageId);
14779if (page) {
14780  // Store the page elements
14781  page.pageElements = data.elements;
14782  
14783  // Save to server
14784  saveGalleries();
14785}
14786}
14787});
14788
14789document.addEventListener('DOMContentLoaded', function() {
14790function preloadRichTextEditor() {
14791  if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') {
14792    console.log('Preloading Rich Text Editor dependencies');
14793    return window.RichTextEditor._ensureEditorDependencies()
14794      .then(() => console.log('Rich Text Editor dependencies preloaded successfully'))
14795      .catch(error => console.warn('Failed to preload Rich Text Editor:', error));
14796  }
14797
14798  if (window.PageManager && typeof window.PageManager._ensureRichTextEditor === 'function') {
14799    return window.PageManager._ensureRichTextEditor()
14800      .catch(error => console.warn('Failed to preload Rich Text Editor via PageManager:', error));
14801  }
14802
14803  return null;
14804}
14805
14806preloadRichTextEditor();
14807setTimeout(preloadRichTextEditor, 1000);
14808setTimeout(preloadRichTextEditor, 3000);
14809});
14810
14811document.addEventListener('DOMContentLoaded', function() {
14812const submenuCheck = document.getElementById('createSubmenu');
14813const submenuTitleField = document.getElementById('submenuTitle');
14814const submenuTitleGroup = submenuTitleField?.parentElement;
14815
14816if (submenuCheck && submenuTitleGroup) {
14817submenuCheck.addEventListener('change', function() {
14818  submenuTitleGroup.style.display = this.checked ? 'block' : 'none';
14819});
14820}
14821});
14822
14823// Also check immediately if document is already loaded (fallback only — avoid duplicate loadPage vs bootstrap)
14824if (document.readyState !== 'loading') {
14825setTimeout(function() {
14826console.log('Document already loaded, checking for deep links');
14827var __p2 = window.location.pathname;
14828if (__p2 !== '/' && __p2 !== '/_manage' && __p2 !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) {
14829  handleURLNavigation();
14830}
14831}, 300);
14832}
14833
14834// ============================================
14835// CONCURRENT EDIT CONFLICT DETECTION
14836// ============================================
14837(function() {
14838  // Initialize local timestamp tracking
14839  window.lastKnownConfigTimestamp = null;
14840  let conflictPollInterval = null;
14841  
14842  // Initialize timestamp on page load
14843  async function initializeConfigTimestamp() {
14844    try {
14845      const timestampUrl = typeof getApiUrl === 'function' ? 
14846        getApiUrl('/api/config-timestamp') : 
14847        '/api/config-timestamp';
14848      console.error('[CONFLICT DETECT] Initializing config timestamp from:', timestampUrl);
14849      const response = await fetch(timestampUrl);
14850      if (response.ok) {
14851        const data = await response.json();
14852        if (data.lastModified) {
14853          window.lastKnownConfigTimestamp = data.lastModified;
14854          console.error('[CONFLICT DETECT] Initialized config timestamp:', window.lastKnownConfigTimestamp);
14855        } else {
14856          console.error('[CONFLICT DETECT] No lastModified in response:', data);
14857        }
14858      } else {
14859        console.error('[CONFLICT DETECT] Failed to initialize timestamp, status:', response.status);
14860      }
14861    } catch (error) {
14862      console.error('[CONFLICT DETECT] Could not initialize config timestamp:', error);
14863    }
14864  }
14865  
14866  // Check for conflicts by polling server
14867  async function checkForConflicts() {
14868    // Only poll when in edit mode
14869    if (!document.body.classList.contains('edit-mode-active')) {
14870      console.error('[CONFLICT DETECT] Skipping poll - not in edit mode');
14871      return;
14872    }
14873    
14874    try {
14875      const timestampUrl = typeof getApiUrl === 'function' ? 
14876        getApiUrl('/api/config-timestamp') : 
14877        '/api/config-timestamp';
14878      console.error('[CONFLICT DETECT] Polling for conflicts from:', timestampUrl);
14879      console.error('[CONFLICT DETECT] Current local timestamp:', window.lastKnownConfigTimestamp);
14880      
14881      const response = await fetch(timestampUrl);
14882      
14883      if (response.ok) {
14884        const data = await response.json();
14885        const serverTimestamp = data.lastModified;
14886        console.error('[CONFLICT DETECT] Server timestamp:', serverTimestamp);
14887        
14888        // If we have a local timestamp and it differs from server, show conflict
14889        if (window.lastKnownConfigTimestamp !== null && 
14890            serverTimestamp !== null && 
14891            serverTimestamp !== window.lastKnownConfigTimestamp) {
14892          console.error('[CONFLICT DETECT] ⚠️ CONFLICT DETECTED! Local:', window.lastKnownConfigTimestamp, 'Server:', serverTimestamp);
14893          showConflictIndicator();
14894        } else {
14895          console.error('[CONFLICT DETECT] No conflict - timestamps match or null');
14896          hideConflictIndicator();
14897        }
14898      } else {
14899        console.error('[CONFLICT DETECT] Poll failed with status:', response.status);
14900      }
14901    } catch (error) {
14902      console.error('[CONFLICT DETECT] Error checking for conflicts:', error);
14903    }
14904  }
14905  
14906  // Show conflict indicator icon
14907  function showConflictIndicator() {
14908    const indicator = document.getElementById('conflictIndicator');
14909    console.error('[CONFLICT DETECT] Showing conflict indicator, element found:', !!indicator);
14910    if (indicator) {
14911      indicator.style.display = 'flex';
14912      indicator.style.visibility = 'visible';
14913      indicator.style.opacity = '1';
14914      indicator.style.background = '#d32f2f';
14915      indicator.style.borderColor = '#d32f2f';
14916      console.error('[CONFLICT DETECT] Conflict indicator displayed');
14917    } else {
14918      console.error('[CONFLICT DETECT] ⚠️ Conflict indicator element not found in DOM!');
14919    }
14920  }
14921  
14922  // Hide conflict indicator icon
14923  function hideConflictIndicator() {
14924    const indicator = document.getElementById('conflictIndicator');
14925    if (indicator) {
14926      indicator.style.display = 'none';
14927      indicator.style.visibility = 'hidden';
14928      indicator.style.opacity = '0';
14929    }
14930  }
14931  
14932  // Start/stop polling based on edit mode
14933  function updatePolling() {
14934    const isEditMode = document.body.classList.contains('edit-mode-active');
14935    
14936    // Always hide indicator when not in edit mode
14937    if (!isEditMode) {
14938      hideConflictIndicator();
14939    }
14940    
14941    if (isEditMode && !conflictPollInterval) {
14942      // Start polling every 10 seconds
14943      conflictPollInterval = setInterval(checkForConflicts, 10000);
14944      console.error('[CONFLICT DETECT] ✅ Started conflict detection polling (every 10s)');
14945      
14946      // Also check immediately
14947      checkForConflicts();
14948    } else if (!isEditMode && conflictPollInterval) {
14949      // Stop polling when not in edit mode
14950      clearInterval(conflictPollInterval);
14951      conflictPollInterval = null;
14952      hideConflictIndicator();
14953      console.error('[CONFLICT DETECT] Stopped conflict detection polling');
14954    }
14955  }
14956  
14957  // Initialize on page load
14958  if (document.readyState === 'loading') {
14959    document.addEventListener('DOMContentLoaded', function() {
14960      initializeConfigTimestamp();
14961      // Watch for edit mode changes
14962      const editModeObserver = new MutationObserver(updatePolling);
14963      editModeObserver.observe(document.body, { 
14964        attributes: true, 
14965        attributeFilter: ['class'] 
14966      });
14967      updatePolling();
14968    });
14969  } else {
14970    initializeConfigTimestamp();
14971    // Watch for edit mode changes
14972    const editModeObserver = new MutationObserver(updatePolling);
14973    editModeObserver.observe(document.body, { 
14974      attributes: true, 
14975      attributeFilter: ['class'] 
14976    });
14977    updatePolling();
14978  }
14979})();
14980
14981</script>
14981
14982</head>
14983<body class="menu-layout-sidebar">
14984  <a class="hydra-skip-link" href="#hydra-main-content">Skip to content</a>
14985<nav class="hydra-seo-nav" aria-label="Site navigation">
14986  <ul>
14987    <li><a href="/">Intro</a></li>
14988    <li><a href="/ferguson-michael-brown">Ferguson Uprising, Michael Brown</a></li>
14989    <li><a href="/life">Life</a></li>
14990    <li><a href="/news">News</a></li>
14991    <li><a href="/sports">Sports</a></li>
14992    <li><a href="/portraits-&amp;-illustrations">Portraits &amp; Illustrations</a></li>
14993    <li><a href="/heroin-in-st.-louis">Heroin in St. Louis</a></li>
14994    <li><a href="/death-on-the-rails">Death on the rails</a></li>
14995    <li><a href="/st.-louis,-guns,-shootings-and-crimes">St. Louis, guns, shootings and crimes</a></li>
14996    <li><a href="/afghanistan,-october-2006">Afghanistan, October 2006</a></li>
14997    <li><a href="/iraq,-september-2004">Iraq, September 2004</a></li>
14998    <li><a href="/dominician-baseball">Dominician baseball</a></li>
14999    <li><a href="/oliver,-$6.50-an-hour-cop">Oliver, $6.50 an hour cop</a></li>
15000    <li><a href="/gephardt's-last-run">Gephardt's last run</a></li>
15001    <li><a href="/multimedia-copy">Multimedia</a></li>
15002    <li><a href="/page-1772690596779">Fur, Feathers and Friends</a></li>
15003    <li><a href="/view-from-above">View From Above</a></li>
15004    <li><a href="/sun-moon-and-stars">Sun, Moon and Stars</a></li>
15005    <li><a href="/bio">Bio</a></li>
15006    <li><a href="/contact">Contact</a></li>
15007  </ul>
15008</nav>
15009<noscript class="hydra-gallery-noscript">
15010  <article>
15011    <h1>Life | Simple Image Page</h1>
15012    <div class="gallery-overview"><p style="line-height: 1; margin-bottom: 0.5em;">Scroll Horizontally--&gt;</p></div>
15013    <ul class="gallery-images">
15014    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg" alt="Supermoon St. Louis, Arch" loading="lazy"></li>
15015    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/34-60-08-CarsD-04a.jpg" alt="Prayer Shot" loading="lazy"></li>
15016    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Scout-Flags-1a-copy.jpg" alt="Memorial Day scouts" loading="lazy"></li>
15017    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/57-CD-DC-Tumble-Kids1.jpg" alt="Flipping out" loading="lazy"></li>
15018    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/a04-CD-DC-Robins-1.jpg" alt="Baby Robbins" loading="lazy"></li>
15019    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/54-CD-DC-New-Town-Kids.jpg" alt="Dock" loading="lazy"></li>
15020    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Frosty-December-2.jpg" alt="Frosty morning" loading="lazy"></li>
15021    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---022.jpg" alt="Santa hatch" loading="lazy"></li>
15022    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/070413-DC-Fireworks-2A.jpg" alt="Fair St. Louis fireworks light up the St. Louis skyline on Thursday, July 4, 2013. This picture was shot from the roof of the Pierre Chouteau building in 4400 Lindell Blvd." loading="lazy"></li>
15023    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---018.jpg" alt="Memorial Day" loading="lazy"></li>
15024    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/64-abes-.jpg" alt="00011184-GNN-004" loading="lazy"></li>
15025    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Dance-BWb.jpg" alt="Dance BWb" loading="lazy"></li>
15026    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/fashion-show.jpg" alt="fashion show" loading="lazy"></li>
15027    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Big-Surf-waves.jpg" alt="Big Surf waves" loading="lazy"></li>
15028    <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-STL-Heat.jpg" alt="Rainbow hop" loading="lazy"></li>
15029  </ul>
15030    <p>
15030<a href="https://davidcarsonphotos.com/life">View full gallery</a></p>
15031  </article>
15032</noscript>
15033
15034  <div class="global-header-controls">
15035    
15036    <button id="editButton" class="btn-icon btn-primary" onclick="toggleEditMode()" style="display: none;" data-help-id="edit-toggle">
15037       <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15038        <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
15039          <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"/>
15040          <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"/>
15041        </g>
15042      </svg>
15043    </button>
15044    <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">
15045      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="color: white;">
15046        <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"/>
15047      </svg>
15048    </button>
15049  </div>
15050
15051  <div class="container">
15052    <div class="mobile-header">
15053      <div class="mobile-header-content">
15054        <div class="horizontal-header-logo"></div>
15055        <div class="horizontal-menu-container"></div>
15056      </div>
15057      <button id="hamburgerBtn" class="hamburger-btn" type="button" aria-label="Menu" aria-expanded="false" aria-controls="siteSidebar">
15058        <span class="hamburger-icon" aria-hidden="true">
15059          <span class="hamburger-line"></span>
15060          <span class="hamburger-line"></span>
15061          <span class="hamburger-line"></span>
15062        </span>
15063      </button>
15064    </div>
15065    <div id="mobileBackdrop" class="mobile-backdrop"></div>
15066
15067    <div class="sidebar" id="siteSidebar" role="navigation" aria-label="Site">
15068      <div class="sidebar-header">
15069</div>
15070
15071  
15072  <div class="edit-controls" data-help-id="edit-toolbar">
15073  <button type="button" class="btn-icon btn-primary" onclick="openHydraHelp()" data-custom-tooltip="Help" data-help-id="help-toolbar">
15074    <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
15075      <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" />
15076    </svg>
15077  </button>
15078  <button class="btn-icon btn-primary" onclick="toggleStyleEditor(undefined, event)" data-custom-tooltip="Edit Style" data-help-id="style-toolbar">
15079    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15080      <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"/>
15081    </svg>
15082  </button>
15083  <button class="btn-icon btn-primary" onclick="toggleAddForm()" data-custom-tooltip="Add">
15084    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15085      <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
15086        <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"/>
15087        <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"/>
15088      </g>
15089    </svg>
15090  </button>
15091  <button class="btn-icon btn-primary" onclick="toggleSidebarElementForm(
15091)" data-custom-tooltip="Add Element">
15092    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15093      <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"/>
15094    </svg>
15095  </button>
15096<button class="btn-icon btn-primary" onclick="toggleMetadataEditor()" data-custom-tooltip="Site Metadata">
15097<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M7.5 4.945H16a1.5 1.5 0 0 1 1.5 1.5v1.5m-2.5 5H9m3 4H9"/><path d="M18.497 2H6.307c-.496 0-1.005.073-1.406.368c-1.274.935-2.256 3.02-.273 4.903c.556.528 1.334.72 2.099.72h11.557c.793 0 2.216.113 2.216 2.536v7.454c0 2.22-1.79 4.019-3.997 4.019h-9.03c-2.204 0-3.807-1.557-3.933-3.929L3.506 5.166"/></g></svg>
15098</button>
15099<button id="logoutButton" class="btn-icon btn-primary" onclick="logout()" data-custom-tooltip="Log Out" style="display: none;">
15100      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
15101        <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"/>
15102      </svg>
15103    </button>
15104    
15104<script>
15105
15106// Initialize mobile menu functionality
15107function initMobileMenu() {
15108  const hamburgerBtn = document.getElementById('hamburgerBtn');
15109  const sidebar = document.querySelector('.sidebar');
15110  const mobileBackdrop = document.getElementById('mobileBackdrop');
15111
15112  // --- ADDED CHECK: Only update header if layout is NOT horizontal ---
15113  let currentLayout = 'sidebar'; // Default
15114  if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) {
15115      currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar';
15116  }
15117
15118  // --- END ADDED CHECK ---
15119
15120  // Ensure backdrop is properly hidden for horizontal layout on page load
15121  if (mobileBackdrop && document.body.classList.contains('menu-layout-horizontal')) {
15122    mobileBackdrop.classList.remove('mobile-open');
15123    mobileBackdrop.classList.remove('active');
15124    // Clear any inline overrides - CSS controls backdrop visibility via .mobile-open
15125    mobileBackdrop.style.display = '';
15126    mobileBackdrop.style.pointerEvents = '';
15127    mobileBackdrop.style.opacity = '';
15128    mobileBackdrop.style.visibility = '';
15129  }
15130
15131  // Toggle mobile menu when hamburger is clicked
15132  hamburgerBtn.addEventListener('click', function(e) {
15133    e.stopPropagation();
15134    toggleMobileMenu(); // Assumes this function exists
15135  });
15136
15137  // Close mobile menu when backdrop is clicked
15138  if (mobileBackdrop) {
15139    mobileBackdrop.addEventListener('click', function() {
15140      closeMobileMenu(); // Assumes this function exists
15141    });
15142  }
15143
15144  // Escape closes mobile menu (keyboard / SR)
15145  document.addEventListener('keydown', function(e) {
15146    if (e.key !== 'Escape') return;
15147    const sb = document.querySelector('.sidebar');
15148    if (sb && sb.classList.contains('mobile-open') && typeof closeMobileMenu === 'function') {
15149      closeMobileMenu();
15150    }
15151  });
15152
15153  // Handle window resize events to ensure proper state
15154  window.addEventListener('resize', function() {
15155    // If we're not on mobile anymore, remove mobile-open class
15156    if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) {
15157      closeMobileMenu(); // Assumes this function exists
15158    }
15159  });
15160}
15161
15162document.addEventListener('keydown', function(e) {
15163  // Check for caret (Shift+6)
15164  if (e.key === '^' || (e.key === '6' && e.shiftKey)) {
15165    // Only work in edit mode
15166    if (window.isInEditMode && window.isInEditMode()) {
15167      console.log('Import shortcut detected');
15168      toggleImportClassicForm();
15169      e.preventDefault();
15170    }
15171  }
15172});
15173
15174// Enhanced tooltip management with automatic timeouts
15175
15176// Store active tooltip timeouts by ID
15177window.tooltipTimeouts = {};
15178
15179function showTooltip(trigger) {
15180    const tooltipText = trigger.getAttribute('data-custom-tooltip');
15181    if (!tooltipText) return;
15182
15183    // Get or create the main tooltip container
15184    let container = document.getElementById('tooltip-container');
15185    if (!container) {
15186        console.log("Tooltip container not found, creating one.");
15187        container = document.createElement('div'); // Assign to 'container'
15188        container.id = 'tooltip-container';
15189        // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc.
15190        document.body.appendChild(container);
15191    }
15192
15193    const tooltipId = `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`;
15194
15195    // Remove any existing tooltip for this trigger
15196    const existingTooltip = document.getElementById(tooltipId);
15197    if (existingTooltip) {
15198        existingTooltip.remove();
15199    }
15200
15201    // Clear any existing timeout for this tooltip
15202    if (!window.tooltipTimeouts) {
15203        window.tooltipTimeouts = {}; // Initialize if it doesn't exist
15204    }
15205    
15206    if (window.tooltipTimeouts[tooltipId]) {
15207        clearTimeout(window.tooltipTimeouts[tooltipId]);
15208        delete window.tooltipTimeouts[tooltipId];
15209    }
15210
15211    // Create tooltip element
15212    const tooltip = document.createElement('div');
15213    tooltip.className = 'tooltip'; // Ensure your CSS styles .tooltip
15214    tooltip.id = tooltipId;
15215    tooltip.textContent = tooltipText;
15216
15217    // Add to container (now 'container' is guaranteed to be an element)
15218    container.appendChild(tooltip);
15219
15220    // Position tooltip
15221    if (typeof positionTooltip === 'function') {
15222        positionTooltip(tooltip, trigger);
15223    } else {
15224        console.error("positionTooltip function is not defined.");
15225        tooltip.remove(); // Clean up if we can't position
15226        return;
15227    }
15228
15229    // Show with animation (slight delay for CSS transition)
15230    setTimeout(() => {
15231        tooltip.style.opacity = '1';
15232        // The transform for animation should be handled by CSS transitions
15233        // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); }
15234        // Or ensure positionTooltip sets final transform if it needs to override CSS.
15235        // For now, assuming CSS handles the animated transform from its initial state.
15236    }, 10);
15237
15238    trigger.setAttribute('data-active-tooltip', tooltipId);
15239
15240    // Set automatic timeout to hide after 3 seconds
15241    window.tooltipTimeouts[tooltipId] = setTimeout(() => {
15242        hideTooltip(trigger); // Ensure hideTooltip function exists
15243    }, 3000);
15244    
15245    // Also set up a global timer to ensure all tooltips are dismissed
15246    if (window.globalTooltipTimer) {
15247        clearTimeout(window.globalTooltipTimer);
15248    }
15249    window.globalTooltipTimer = setTimeout(() => {
15250        // Force hide all visible tooltips after 3 seconds
15251        const allTooltips = document.querySelectorAll('.tooltip');
15252        allTooltips.forEach(tooltip => {
15253            if (tooltip.style.opacity !== '0') {
15254                tooltip.style.opacity = '0';
15255                setTimeout(() => {
15256                    if (tooltip.parentNode) {
15257                        tooltip.parentNode.removeChild(tooltip);
15258                    }
15259                }, 200);
15260            }
15261        });
15262        // Clear all timeout references
15263        if (window.tooltipTimeouts) {
15264            for (const id in window.tooltipTimeouts) {
15265                clearTimeout(window.tooltipTimeouts[id]);
15266            }
15267            window.tooltipTimeouts = {};
15268        }
15269    }, 3000);
15270}
15271
15272// Function to hide a tooltip
15273function hideTooltip(trigger) {
15274  const tooltipId = trigger.getAttribute('data-active-tooltip');
15275  if (!tooltipId) return;
15276  
15277  const tooltip = document.getElementById(tooltipId);
15278  if (tooltip) {
15279    // Fade out
15280    tooltip.style.opacity = '0';
15281    tooltip.style.transform = 'translateX(-50%) translateY(0)';
15282    
15283    // Remove after animation
15284    setTimeout(() => {
15285      if (tooltip.parentNode) {
15286        tooltip.parentNode.removeChild(tooltip);
15287      }
15288    }, 200);
15289  }
15290  
15291  // Clear any active timeout for this tooltip
15292  if (window.tooltipTimeouts && window.tooltipTimeouts[tooltipId]) {
15293    clearTimeout(window.tooltipTimeouts[tooltipId]);
15294    delete window.tooltipTimeouts[tooltipId];
15295  }
15296  
15297  // Clear reference
15298  trigger.removeAttribute('data-active-tooltip');
15299  
15300  // If this was the last tooltip, clear the global timer
15301  const remainingTooltips = document.querySelectorAll('.tooltip');
15302  if (remainingTooltips.length === 0 && window.globalTooltipTimer) {
15303    clearTimeout(window.globalTooltipTimer);
15304    window.globalTooltipTimer = null;
15305  }
15306}
15307
15308// Function to initialize tooltips across the document
15309function initTooltipSystem() {
15310  // Check if container already exists
15311  let tooltipContainer = document.getElementById('tooltip-container');
15312  
15313  // Create it if it doesn't exist
15314  if (!tooltipContainer) {
15315    tooltipContainer = document.createElement('div');
15316    tooltipContainer.id = 'tooltip-container';
15317    document.body.appendChild(tooltipContainer);
15318  }
15319  
15320  // Find all elements with data-custom-tooltip attribute
15321  const tooltipTriggers = document.querySelectorAll('[data-custom-tooltip]');
15322  
15323  // Add event listeners to each trigger
15324  tooltipTriggers.forEach(trigger => {
15325    // Skip if already initialized
15326    if (trigger.hasAttribute('data-custom-tooltip-initialized')) return;
15327    
15328    // Mark as initialized
15329    trigger.setAttribute('data-custom-tooltip-initialized', 'true');
15330    
15331    // MouseEnter: Show tooltip
15332    trigger.addEventListener('mouseenter', () => {
15333      showTooltip(trigger);
15334    });
15335    
15336    // MouseLeave: Hide tooltip
15337    trigger.addEventListener('mouseleave', () => {
15338      hideTooltip(trigger);
15339    });
15340    
15341    // Click: Hide tooltip immediately
15342    trigger.addEventListener('click', () => {
15343      hideTooltip(trigger);
15344    });
15345    
15346    // Also handle focus/blur for accessibility
15347    trigger.addEventListener('focus', () => {
15348      showTooltip(trigger);
15349    });
15350    
15351    trigger.addEventListener('blur', () => {
15352      hideTooltip(trigger);
15353    });
15354  });
15355  
15356  // Set up a failsafe function to clear any orphaned tooltips
15357  window.clearAllTooltips = function() {
15358    const container = document.getElementById('tooltip-container');
15359    if (container) {
15360      container.innerHTML = '';
15361    }
15362    
15363    // Clear all timeouts
15364    if (window.tooltipTimeouts) {
15365      for (const id in window.tooltipTimeouts) {
15366        clearTimeout(window.tooltipTimeouts[id]);
15367      }
15368      window.tooltipTimeouts = {};
15369    }
15370    
15371    // Clear global timer
15372    if (window.globalTooltipTimer) {
15373      clearTimeout(window.globalTooltipTimer);
15374      window.globalTooltipTimer = null;
15375    }
15376    
15377    // Clear all data-active-tooltip attributes
15378    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15379      el.removeAttribute('data-active-tooltip');
15380    });
15381  };
15382  
15383  // Initialize new tooltips when DOM changes
15384  setupMutationObserver();
15385  
15386  // Add a global function to manually dismiss all tooltips (for testing)
15387  window.dismissAllTooltips = function() {
15388    const allTooltips = document.querySelectorAll('.tooltip');
15389    allTooltips.forEach(tooltip => {
15390      if (tooltip.style.opacity !== '0') {
15391        tooltip.style.opacity = '0';
15392        setTimeout(() => {
15393          if (tooltip.parentNode) {
15394            tooltip.parentNode.removeChild(tooltip);
15395          }
15396        }, 200);
15397      }
15398    });
15399    
15400    // Clear all timeout references
15401    if (window.tooltipTimeouts) {
15402      for (const id in window.tooltipTimeouts) {
15403        clearTimeout(window.tooltipTimeouts[id]);
15404      }
15405      window.tooltipTimeouts = {};
15406    }
15407    
15408    // Clear global timer
15409    if (window.globalTooltipTimer) {
15410      clearTimeout(window.globalTooltipTimer);
15411      window.globalTooltipTimer = null;
15412    }
15413    
15414    // Clear all data-active-tooltip attributes
15415    document.querySelectorAll('[data-active-tooltip]').forEach(el => {
15416      el.removeAttribute('data-active-tooltip');
15417    });
15418  };
15419}
15420
15421function positionTooltip(tooltip, trigger) {
15422    const triggerRect = trigger.getBoundingClientRect(); // Viewport-relative coordinates
15423
15424    // To accurately get tooltip dimensions, temporarily make it visible but off-screen.
15425    // The #tooltip-container is position:fixed, so the tooltip's absolute positioning
15426    // will be relative to the viewport.
15427    tooltip.style.visibility = 'hidden'; 
15428    tooltip.style.display = 'block'; // Or 'inline-block' if that's its intended final display
15429    // No need to change position to 'absolute' here if its CSS is already 'position: absolute;'
15430    // within the 'fixed' #tooltip-container.
15431
15432    const tooltipHeight = tooltip.offsetHeight;
15433    const tooltipWidth = tooltip.offsetWidth;
15434
15435    // Restore visibility; opacity transition will handle the fade-in.
15436    tooltip.style.visibility = ''; 
15437    // tooltip.style.display = ''; // Let CSS manage display if opacity is used for showing/hiding
15438
15439    // Check if tooltip should be positioned to the right (for conflict indicator)
15440    const tooltipPosition = trigger.getAttribute('data-tooltip-position');
15441    if (tooltipPosition === 'right') {
15442        // Position to the right of the trigger
15443        const viewportWidth = window.innerWidth;
15444        const viewportHeight = window.innerHeight;
15445        let left = triggerRect.right + 10; // 10px gap from the right edge of trigger
15446        let top = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2); // Vertically centered
15447        
15448        // Recalculate tooltip dimensions after allowing text wrap
15449        tooltip.style.maxWidth = '200px';
15450        tooltip.style.whiteSpace = 'normal';
15451        tooltip.style.wordWrap = 'break-word';
15452        const tooltipWidthAfterWrap = tooltip.offsetWidth;
15453        const tooltipHeightAfterWrap = tooltip.offsetHeight;
15454        
15455        // If tooltip goes off-screen to the right, try positioning to the left of the button
15456        if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15457            left = triggerRect.left - tooltipWidthAfterWrap - 10; // Position to the left of button
15458            // If it still goes off the left edge, position it at the left edge with padding
15459            if (left < 5) {
15460                left = 5; // Keep it at least 5px from the left edge
15461                // The tooltip will wrap to fit within the available space
15462                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15463            }
15464        } else {
15465            // Tooltip fits on the right, but make sure it doesn't go off-screen
15466            if (left + tooltipWidthAfterWrap > viewportWidth - 5) {
15467                tooltip.style.maxWidth = `${viewportWidth - left - 10}px`;
15468            }
15469        }
15470        
15471        // Recalculate after max-width adjustment
15472        const finalTooltipWidth = tooltip.offsetWidth;
15473        const finalTooltipHeight = tooltip.offsetHeight;
15474        
15475        // Ensure tooltip doesn't go off-screen on the right side
15476        if (left + finalTooltipWidth > viewportWidth - 5) {
15477            left = viewportWidth - finalTooltipWidth - 5;
15478        }
15479        
15480        // Adjust if tooltip goes off-screen vertically
15481        if (top < 5) {
15482            top = 5;
15483        } else if (top + finalTooltipHeight > viewportHeight - 5) {
15484            top = viewportHeight - finalTooltipHeight - 5;
15485        }
15486        
15487        tooltip.style.left = `${left}px`;
15488        tooltip.style.top = `${top}px`;
15489        tooltip.classList.remove('tooltip-bottom'); // No arrow needed for side positioning
15490        return;
15491    }
15492
15493    // Calculate desired horizontal center point of the trigger
15494    const triggerCenter = triggerRect.left + triggerRect.width / 2;
15495    
15496    // Calculate tooltip's left position to center it under/over trigger's center
15497    let left = triggerCenter - tooltipWidth / 2;
15498
15499    // Default position: above the trigger
15500    // Arrow height (8px) + desired gap (e.g., 2px) = 10px total offset
15501    let top = triggerRect.top - tooltipHeight - 10; 
15502    tooltip.classList.remove('tooltip-bottom'); // Manages arrow direction via CSS
15503
15504    // Check if it goes off-screen at the top (add a small buffer, e.g., 5px)
15505    if (top < 5) { 
15506        // Position below the trigger
15507        top = triggerRect.bottom + 10; // 10px gap (arrow height + small space)
15508        tooltip.classList.add('tooltip-bottom');
15509    }
15510
15511    // Boundary checks for left position to prevent going off-screen horizontally
15512    const viewportWidth = window.innerWidth;
15513    if (left < 5) { // 5px buffer from left edge
15514        left = 5; 
15515    } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge
15516        left = viewportWidth - tooltipWidth - 5;
15517    }
15518
15519    tooltip.style.left = `${left}px`;
15520    tooltip.style.top = `${top}px`;
15521
15522    // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering.
15523    // Since JS now calculates the exact 'left' for centering,
15524    // we can clear any such transform that might interfere.
15525    // However, if your CSS animation relies on a transform, adjust accordingly.
15526    // If your CSS only uses opacity for transition, this is fine:
15527    tooltip.style.transform = ''; 
15528}
15529
15530// Watch for new tooltip triggers added to the DOM
15531function setupMutationObserver() {
15532  // Check if MutationObserver is available
15533  if (!window.MutationObserver) return;
15534  
15535  // Create observer instance
15536  const observer = new MutationObserver((mutations) => {
15537    let shouldInit = false;
15538    
15539    // Check if relevant nodes were added
15540    mutations.forEach(mutation => {
15541      if (mutation.type === 'childList') {
15542        mutation.addedNodes.forEach(node => {
15543          // Check if it's an Element and has data-custom-tooltip or contains elements with data-custom-tooltip
15544          if (node.nodeType === 1) { // ELEMENT_NODE
15545            if (node.hasAttribute && node.hasAttribute('data-custom-tooltip')) {
15546              shouldInit = true;
15547            } else if (node.querySelectorAll) {
15548              const tooltips = node.querySelectorAll('[data-custom-tooltip]');
15549              if (tooltips.length) shouldInit = true;
15550            }
15551          }
15552        });
15553      }
15554    });
15555    
15556    // If relevant nodes found, reinitialize tooltips
15557    if (shouldInit) {
15558      initTooltipSystem();
15559    }
15560  });
15561  
15562  // Start observing the entire document with the configured parameters
15563  observer.observe(document.body, { 
15564    childList: true, 
15565    subtree: true 
15566  });
15567}
15568
15569// Initialize when DOM is ready
15570document.addEventListener('DOMContentLoaded', initTooltipSystem);
15571
15572// Also initialize immediately if document already loaded
15573if (document.readyState === 'complete' || document.readyState === 'interactive') {
15574  initTooltipSystem();
15575}
15576
15577// Add a global safety function that runs every minute to clear any tooltips that might have gotten stuck
15578setInterval(() => {
15579  // Get all visible tooltips
15580  const visibleTooltips = document.querySelectorAll('.tooltip[style*="opacity: 1"]');
15581  
15582  // If any are found, check how long they've been visible
15583  if (visibleTooltips.length > 0) {
15584    console.log('Found visible tooltips during cleanup check, clearing them');
15585    window.clearAllTooltips();
15586  }
15587}, 5000); // 
15588
15589// Mobile drawer a11y: Tab cycle + restore focus. No visual/mouse behavior changes.
15590var _mobileMenuFocusTrapHandler = null;
15591
15592function getMobileMenuFocusables(sidebar) {
15593  if (!sidebar) return [];
15594  return Array.prototype.filter.call(
15595    sidebar.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
15595([tabindex="-1"])'),
15596    function (el) {
15597      if (el.getAttribute('aria-hidden') === 'true') return false;
15598      var style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null;
15599      if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
15600      return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement;
15601    }
15602  );
15603}
15604
15605function releaseMobileMenuFocusTrap() {
15606  if (_mobileMenuFocusTrapHandler) {
15607    document.removeEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15608    _mobileMenuFocusTrapHandler = null;
15609  }
15610}
15611
15612function installMobileMenuFocusTrap(sidebar) {
15613  releaseMobileMenuFocusTrap();
15614  if (!sidebar) return;
15615  _mobileMenuFocusTrapHandler = function (event) {
15616    if (event.key !== 'Tab') return;
15617    var sb = document.querySelector('.sidebar');
15618    if (!sb || !sb.classList.contains('mobile-open')) return;
15619    var focusables = getMobileMenuFocusables(sb);
15620    var hamburger = document.getElementById('hamburgerBtn');
15621    // Include hamburger so Shift+Tab from first item returns to the control that opened the drawer
15622    var cycle = hamburger ? [hamburger].concat(focusables.filter(function (el) { return el !== hamburger; })) : focusables;
15623    if (!cycle.length) return;
15624    var first = cycle[0];
15625    var last = cycle[cycle.length - 1];
15626    if (event.shiftKey) {
15627      if (document.activeElement === first || (sb.contains(document.activeElement) === false && document.activeElement !== hamburger)) {
15628        event.preventDefault();
15629        last.focus();
15630      }
15631    } else if (document.activeElement === last) {
15632      event.preventDefault();
15633      first.focus();
15634    }
15635  };
15636  document.addEventListener('keydown', _mobileMenuFocusTrapHandler, true);
15637}
15638
15639// Toggle mobile menu state
15640function toggleMobileMenu() {
15641const sidebar = document.querySelector('.sidebar');
15642const hamburgerBtn = document.getElementById('hamburgerBtn');
15643const mobileBackdrop = document.getElementById('mobileBackdrop');
15644
15645// Check if we're in horizontal layout mode
15646const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15647
15648sidebar.classList.toggle('mobile-open');
15649hamburgerBtn.classList.toggle('active');
15650const isOpen = sidebar.classList.contains('mobile-open');
15651hamburgerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
15652
15653if (mobileBackdrop) {
15654  if (isHorizontalLayout) {
15655    // For horizontal layout on mobile, use normal backdrop (tap outside to close)
15656    // Clear any inline overrides so CSS can control visibility
15657    mobileBackdrop.style.display = '';
15658    mobileBackdrop.style.pointerEvents = '';
15659    mobileBackdrop.style.opacity = '';
15660    mobileBackdrop.style.visibility = '';
15661    mobileBackdrop.classList.toggle('mobile-open', isOpen);
15662    setTimeout(() => {
15663      mobileBackdrop.classList.toggle('active', isOpen);
15664    }, 10);
15665  } else {
15666    // For other layouts, use normal backdrop behavior
15667    mobileBackdrop.classList.toggle('mobile-open');
15668    // Slight delay to show opacity transition
15669    setTimeout(() => {
15670      mobileBackdrop.classList.toggle('active', isOpen);
15671    }, 10);
15672  }
15673}
15674
15675// Prevent body scrolling when menu is open
15676document.body.style.overflow = isOpen ? 'hidden' : '';
15677
15678if (isOpen) {
15679  // Trap Tab only — do not steal focus on mouse/touch open (avoids focus rings / scroll jumps)
15680  installMobileMenuFocusTrap(sidebar);
15681} else {
15682  releaseMobileMenuFocusTrap();
15683  // Restore focus only if keyboard focus was inside the drawer
15684  if (hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15685    try { hamburgerBtn.focus(); } catch (e) {}
15686  }
15687}
15688}
15689
15690// Close the mobile menu
15691function closeMobileMenu() {
15692const sidebar = document.querySelector('.sidebar');
15693const hamburgerBtn = document.getElementById('hamburgerBtn');
15694const mobileBackdrop = document.getElementById('mobileBackdrop');
15695const wasOpen = sidebar && sidebar.classList.contains('mobile-open');
15696
15697// Check if we're in horizontal layout mode
15698const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal');
15699
15700sidebar.classList.remove('mobile-open');
15701hamburgerBtn.classList.remove('active');
15702if (hamburgerBtn) hamburgerBtn.setAttribute('aria-expanded', 'false');
15703
15704if (mobileBackdrop) {
15705  if (isHorizontalLayout) {
15706    // For horizontal layout, clear inline overrides and remove mobile-open
15707    mobileBackdrop.style.display = '';
15708    mobileBackdrop.style.pointerEvents = '';
15709    mobileBackdrop.style.opacity = '';
15710    mobileBackdrop.style.visibility = '';
15711    mobileBackdrop.classList.remove('mobile-open');
15712    mobileBackdrop.classList.remove('active');
15713  } else {
15714    // For other layouts, use normal backdrop behavior
15715    mobileBackdrop.classList.remove('active');
15716    setTimeout(() => {
15717      mobileBackdrop.classList.remove('mobile-open');
15718    }, 300); // Match transition duration
15719  }
15720}
15721
15722// Re-enable body scrolling
15723document.body.style.overflow = '';
15724releaseMobileMenuFocusTrap();
15725// Return focus to hamburger only if focus was inside the drawer (keyboard/SR); mouse users unchanged
15726if (wasOpen && hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) {
15727  try { hamburgerBtn.focus(); } catch (e) {}
15728}
15729}
15730
15731function updateMobileHeaderContent() {
15732    const currentLayout = typeof getEffectiveMenuLayout === 'function'
15733        ? getEffectiveMenuLayout()
15734        : (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings
15735            ? window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'
15736            : (document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 'sidebar'));
15737
15738    const isMobileView = window.innerWidth <= 768; // Assuming 768px is your mobile breakpoint
15739
15740    const mobileHeaderContent = document.querySelector('.mobile-header-content');
15741    if (!mobileHeaderContent) {
15742        console.warn("updateMobileHeaderContent: .mobile-header-content element not found.");
15743        return;
15744    }
15745    
15746    // On mobile, center the content unless Space Colony (left-aligned title)
15747    if (isMobileView) {
15748        const isSpaceColonyTheme = typeof isSpaceColonySiteTheme === 'function' && isSpaceColonySiteTheme();
15749        mobileHeaderContent.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15750        // Also ensure logo-menu-container doesn't grow
15751        const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container');
15752        if (logoMenuContainer) {
15753            logoMenuContainer.style.flexGrow = '0';
15754            logoMenuContainer.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center';
15755        }
15756    }
15757
15758    console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`);
15759
15760    // Clear previous generic mobile content (like centered logo/title)
15761    const existingMobileImage = mobileHeaderContent.querySelector('.mobile-header-image');
15762    const existingMobileText = mobileHeaderContent.querySelector('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title');
15763    if (existingMobileImage) {
15764        // The img is always appended directly (never wrapped in an <a>). Remove it straight.
15765        // Also handle any legacy <a>-wrapped images left from older deployments.
15766        const imageParent = existingMobileImage.parentElement;
15767        if (imageParent && imageParent.tagName === 'A' && imageParent.parentElement === mobileHeaderContent) {
15768            imageParent.remove();
15769        } else {
15770            existingMobileImage.remove();
15771        }
15772    }
15773    // Sweep any stray empty <a> tags left by older deployments that used the wrapper approach
15774    Array.from(mobileHeaderContent.children).forEach(function(child) {
15775        if (child.tagName === 'A' && !child.querySelector('.mobile-header-image') && !(child.textContent || '').trim()) {
15776            child.remove();
15777        }
15778    });
15779    if (existingMobileText) existingMobileText.remove();
15780    
15781    if (currentLayout === 'horizontal' && !isMobileView) {
15782        // Desktop Horizontal Layout:
15783        // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo.
15784        // The menu items are in .horizontal-menu-container.
15785        // Remove stray mobile-only title/image nodes (duplicate of horizontal-header-logo).
15786        mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title, .mobile-header-image').forEach(function(el) {
15787            el.remove();
15788        });
15789        console.log("updateMobileHeaderContent: Desktop horizontal layout, re-rendering horizontal menu to ensure sidebar elements are available.");
15790        if (typeof renderHorizontalMenu === 'function') {
15791            // Wait for sidebar elements to be available before rendering
15792            const waitForSidebarElements = (attempts = 0) => {
15793                const maxAttempts = 10;
15794                if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
15795                    console.log("updateMobileHeaderContent: Sidebar elements available, rendering horizontal menu.");
15796                    renderHorizontalMenu();
15797                } else if (attempts < maxAttempts) {
15798                    const attemptNum = attempts + 1;
15799                    console.log("updateMobileHeaderContent: Waiting for sidebar elements (attempt " + attemptNum + "/" + maxAttempts + ")...");
15800                    setTimeout(() => waitForSidebarElements(attempts + 1), 100);
15801                } else {
15802                    console.warn("updateMobileHeaderContent: Sidebar elements not available after waiting, rendering anyway.");
15803                    renderHorizontalMenu();
15804                }
15805            };
15806            waitForSidebarElements();
15807        }
15808        return; 
15809    }
15810    
15811    // For Horizontal layout on mobile, we need to clear the horizontal menu items
15812    // and show only the mobile header logo/title
15813    if (currentLayout === 'horizontal' && isMobileView) {
15814        console.log("updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo.");
15815        
15816        // Clear horizontal menu items from mobile header
15817        const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu
15817-container');
15818        if (horizontalMenuContainer) {
15819            console.log("updateMobileHeaderContent: Clearing horizontal menu container");
15820            horizontalMenuContainer.innerHTML = '';
15821        } else {
15822            console.log("updateMobileHeaderContent: No horizontal menu container found to clear");
15823        }
15824        
15825        // Clear horizontal header logo from mobile header
15826        const horizontalHeaderLogo = mobileHeaderContent.querySelector('.horizontal-header-logo');
15827        if (horizontalHeaderLogo) {
15828            console.log("updateMobileHeaderContent: Clearing horizontal header logo");
15829            horizontalHeaderLogo.innerHTML = '';
15830        } else {
15831            console.log("updateMobileHeaderContent: No horizontal header logo found to clear");
15832        }
15833        
15834        // Also clear any existing horizontal menu items that might be directly in mobile-header-content
15835        const existingHorizontalMenuItems = mobileHeaderContent.querySelectorAll('.horizontal-menu-item');
15836        if (existingHorizontalMenuItems.length > 0) {
15837            console.log('updateMobileHeaderContent: Removing ' + existingHorizontalMenuItems.length + ' existing horizontal menu items');
15838            existingHorizontalMenuItems.forEach(item => item.remove());
15839        }
15840    }
15841    
15842    // For Sidebar, Top, OR Horizontal layout on mobile:
15843    // Populate .mobile-header-content with the first sidebar element (logo/title).
15844    // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout.
15845
15846    console.log("updateMobileHeaderContent: Populating with first sidebar element.");
15847    
15848    // For horizontal layout on mobile, use the header-specific function to get logo/title
15849    // For other layouts, use the general function
15850    let firstElement;
15851    if (currentLayout === 'horizontal' && isMobileView) {
15852        firstElement = getFirstSidebarElementForHeader(); // Gets logo/title specifically
15853        console.log("updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout");
15854    } else {
15855        firstElement = getFirstSidebarElement(); // Gets any first element
15856        console.log("updateMobileHeaderContent: Using getFirstSidebarElement for other layouts");
15857    }
15858
15859    if (firstElement) {
15860        console.log('updateMobileHeaderContent: First element found:', JSON.stringify(firstElement));
15861        
15862        if (firstElement.type === 'image' && (firstElement.imageData || firstElement.imageUrl)) {
15863            console.log('updateMobileHeaderContent: Creating mobile header image');
15864            const img = document.createElement('img');
15865            img.src = firstElement.imageUrl || firstElement.imageData;
15866            img.alt = resolveBrandLogoAlt(firstElement);
15867            img.className = 'mobile-header-image sidebar-image'; // For specific styling if needed
15868
15869            let mobileDisplayNode = img;
15870            if (firstElement.colorizeWithSiteColors &&
15871                window.SidebarManager &&
15872                typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') {
15873              const buildData = Object.assign({}, firstElement);
15874              if (!buildData.imageWidth) {
15875                buildData.imageWidth = 'auto';
15876              }
15877              mobileDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData);
15878            }
15879
15880            // Opt-in per-image padding wrapper (no-op when unset)
15881            if (window.SidebarManager &&
15882                typeof window.SidebarManager._wrapWithImagePadding === 'function') {
15883              mobileDisplayNode = window.SidebarManager._wrapWithImagePadding(mobileDisplayNode, firstElement);
15884            }
15885            
15886            // Handle linking: always append the img directly so existing CSS centering is
15887            // not disrupted by an <a> wrapper (which the many !important positioning rules
15888            // were not written to handle). Navigation is wired via a click handler.
15889            if (firstElement.linkType && firstElement.linkType !== 'none') {
15890                let finalLinkHref = '#';
15891                let isValidLink = false;
15892                const linkTarget = firstElement.linkTarget || '_self';
15893                
15894                if (firstElement.linkType === 'external' && firstElement.linkUrl) {
15895                    finalLinkHref = firstElement.linkUrl;
15896                    isValidLink = true;
15897                } else if (firstElement.linkType === 'internal' && firstElement.linkPageId) {
15898                    const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15899                    const targetPage = allGalleries.find(g => g.id === parseInt(firstElement.linkPageId));
15900                    
15901                    if (targetPage) {
15902                        const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
15903                        if (isPreviewMode) {
15904                            const pathParts = window.location.pathname.split('/').filter(Boolean);
15905                            const siteGuid = pathParts[0];
15906                            if (siteGuid && targetPage.slug) {
15907                                finalLinkHref = '/' + siteGuid + '/' + targetPage.slug;
15908                                isValidLink = true;
15909                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15910                                finalLinkHref = targetPage.url;
15911                                isValidLink = true;
15912                            }
15913                        } else {
15914                            if (targetPage.slug) {
15915                                finalLinkHref = '/' + targetPage.slug;
15916                                isValidLink = true;
15917                            } else if (targetPage.url && targetPage.url.startsWith('/')) {
15918                                finalLinkHref = targetPage.url;
15919                                isValidLink = true;
15920                            }
15921                        }
15922                    }
15923                }
15924                
15925                if (isValidLink) {
15926                    mobileDisplayNode.style.cursor = 'pointer';
15927                    const capturedHref = finalLinkHref;
15928                    const capturedTarget = linkTarget;
15929                    const capturedLinkType = firstElement.linkType;
15930                    const capturedPageId = firstElement.linkPageId;
15931                    
15932                    mobileDisplayNode.addEventListener('click', (e) => {
15933                        e.preventDefault();
15934                        if (capturedLinkType === 'internal' && capturedPageId) {
15935                            const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []);
15936                            const targetPage = allGalleries.find(g => g.id === parseInt(capturedPageId));
15937                            if (targetPage) {
15938                                if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') {
15939                                    window.loadPage(targetPage.id);
15940                                } else if (typeof window.loadGallery === 'function') {
15941                                    window.loadGallery(targetPage.id);
15942                                } else {
15943                                    window.location.href = capturedHref;
15944                                }
15945                            } else {
15946                                window.location.href = capturedHref;
15947                            }
15948                        } else {
15949                            if (capturedTarget === '_blank') {
15950                                window.open(capturedHref, '_blank');
15951                            } else {
15952                                window.location.href = capturedHref;
15953                            }
15954                        }
15955                    });
15956                }
15957            }
15958            mobileHeaderContent.appendChild(mobileDisplayNode);
15959        } else if (firstElement.type === 'text' && firstElement.textContent) {
15960            console.log('updateMobileHeaderContent: Creating mobile header text');
15961            const textDiv = document.createElement('div');
15962            textDiv.className = 'mobile-header-text'; // For specific styling
15963            textDiv.innerHTML = firstElement.textContent; // Use innerHTML if textContent can contain HTML
15964            const titleEl = textDiv.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type');
15965            if (titleEl && !titleEl.classList.contains('site-title-text')) {
15966              titleEl.classList.add('site-title-text');
15967            }
15968            mobileHeaderContent.appendChild(textDiv);
15969        } else if (firstElement.type === 'menu' && galleries && galleries.length > 0) {
15970            console.log('updateMobileHeaderContent: Creating mobile header menu title');
15971            // Fallback for 'menu' type, find the first visible gallery title
15972            const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer);
15973            const titleText = firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || 'Menu');
15974            const titleDiv = document.createElement('div');
15975            titleDiv.className = 'mobile-header-menu-title'; // For specific styling
15976            titleDiv.textContent = titleText;
15977            mobileHeaderContent.appendChild(titleDiv);
15978        } else { // Default fallback
15979            console.log('updateMobileHeaderContent: Creating mobile header title (fallback)');
15980            const titleDiv = document.createElement('div');
15981            titleDiv.className = 'mobile-header-title'; // For specific styling
15982            titleDiv.textContent = firstElement.title || (galleries && galleries.length > 0 && galleries[0].title) || 'Menu';
15983            mobileHeaderContent.appendChild(titleDiv);
15984        }
15985    } else {
15986        // Fallback if no sidebar elements exist
15987         const defaultTitle = document.createElement('div');
15988         defaultTitle.className = 'mobile-header-title';
15989         defaultTitle.textContent = 'Menu';
15990         mobileHeaderContent.appendChild(defaultTitle);
15991        console.log("updateMobileHeaderContent: No first sidebar element found, showing default title.");
15992    }
15993
15994    if (typeof syncContentMarginToHorizontalHeader === 'function') {
15995      syncContentMarginToHorizontalHeader();
15996    }
15997}
15998
15999// Helper function to get the first element in the sidebar
16000function getFirstSidebarElement() {
16001// Check if window.SidebarManager exists and has elements
16002if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) {
16003// Filter to only visible elements
16004const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false && el.type !== 'spacer');
16005
16006if (visibleElements.length > 0) {
16007  // Sort by position and return the first one
16008  return visibleElements.sort((a, b) => a.position - b.position)[0];
16009}
16010}
16011
16012// If no sidebar elements, return first gallery as fallback
16013if (galleries.length > 0) {
16014const firstVisibleGallery = galleries.find(g => g.visible !== false);
16015if (firstVisibleGallery) {
16016  return {
16017    type: 'menu',
16018    title: firstVisibleGallery.title
16019  };
16020}
16021}
16022
16023// Default fallback
16024return {
16025type: 'text',
16026title: 'Menu'
16027};
16028}
16029// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE
16030// Just replace the updateMobileTitle function and add our initialization code
16031
16032// Replace the existing updateMobileTitle function with this one
16033function updateMobileTitle() {
16034// Always call our implementation that shows the top sidebar element
16035updateMobileHeaderContent();
16036}
16037
16038// Add window resize listener to handle mobile header content updates
16039window.addEventListener('resize', () => {
16040  // Debounce the resize event to avoid excessive calls
16041  clearTimeout(window.resizeTimeout);
16042  window.resizeTimeout = setTimeout(() => {
16043    if (typeof updateMobileHeaderContent === 'function') {
16044      updateMobileHeaderContent();
16045    }
16046    if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') {
16047      applyHorizontalLogoMenuLayoutStyles();
16048    }
16049    if (typeof syncContentMarginToHorizontalHeader === 'function') {
16050      syncContentMarginToHorizontalHeader();
16051    }
16052  }, 250);
16053});
16054
16055//separate initialization function that doesn't replace the original initMobileMenu
16056function initTopElementDisplay() {
16057console.log("Initializing top element display");
16058
16059// Run immediately
16060updateMobileHeaderContent();
16061
16062if (typeof syncContentMarginToHorizontalHeader === 'function') {
16063  setTimeout(syncContentMarginToHorizontalHeader, 150);
16064}
16065
16066// Also hook into loadGallery to ensure it runs whenever galleries change
16067const originalLoadGallery = window.loadGallery;
16068if (typeof originalLoadGallery === 'function') {
16069window.loadGallery = function(id, event, options) {
16070  // Forward all args (including post-save { forceReload: true }) to the underlying loader
16071  const result = originalLoadGallery(id, event, options);
16072  updateMobileHeaderContent();
16073  return result;
16074};
16075}
16076}
16077
16078
16079// Initialize our functionality after a short delay to ensure SidebarManager is ready
16080document.addEventListener('DOMContentLoaded', function() {
16081// Short delay to ensure SidebarManager is initialized
16082setTimeout(initTopElementDisplay, 100);
16083
16084// Also run a bit later to catch any late-loading elements
16085setTimeout(updateMobileHeaderContent, 500);
16086});
16087
16088// Run immediately if document is already loaded
16089if (document.readyState === 'complete' || document.readyState === 'interactive') {
16090setTimeout(initTopElementDisplay, 100);
16091}
16092
16093// Add a final fallback to ensure gallery visibility on window load
16094window.addEventListener('load', function() {
16095  setTimeout(ensureGalleryVisibility, 2000);
16096});
16097
16098
16099(function() {
16100  /**
16101   * EnhancedGallerySystem - Comprehensive gallery script and state management
16102   */
16103  class EnhancedGallerySystem {
16104    constructor() {
16105      // Core state tracking
16106      this.initialized = false;
16107      this.initPromise = null;
16108      this.isLoading = false;
16109      this.currentGalleryId = null;
16110      this.version = '260923-002'; // Increment when gallery scripts change (matches current versioned filenames)
16111
16112      // Track loaded resources
16113      this.loadedScripts = new Set();
16114      this.loadedStyles = new Set();
16115      
16116      // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues)
16117      const galleryScriptsBase = window.location.origin + '/gallery-scripts/';
16118      this.essentialResources = {
16119        scripts: [
16120          { id: 'neon-gallery-main', src: galleryScriptsBase + 'neon-gallery-main-v260923-002.js' },
16121          { id: 'neon-gallery-hp', src: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.js' }
16122        ],
16123        styles: [
16124          { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' }
16125        ]
16126      };
16127      
16128      // Keep track of original initialization functions
16129      this.originalInitFunctions = {};
16130      
16131      // Keep reference to gallery state between changes
16132      this.previousGalleryState = null;
16133      
16134      // Track last initialization time per gallery ID to prevent duplicates
16135      this.lastInitializationTime = new Map();
16136      this._loadInProgress = null;
16137      this._loadingGalleryId = null;
16138    }
16139
16140    /**
16141 * Setup the correct gallery container structure
16142 * @param {HTMLElement} container - The main container element
16143 * @returns {boolean} - Success indicator
16144 */
16145setupCorrectGalleryContainer(container) {
16146  console.log('EnhancedGallerySystem: Setting up correct gallery container structure');
16147  
16148  // Validate container
16149  if (!container) {
16150    console.error('EnhancedGallerySystem: Main gallery container is null');
16151    return false;
16152  }
16153  
16154  try {
16155    // Clear the container
16156    container.innerHTML = '';
16157    
16158    // Create the exact container structure expected by the gallery script
16159    const neonContainer = document.createElement('div');
16160    neonContainer.id = 'neon-gallery-container';
16161    neonContainer.style.width = '100%';
16162    neonContainer.style.height = '100%';
16163    neonContainer.style.position = 'relative';
16164    
16165    // Add the container to the DOM
16166    container.appendChild(neonContainer);
16167    
16168    // Set visibility
16169    container.style.opacity = '1';
16170    container.style.display = 'block';
16171    
16172    console.log('EnhancedGallerySystem: Gallery container structure created');
16173    return true;
16174  } catch (error) {
16175    console.error('EnhancedGallerySystem: Error setting up gallery container:', error);
16176    return false;
16177  }
16178}
16179
16180    /**
16181     * Ensure #neon-gallery-container exists before gallery scripts execute.
16182     * neon-gallery-main bails early without it; hp may leave neonGalleryUtils as {}.
16183     */
16184    ensureGalleryContainerPlaceholder(container) {
16185      if (document.getElementById('neon-gallery-container')) {
16186        return document.getElementById('neon-gallery-container');
16187      }
16188      const host = container || document.querySelector('.gallery-container') || document.getElementById('galleryFrame')?.parentElement;
16189      if (!host) {
16190        return null;
16191      }
16192      const neonContainer = document.createElement('div');
16193      neonContainer.id = 'neon-gallery-container';
16194      neonContainer.className = 'gallery-direct-content';
16195      neonContainer.style.width = '100%';
16196      neonContainer.style.height = '100%';
16197      neonContainer.style.position = 'relative';
16198      host.appendChild(neonContainer);
16199      console.log('EnhancedGallerySystem: Created placeholder neon-gallery-container before script load');
16200      return neonContainer;
16201    }
16202
16203    /**
16204     * Wait for neon-gallery-main to expose loadGallerySettings; reload main if needed.
16205     */
16206    async ensureNeonGalleryUtilsReady(container) {
16207      const hasLoadGallerySettings = () =>
16208        typeof window.neonGalleryUtils?.loadGallerySettings === 'function';
16209
16210      if (hasLoadGallerySettings()) {
16211        return true;
16212      }
16213
16214      const mainScript = this.essentialResources.scripts.find(s => s.id === 'neon-gallery-main');
16215      if (!mainScript) {
16216        return false;
16217      }
16218
16219      console.warn('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings missing — reloading main script');
16220      this.ensureGalleryContainerPlaceholder(container);
16221
16222      document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
16223        if (el.parentNode) el.parentNode.removeChild(el);
16224      });
16225      this.loadedScripts.delete('neon-gallery-main');
16226      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16227
16228      await new Promise((resolve, reject) => {
16229        const scriptElement = document.createElement('script');
16230        scriptElement.id = 'neon-gallery-main';
16231        scriptElement.src = `${mainScript.src}?v=${this.version}&reload=${Date.now()}`;
16232        scriptElement.async = false;
16233        scriptElement.onload = () => {
16234          this.loadedScripts.add('neon-gallery-main');
16235          resolve();
16236        };
16237        scriptElement.onerror = (err) => reject(err);
16238        document.body.appendChild(scriptElement);
16239      });
16240
16241      for (let i = 0; i < 50; i++) {
16242        if (hasLoadGallerySettings()) {
16243          return true;
16244        }
16245        await new Promise(r => setTimeout(r, 100));
16246      }
16247
16248      return hasLoadGallerySettings();
16249    }
16250
16251    /**
16252     * Preload styles only — defer script loading until a gallery load provides a container.
16253     */
16254    preloadStylesOnly() {
16255      return this.loadStyles();
16256    }
16257
16258    /**
16259     * Reset initialization tracking when gallery scripts are removed from the DOM
16260     * (e.g. loadPage cleanup). Without this, a pending initPromise never resolves and
16261     * subsequent loadGallery calls hang with isLoading:true, initialized:false.
16262     */
16263    resetInitializationState() {
16264      console.log('EnhancedGallerySystem: Resetting initialization state');
16265      this.initPromise = null;
16266      this.initialized = false;
16267      this.isLoading = false;
16268      this.loadedScripts.clear();
16269      this.loadedStyles.clear();
16270      // Allow neon-gallery-main bootstrap wipe once again after scripts are re-injected
16271      try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16272    }
16273
16274    /**
16275     * Initialize the gallery system
16276     * @returns {Promise} Resolves when all core dependencies are loaded
16277     */
16278    initialize() {
16279      // Return existing promise if already initializing or complete
16280      if (this.initPromise) {
16281        return this.initPromise;
16282      }
16283
16284      console.log('EnhancedGallerySystem: Initializing gallery framework');
16285      this.isLoading = true;
16286
16287      // Create a promise to track loading completion
16288      this.initPromise = (async () => {
16289        try {
16290          // CRITICAL: Initialize neonGalleryState before anything else
16291          // This prevents "Cannot set properties of undefined" errors
16292          if (!window.neonGalleryState) {
16293            window.neonGalleryState = {
16294              initialized: false,
16295              initializing: false,
16296              domInitialized: false,
16297              configApplied: false,
16298              initializationPromise: null
16299            };
16300            console.log('EnhancedGallerySystem: Initialized neonGalleryState');
16301          }
16302
16303          // 1. First ensure all essential styles are loaded
16304          await this.loadStyles();
16305
16306          // 2. Preload all scripts to hint to the browser
16307          this.preloadScripts();
16308
16309          // 3. Ensure gallery container exists before scripts that require it
16310          this.ensureGalleryContainerPlaceholder(
16311            document.querySelector('.gallery-container')
16312          );
16313
16314          // 4. Load all essential scripts
16315          await this.loadScripts();
16316          
16317          // 5. Initialize the gallery events
16318          this.setupEvents();
16319          
16320          // Mark as fully initialized
16321          this.initialized = true;
16322          this.isLoading = false;
16323          
16324          console.log('EnhancedGallerySystem: Successfully initialized');
16325          return true;
16326        } catch (error) {
16327          console.error('EnhancedGallerySystem: Initialization failed:', error);
16328          this.isLoading = false;
16329          throw error;
16330        }
16331      })();
16332
16333      return this.initPromise;
16334    }
16335
16336    /**
16337     * Load all necessary stylesheet resources
16338     */
16339    async loadStyles() {
16340      console.log('EnhancedGallerySystem: Loading essential stylesheets');
16341      
16342      const stylePromises = this.essentialResources.styles.map(style => {
16343        // Skip if already loaded
16344        if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) {
16345          console.log(`Style already loaded: ${style.id}`);
16346          this.loadedStyles.add(style.id);
16347          return Promise.resolve();
16348        }
16349        
16350        return new Promise((resolve, reject) => {
16351          const link = document.createElement('link');
16352          link.id = style.id;
16353          link.rel = 'stylesheet';
16354          // Use proxy URL if flag is set, otherwise use CDN URL
16355          let styleUrl = `${style.href}?v=${this.version}`;
16356          if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes('cdn.neonsky.app')) {
16357            styleUrl = window.convertCdnToProxyUrl(style.href) + '?v=' + this.version;
16358            console.log('🔍 Using proxy URL for style:', style.id, styleUrl);
16359          }
16360          
16361          link.href = styleUrl;
16362          
16363          link.onload = () => {
16364            console.log(`✅ Style loaded: ${style.id}`);
16365            this.loadedStyles.add(style.id);
16366            resolve();
16367          };
16368          
16369          link.onerror = (err) => {
16370            console.error(`❌ Failed to load style: ${style.id}`, err);
16371            if (style.href.includes('cdn.neonsky.app') && !styleUrl.includes('/cdn-proxy/')) {
16372              console.error('🔄 Retrying via proxy:', style.href);
16373              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16374              link.href = proxyUrl + '?v=' + this.version;
16375              link.onerror = function() {
16376                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16377                link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app');
16378                // Don't reject - let it try storage
16379              };
16380            } else {
16381              reject(new Error(`Failed to load ${style.id}`));
16382            }
16383          };
16384          
16385          document.head.appendChild(link);
16386        });
16387      });
16388      
16389      // Wait for all styles to load
16390      await Promise.all(stylePromises);
16391      console.log('EnhancedGallerySystem: All stylesheets loaded');
16392    }
16393    
16394    /**
16395     * Preload scripts to improve browser loading priority
16396     */
16397    preloadScripts() {
16398      this.essentialResources.scripts.forEach(script => {
16399        // Enhanced check for already preloaded scripts
16400        const scriptName = script.src.split('/').pop();
16401        const alreadyPreloaded = document.querySelector(`link[rel="preload"][href*="${scriptName}"]`) ||
16402                                document.querySelector(`script[src*="${scriptName}"]`);
16403        
16404        if (alreadyPreloaded) {
16405          console.log(`Script already preloaded or loaded: ${script.id}`);
16406          return;
16407        }
16408        
16409        const preload = document.createElement('link');
16410        preload.rel = 'preload';
16411        preload.href = `${script.src}?v=${this.version}`;
16412        preload.as = 'script';
16413        document.head.appendChild(preload);
16414        
16415        console.log(`Script preloaded: ${script.id}`);
16416      });
16417    }
16418    
16419    /**
16420     * Load all necessary scripts
16421     */
16422    async loadScripts() {
16423      console.log('EnhancedGallerySystem: Loading essential scripts');
16424      
16425      // Load sequentially (main before hp) so neon-gallery-main assigns window.neonGalleryUtils
16426      // before hp's initNeonGallery can run; parallel async scripts could execute hp before main finishes.
16427      const loadOne = (script) => {
16428        const alreadyLoaded = this.loadedScripts.has(script.id) || 
16429                             document.getElementById(script.id) ||
16430                             document.querySelector(`script[src*="${script.src.split('/').pop()}"]`);
16431        
16432        if (alreadyLoaded) {
16433          const mainNeedsReload =
16434            script.id === 'neon-gallery-main' &&
16435            typeof window.neonGalleryUtils?.loadGallerySettings !== 'function';
16436          if (mainNeedsReload) {
16437            console.warn(`EnhancedGallerySystem: ${script.id} in DOM but neonGalleryUtils incomplete — reloading`);
16438            document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => {
16439              if (el.parentNode) el.parentNode.removeChild(el);
16440            });
16441            this.loadedScripts.delete(script.id);
16442            try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; }
16443          } else {
16444          console.log(`Script already loaded: ${script.id}`);
16445          this.loadedScripts.add(script.id);
16446          return Promise.resolve();
16447          }
16448        }
16449        
16450        return new Promise((resolve, reject) => {
16451          const scriptElement = document.createElement('script');
16452          scriptElement.id = script.id;
16453          
16454          let scriptUrl = `${script.src}?v=${this.version}`;
16455          if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes('cdn.neonsky.app')) {
16456            scriptUrl = window.convertCdnToProxyUrl(script.src) + '?v=' + this.version;
16457            console.log('🔍 Using proxy URL for script:', script.id, scriptUrl);
16458          }
16459          
16460          scriptElement.src = scriptUrl;
16461          scriptElement.async = false;
16462          
16463          scriptElement.onload = () => {
16464            console.log(`✅ Script loaded: ${script.id}`);
16465            this.loadedScripts.add(script.id);
16466            resolve();
16467          };
16468          
16469          scriptElement.onerror = (err) => {
16470            console.error(`❌ Failed to load script: ${script.id}`, err);
16471            if (script.src.includes('cdn.neonsky.app') && !scriptUrl.includes('/cdn-proxy/')) {
16472              console.error('🔄 Retrying script via proxy:', script.id);
16473              const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/');
16474              scriptElement.src = proxyUrl + '?v=' + this.version;
16475              scriptElement.onerror = function() {
16476                console.error('❌ Proxy failed, trying storage:', proxyUrl);
16477                scriptElement.src = scriptElement.src.replace('cdn.neonsky.app', 'storage.neonsky.app');
16478              };
16479            } else {
16480              reject(new Error(`Failed to load ${script.id}`));
16481            }
16482          };
16483          
16484          document.body.appendChild(scriptElement);
16485        });
16486      };
16487      
16488      for (const script of this.essentialResources.scripts) {
16489        await loadOne(script);
16490      }
16491      console.log('EnhancedGallerySystem: All scripts loaded');
16492      
16493      // Save references to original initialization functions after scripts are loaded
16494      if (window.initNeonGallery && typeof window.initNeonGallery === 'function') {
16495        this.originalInitFunctions.initNeonGallery = window.initNeonGallery;
16496      }
16497      
16498      if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave === 'function') {
16499        this.originalInitFunctions.reinitializeGalleryAfterSave = window.reinitializeGalleryAfterSave;
16500      }
16501    }
16502
16503    /**
16504     * Set up enhanced event listeners
16505     */
16506    setupEvents() {
16507      // Listen for gallery save events to capture latest state
16508      document.addEventListener('gallery-saved', (event) => {
16509        if (event.detail && event.detail.settings) {
16510          this.captureGalleryState(event.detail.settings);
16511        }
16512      });
16513      
16514      // Override existing functions to capture state changes
16515      this.enhanceExistingFunctions();
16516    }
16517    
16518    /**
16519     * Enhance existing gallery functions to work with our system
16520     */
16521    enhanceExistingFunctions() {
16522      // Save original functions
16523      const originalInit = window.initNeonGallery;
16524      const originalReinit = window.reinitializeGalleryAfterSave;
16525      
16526      // Only enhance if they exist
16527      if (typeof originalInit === 'function') {
16528        // Track if initNeonGallery is currently executing to prevent concurrent calls
16529        let isExecuting = false;
16530        let lastExecutionTime = 0;
16531        
16532        window.initNeonGallery = async (...args) => {
16533          // Prevent duplicate initializations within 1 second
16534          const now = Date.now();
16535          
16536          // Check if already executing or called recently
16537          if (isExecuting) {
16538            console.log('EnhancedGallerySystem: initNeonGallery already executing, skipping duplicate call');
16539            return Promise.resolve();
16540          }
16541          
16542          if (lastExecutionTime && (now - lastExecutionTime) < 1000) {
16543            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call (last call ' + (now - lastExecutionTime) + 'ms ago)');
16544            return Promise.resolve();
16545          }
16546          
16547          // Set flag and timestamp immediately to prevent concurrent calls
16548          isExecuting = true;
16549          lastExecutionTime = now;
16550          this.lastInitializationTime.set('initNeonGallery', now);
16551          
16552          try {
16553            // Call original with all arguments
16554            const result = await originalInit.apply(this, args);
16555            
16556            // Capture state after initialization
16557            if (window.neonGalleryConfig) {
16558              this.captureGalleryState(window.neonGalleryConfig);
16559            }
16560            
16561            return result;
16562          } finally {
16563            // Clear execution flag after a short delay to allow for async operations
16564            setTimeout(() => {
16565              isExecuting = false;
16566            }, 100);
16567          }
16568        };
16569      }
16570      
16571      if (typeof originalReinit === 'function') {
16572        window.reinitializeGalleryAfterSave = async (savedSettings) => {
16573          // Call original function
16574          const result = await originalReinit(savedSettings);
16575          
16576          // Capture updated state
16577          this.captureGalleryState(savedSettings);
16578          
16579          return result;
16580        };
16581      }
16582    }
16583    
16584    /**
16585     * Capture current gallery state for potential reuse
16586     */
16587    captureGalleryState(settings) {
16588      if (!settings) return;
16589      
16590      // Store a deep copy of the settings to avoid reference issues
16591      try {
16592        this.previousGalleryState = JSON.parse(JSON.stringify(settings));
16593        console.log('EnhancedGallerySystem: Captured gallery state for possible reuse');
16594      } catch (e) {
16595        console.error('EnhancedGallerySystem: Failed to capture gallery state:', e);
16596      }
16597    }
16598
16599    /**
16600     * Load a specific gallery using the enhanced system
16601     * @param {Object} gallery The gallery configuration
16602     * @param {HTMLElement} container The container to load the gallery into
16603     */
16604    async loadGallery(gallery, container, options) {
16605      if (!gallery) {
16606        console.error('EnhancedGallerySystem: Invalid gallery configuration');
16607        return false;
16608      }
16609
16610      const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
16611
16612      // Coalesce concurrent loads for the same gallery — await instead of wiping mid-flight
16613      if (this._loadInProgress && this._loadingGalleryId === gallery.id && !forceGalleryReload) {
16614        const pageViewStillShowing = window._activeContentType === 'page' ||
16615          !!document.querySelector('.gallery-container .page-container, .page-container');
16616        if (!pageViewStillShowing) {
16617          console.log('EnhancedGallerySystem: Awaiting in-flight load for gallery ' + gallery.id);
16618          return this._loadInProgress;
16619        }
16620        console.log('EnhancedGallerySystem: Abandoning standalone in-flight load for page embed', gallery.id);
16621        this._loadInProgress = null;
16622        this._loadingGalleryId = null;
16623      }
16624
16625      const now = Date.now();
16626      const lastInitTime = this.lastInitializationTime.get(gallery.id);
16627      if (!forceGalleryReload && lastInitTime && (now - lastInitTime) < 1000) {
16628        const hasContent = !!document.querySelector('.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry');
16629        const pageViewStillShowing = window._activeContentType === 'page' ||
16630          !!document.querySelector('.gallery-container .page-container, .page-container');
16631        if (hasContent && !pageViewStillShowing) {
16632          console.log('EnhancedGallerySystem: Gallery ' + gallery.id + ' recently loaded with content, skipping re-init');
16633          if (container) container.style.opacity = '1';
16634          if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
16635            window.syncGalleryOptionsButtonVisibility();
16636          }
16637          return true;
16638        }
16639      }
16640
16641      if (forceGalleryReload) {
16642        console.log('EnhancedGallerySystem: force reload requested for gallery ' + gallery.id);
16643        this.lastInitializationTime.delete(gallery.id);
16644        this._loadInProgress = null;
16645        this._loadingGalleryId = null;
16646      }
16647
16648      const loadTask = this._executeGalleryLoad(gallery, container, now, options);
16649      this._loadInProgress = loadTask;
16650      this._loadingGalleryId = gallery.id;
16651      try {
16652        return await loadTask;
16653      } finally {
16654        if (this._loadInProgress === loadTask) {
16655          this._loadInProgress = null;
16656          this._loadingGalleryId = null;
16657        }
16658      }
16659    }
16660
16661    async _executeGalleryLoad(gallery, container, startedAt, options) {
16662      this.lastInitializationTime.set(gallery.id, startedAt);
16663      const contentLoadGeneration = options && options.contentLoadGeneration != null
16664        ? options.contentLoadGeneration
16665        : window.__hydraContentLoadGeneration;
16666      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16667        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16668        : contentLoadGeneration === window.__hydraContentLoadGeneration &&
16669          (!window._activeContentType || window._activeContentType === 'gallery') &&
16670          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16671
16672      stopAutoAdvanceTimer();
16673
16674      // CRITICAL: Verify container exists
16675      if (!container) {
16676        console.error('EnhancedGallerySystem: Gallery container is null or undefined');
16677        return false;
16678      }
16679
16680  // Create a unique instance ID for this gallery load (needed before script init)
16681  const galleryInstanceId = Date.now();
16682
16683  try {
16684    // Clean up previous gallery state and prepare container BEFORE loading scripts
16685    this.cleanupGalleryState(container);
16686    const containerSetup = this.setupCorrectGalleryContainer(container);
16687    if (!containerSetup) {
16688      throw new Error('Failed to set up gallery container');
16689    }
16690    this.setupGalleryParameters(gallery, galleryInstanceId);
16691  } catch (prepError) {
16692    console.error('EnhancedGallerySystem: Failed to prepare gallery container:', prepError);
16693    return false;
16694  }
16695
16696  // Make sure the system is initialized (scripts load with container already in DOM)
16697  try {
16698    await this.initialize();
16699  } catch (error) {
16700    console.error('EnhancedGallerySystem: Failed to initialize gallery framework:', error);
16701    //this.showErrorMessage(container, 'Failed to initialize gallery system. Please try again later.');
16702    return false;
16703  }
16704
16705  if (!stillCurrent()) {
16706    console.log('EnhancedGallerySystem: Aborting stale load after framework initialize for', gallery.id);
16707    return false;
16708  }
16709
16710  if (!(await this.ensureNeonGalleryUtilsReady(container))) {
16711    console.error('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings unavailable after script load');
16712    return false;
16713  }
16714
16715  if (!stillCurrent()) {
16716    console.log('EnhancedGallerySystem: Aborting stale load after utils ready for', gallery.id);
16717    return false;
16718  }
16719
16720  // Update current gallery tracking
16721  this.currentGalleryId = gallery.id;
16722  
16723  console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`);
16724
16725  try {
16726    // Determine which initialization method to use
16727    if (this.canUseReinitForGallery(gallery, options)) {
16728      const result = await this.reinitializeGallery(gallery, container, options);
16729      return stillCurrent() ? result : false;
16730    } else {
16731      const result = await this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16732      return stillCurrent() ? result : false;
16733    }
16734  } catch (error) {
16735    console.error('EnhancedGallerySystem: Error loading gallery:', error);
16736    if (container) {
16737      console.error('EnhancedGallerySystem: Error loading gallery:', error);
16738      //this.showErrorMessage(container, 'There was a problem loading the gallery. Please try again later.');
16739    }
16740    return false;
16741  }
16742}
16743    
16744    normalizeGalleryIdentityValue(value) {
16745      if (value === undefined || value === null || value === '') return '';
16746      return String(value);
16747    }
16748
16749    previousGalleryStateBelongsTo(gallery, previousState) {
16750      if (!gallery || !previousState) return false;
16751
16752      const targetGalleryId = this.normalizeGalleryIdentityValue(gallery.id);
16753      const targetPageId = this.normalizeGalleryIdentityValue(gallery.pageId);
16754      if (!targetGalleryId && !targetPageId) return false;
16755
16756      const previousGalleryId = this.normalizeGalleryIdentityValue(
16757        previousState.galleryId || previousState.loadedGalleryId
16758      );
16759      const previousPageId = this.normalizeGalleryIdentityValue(
16760        previousState.pageId || previousState.initialPageUuid
16761      );
16762
16763      if (!previousGalleryId && !previousPageId) return false;
16764      if (previousGalleryId && targetGalleryId && previousGalleryId !== targetGalleryId) {
16765        return false;
16766      }
16767      if (previousPageId && targetPageId && previousPageId !== targetPageId) {
16768        return false;
16769      }
16770
16771      return (
16772        (previousGalleryId && targetGalleryId && previousGalleryId === targetGalleryId) ||
16773        (previousPageId && targetPageId && previousPageId === targetPageId)
16774      );
16775    }
16776
16777    /**
16778     * Determine if we can use reinitialization for this gallery
16779     */
16780    canUseReinitForGallery(gallery, options) {
16781      if (options?.forceReload === true || window._forceGalleryReload === true) {
16782        return false;
16783      }
16784      // Check if reinitializeGalleryAfterSave function exists
16785      if (typeof window.reinitializeGalleryAfterSave !== 'function') {
16786        return false;
16787      }
16788      
16789      // Check if we have a previous state to reuse
16790      if (!this.previousGalleryState) {
16791        return false;
16792      }
16793
16794      if (!this.previousGalleryStateBelongsTo(gallery, this.previousGalleryState)) {
16795        console.log('EnhancedGallerySystem: Cannot use reinit - previous gallery state does not match target gallery');
16796        return false;
16797      }
16798      
16799      // Check if the gallery has the necessary properties for reinit
16800      if (!gallery.pageId || !gallery.galleryOptions) {
16801        return false;
16802      }
16803      
16804      // CRITICAL: Check if the gallery wrapper actually exists in the DOM
16805      // Reinitialization requires an existing gallery structure
16806      const galleryWrapper = document.querySelector('.gallery-wrapper');
16807      const neonContainer = document.getElementById('neon-gallery-container');
16808      if (!galleryWrapper || !neonContainer) {
16809        console.log('EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found');
16810        return false;
16811      }
16812      
16813      return true;
16814    }
16815    
16816    /**
16817     * Reinitialize a gallery using the reinitialization process
16818     */
16819async reinitializeGallery(gallery, container, options) {
16820  console.log('EnhancedGallerySystem: Using reinitialization process for gallery', gallery.id);
16821  
16822  try {
16823    // CRITICAL: Ensure CSS is loaded before attempting reinitialization
16824    ensureGalleryStylesLoaded();
16825    
16826    // We should already have the proper container, but just in case:
16827    if (!document.getElementById('neon-gallery-container')) {
16828      this.setupCorrectGalleryContainer(container);
16829    }
16830    
16831    // CRITICAL: Ensure config is set up with all required fields before reinitialization
16832    const galleryOptions = gallery.galleryOptions || {};
16833    window.neonGalleryConfig = {
16834      useData: true,
16835      useCDN: true,
16836      version: 'live',
16837      manualCollectionName: galleryOptions.manualCollectionName || "mod",
16838      layoutType: galleryOptions.layoutType || "grid",
16839      ...galleryOptions,
16840      siteId: window.siteId || gallery.siteId || '',
16841      galleryId: gallery.id,
16842      // Explicitly include all permanent storage fields
16843      isPerma: galleryOptions.isPerma,
16844      permaURL: galleryOptions.permaURL,
16845      loadTxId: galleryOptions.loadTxId,
16846      loadPermaURL: galleryOptions.loadPermaURL,
16847      containerSelector: '#neon-gallery-container'
16848    };
16849    
16850    // Prepare settings by merging previous state with new gallery options.
16851    // Identity fields always come from the menu target, never leftover snapshot ids.
16852    const settings = {
16853      ...this.previousGalleryState,
16854      ...gallery.galleryOptions,
16855      pageId: gallery.pageId,
16856      initialPageUuid: gallery.pageId,
16857      galleryId: gallery.id,
16858      loadedGalleryId: gallery.id,
16859      forceReload: true,
16860      containerSelector: '#neon-gallery-container' // Add this explicitly
16861    };
16862    
16863    // Call reinitializeGalleryAfterSave with the merged settings
16864    window._reinitializeGalleryFromSystem = true;
16865    let result;
16866    try {
16867      result = await window.reinitializeGalleryAfterSave(settings);
16868    } finally {
16869      window._reinitializeGalleryFromSystem = false;
16870    }
16871    
16872    if (result) {
16873      console.log('EnhancedGallerySystem: Gallery reinitialization completed successfully');
16874      // Fade in the container
16875      container.style.opacity = '1';
16876      return true;
16877    } else {
16878      throw new Error('Reinitialization did not complete successfully');
16879    }
16880  } catch (error) {
16881    console.error('EnhancedGallerySystem: Error reinitializing gallery:', error);
16882    // Fall back to standard initialization - ensure config is set up properly
16883    const galleryInstanceId = Date.now();
16884    // Re-setup parameters and config for the fallback
16885    this.setupGalleryParameters(gallery, galleryInstanceId);
16886    return this.initializeNewGallery(gallery, container, galleryInstanceId, options);
16887  }
16888}
16889    
16890    /**
16891     * Initialize a gallery from scratch (fallback method)
16892     */
16893    async initializeNewGallery(gallery, container, galleryInstanceId, options) {
16894      console.log('EnhancedGallerySystem: Using standard initialization for gallery', gallery.id);
16895      const contentLoadGeneration = options && options.contentLoadGeneration != null
16896        ? options.contentLoadGeneration
16897        : window.__hydraContentLoadGeneration;
16898      const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function'
16899        ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)
16900        : (!window._activeContentType || window._activeContentType === 'gallery') &&
16901          (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id));
16902      
16903      try {
16904        // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements
16905        ensureGalleryStylesLoaded();
16906        
16907        // Clean up container
16908        if (container) {
16909          container.innerHTML = '';
16910        } else {
16911          console.error('EnhancedGallerySystem: Container is null in initializeNewGallery');
16912          return false;
16913        }
16914        
16915        // Create gallery container with unique ID
16916        const galleryContainerId = `gallery-container-${gallery.id}-${galleryInstanceId}`;
16917        const neonGalleryContainer = document.createElement('div');
16918        neonGalleryContainer.id = 'neon-gallery-container';
16919        neonGalleryContainer.className = 'gallery-direct-content';
16920        neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString());
16921        neonGalleryContainer.setAttribute('data-instance-id', galleryInstanceId.toString());
16922        neonGalleryContainer.setAttribute('data-unique-id', galleryContainerId);
16923        neonGalleryContainer.style.width = '100%';
16924        neonGalleryContainer.style.height = '100%';
16925        container.appendChild(neonGalleryContainer);
16926        
16927        // CRITICAL: Initialize neonGalleryState if it doesn't exist
16928        if (!window.neonGalleryState) {
16929          window.neonGalleryState = {
16930            initialized: false,
16931            initializing: false,
16932            domInitialized: false,
16933            configApplied: false,
16934            initializationPromise: null
16935          };
16936        }
16937
16938        // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields
16939        // Don't rely on conditional logic - always set it correctly here
16940        const galleryOptions = gallery.galleryOptions || {};
16941        window.neonGalleryConfig = {
16942          useData: true,
16943          useCDN: true,
16944          version: 'live',
16945          manualCollectionName: galleryOptions.manualCollectionName || "mod",
16946          layoutType: galleryOptions.layoutType || "grid",
16947          ...galleryOptions,
16948          siteId: window.siteId || gallery.siteId || '',
16949          galleryId: gallery.id,
16950          galleryInstanceId: galleryInstanceId,
16951          containerSelector: '#neon-gallery-container',
16952          // Explicitly include all permanent storage fields (critical for proper initialization)
16953          isPerma: galleryOptions.isPerma,
16954          permaURL: galleryOptions.permaURL,
16955          loadTxId: galleryOptions.loadTxId,
16956          loadPermaURL: galleryOptions.loadPermaURL
16957        };
16958        
16959        // Call the initialization function directly if available
16960        if (typeof window.initNeonGallery === 'function') {
16961          console.log('EnhancedGallerySystem: Calling direct initialization method');
16962          
16963          // Additional check: prevent duplicate initNeonGallery calls within 1 second
16964          const initKey = 'init_' + gallery.id + '_' + galleryInstanceId;
16965          const lastInitCall = this.lastInitializationTime.get(initKey);
16966          const now = Date.now();
16967          if (lastInitCall && (now - lastInitCall) < 1000) {
16968            console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call for gallery ' + gallery.id + ' 
16968(last call ' + (now - lastInitCall) + 'ms ago)');
16969            container.style.opacity = '1';
16970            return true;
16971          }
16972          this.lastInitializationTime.set(initKey, now);
16973          
16974          try {
16975            // Wait a tiny bit to ensure DOM updates have propagated
16976            await new Promise(resolve => setTimeout(resolve, 10));
16977            if (!stillCurrent()) {
16978              console.log('EnhancedGallerySystem: Aborting stale initializeNewGallery before initNeonGallery');
16979              return false;
16980            }
16981            
16982            const result = await window.initNeonGallery();
16983            if (!stillCurrent()) {
16984              console.log('EnhancedGallerySystem: Ignoring stale initializeNewGallery completion');
16985              return false;
16986            }
16987            console.log('EnhancedGallerySystem: Direct initialization successful');
16988            
16989            // Save state for future loads
16990            if (window.neonGalleryConfig) {
16991              this.captureGalleryState(window.neonGalleryConfig);
16992            }
16993            
16994            // Fade in the container
16995            container.style.opacity = '1';
16996            return true;
16997          } catch (e) {
16998            if (!stillCurrent()) {
16999              console.log('EnhancedGallerySystem: Ignoring stale direct initialization error');
17000              return false;
17001            }
17002            console.error('EnhancedGallerySystem: Direct initialization failed:', e);
17003            throw e;
17004          }
17005        } else {
17006          // If initNeonGallery isn't available, use event-based initialization
17007          console.log('EnhancedGallerySystem: Using event-based initialization fallback');
17008          
17009          // Create an event to trigger initialization
17010          const initEvent = new CustomEvent('neon-gallery-init', { 
17011            detail: { 
17012              galleryId: gallery.id,
17013              instanceId: galleryInstanceId,
17014              containerId: galleryContainerId
17015            }
17016          });
17017          document.dispatchEvent(initEvent);
17018          
17019          // Wait a short time to see if initialization completes
17020          await new Promise(resolve => setTimeout(resolve, 500));
17021          if (!stillCurrent()) {
17022            console.log('EnhancedGallerySystem: Ignoring stale event-based gallery initialization');
17023            return false;
17024          }
17025          
17026          // Fade in regardless of result - gallery script should handle displaying errors
17027          container.style.opacity = '1';
17028          return true;
17029        }
17030      } catch (error) {
17031        console.error('EnhancedGallerySystem: Error in standard initialization:', error);
17032        if (!stillCurrent()) {
17033          console.log('EnhancedGallerySystem: Ignoring stale standard initialization error');
17034          return false;
17035        }
17036        if (container) {
17037          this.showErrorMessage(container, 'Failed to load gallery content. Please try again later.');
17038        }
17039        return false;
17040      }
17041    }
17042
17043    /**
17044     * Clean up gallery state without removing loaded scripts
17045     */
17046    cleanupGalleryState(container) {
17047      console.log('EnhancedGallerySystem: Cleaning up gallery state');
17048      
17049      // Stop any ongoing singles autoplay from the previous gallery
17050      if (typeof window.stopSinglesAutoplay === 'function') {
17051        try {
17052          window.stopSinglesAutoplay(); 
17053          console.log('EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.');
17054        } catch (error) {
17055          console.warn('EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:', error);
17056        }
17057      } else {
17058        console.warn('EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.');
17059      }
17060
17061      // 1. Reset global gallery state variables
17062      const resetGlobals = [
17063        'neonGalleryInitComplete', 'neonGalleryInitInProgress', 
17064        'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryCache', 'neonGalleryImages', 
17065        'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 
17066        'neonGalleryInstance', 'currentGalleryId', 'galleryData', 
17067        'imageCache', 'thumbnailCache'
17068      ];
17069      
17070      resetGlobals.forEach(prop => {
17071        if (window[prop] !== undefined) {
17072          window[prop] = null;
17073        }
17074      });
17075      
17076      // 2. Reset neonGalleryState if it's a proper object with properties
17077      if (window.neonGalleryState && typeof window.neonGalleryState === 'object') {
17078        window.neonGalleryState = {
17079          initialized: false,
17080          initializing: false,
17081          domInitialized: false,
17082          configApplied: false,
17083          initializationPromise: null
17084        };
17085      }
17086      
17087      // 3. Remove lightbox elements and overlays
17088      document.querySelectorAll('#neon-lightbox, .neon-lightbox, .lightbox-container').forEach(el => {
17089        if (el && el.parentNode) {
17090          el.parentNode.removeChild(el);
17091        }
17092      });
17093
17094      const staleOptionsOverlay = document.getElementById('optionsOverlay');
17095      if (staleOptionsOverlay && staleOptionsOverlay.parentNode) {
17096        staleOptionsOverlay.parentNode.removeChild(staleOptionsOverlay);
17097      }
17098      window.optionsVisible = false;
17099      
17100      // 4. Remove any gallery-specific DOM elements
17101      document.querySelectorAll(
17102        '.neon-gallery-wrapper, .fullscreen-overlay, ' + 
17103        '.neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, ' +
17104        '.neon-thumbnails, .neon-image, .neon-caption, ' +
17105        '.neon-controls, .neon-pagination'
17106      ).forEach(el => {
17107        if (el && el.parentNode) {
17108          el.parentNode.removeChild(el);
17109        }
17110      });
17111      
17112      // 5. Clear container if provided and valid
17113      if (container) {
17114        container.innerHTML = '';
17115        container.style.opacity = '0';
17116      }
17117      
17118      // 6. Clear keyboard listeners to prevent multiple listeners from different galleries
17119      if (typeof window.removeAllLightboxKeyboardListeners === 'function') {
17120        window.removeAllLightboxKeyboardListeners();
17121        console.log('EnhancedGallerySystem: Cleared all lightbox keyboard listeners during cleanup');
17122      } else if (window.currentLightboxKeyboardListener) {
17123        document.removeEventListener('keydown', window.currentLightboxKeyboardListener);
17124        window.currentLightboxKeyboardListener = null;
17125        console.log('EnhancedGallerySystem: Cleared keyboard listeners during cleanup');
17126      }
17127      window.lightboxKeyboardListenerActive = false;
17128    }
17129    
17130    /**
17131     * Prepare container for gallery loading
17132     */
17133    prepareContainer(container, gallery, instanceId) {
17134      // CRITICAL: Add null check for container
17135      if (!container) {
17136        console.error('EnhancedGallerySystem: Container is null in prepareContainer');
17137        return;
17138      }
17139      
17140      try {
17141        // Add attributes and data
17142        container.setAttribute('data-gallery-id', gallery.id.toString());
17143        container.setAttribute('data-instance-id', instanceId.toString());
17144        
17145        // Add loading indicator
17146        const loadingIndicator = document.createElement('div');
17147        loadingIndicator.className = 'gallery-loading';
17148        loadingIndicator.innerHTML = `
17149          <div class="spinner"></div>
17150          <div class="loading-text">Loading Gallery...</div>
17151        `;
17152        
17153        container.appendChild(loadingIndicator);
17154      } catch (error) {
17155        console.error('EnhancedGallerySystem: Error preparing container:', error);
17156      }
17157    }
17158    
17159
17160
17161
17162setupGalleryParameters(gallery, galleryInstanceId) {
17163  // Set up gallery parameters with enhanced uniqueness
17164  window.Parameters = window.Parameters || {};
17165  
17166  // Clone and extend existing Parameters to avoid losing important values
17167  const existingParams = { ...window.Parameters };
17168  
17169  window.Parameters = {
17170    ...existingParams,
17171    SiteAlias: window.location.hostname,
17172    InitialPageUuid: gallery.pageId,
17173    InitialPageAlias: gallery.slug || this.slugify(gallery.title),
17174    // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode
17175    // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active
17176    isInEditor: window.isEditing || 
17177                localStorage.getItem('hydra_is_admin') === 'true' || 
17178                window.isAdmin === true || 
17179                document.body.classList.contains('hydra-admin'),
17180    siteId: window.siteId || gallery.siteId || '',
17181    isHydra: true,
17182    
17183    // CRITICAL: These properties help the gallery script find the container
17184    galleryContainerId: 'neon-gallery-container',
17185    containerSelector: '#neon-gallery-container',
17186    
17187    // Additional properties
17188    galleryInstanceId: galleryInstanceId,
17189    galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`,
17190    loadTimestamp: galleryInstanceId,
17191    loadedGalleryId: gallery.id
17192  };
17193
17194  // Set up gallery config - CRITICAL: Include all permanent storage fields
17195  const galleryOptions = gallery.galleryOptions || {};
17196  window.neonGalleryConfig = {
17197    useData: true,
17198    useCDN: true,
17199    version: 'live',
17200    manualCollectionName: galleryOptions.manualCollectionName || "mod",
17201    layoutType: galleryOptions.layoutType || "grid",
17202    ...galleryOptions,
17203    siteId: window.siteId || gallery.siteId || '',
17204    galleryId: gallery.id,
17205    galleryInstanceId: galleryInstanceId,
17206    
17207    // Add container selector here too
17208    containerSelector: '#neon-gallery-container',
17209    
17210    // CRITICAL: Explicitly include all permanent storage fields to ensure they're available
17211    // These are needed for proper gallery initialization and data loading
17212    isPerma: galleryOptions.isPerma,
17213    permaURL: galleryOptions.permaURL,
17214    loadTxId: galleryOptions.loadTxId,
17215    loadPermaURL: galleryOptions.loadPermaURL
17216  };
17217}
17218    
17219    /**
17220     * Show error message in container
17221     */
17222    showErrorMessage(container, message) {
17223      // CRITICAL: Add null check for container
17224      if (!container) {
17225        console.error('EnhancedGallerySystem: Container is null in showErrorMessage');
17226        return;
17227      }
17228      
17229      try {
17230        // Clear container
17231        container.innerHTML = '';
17232        
17233        // Create error message element
17234        const errorElement = document.createElement('div');
17235        errorElement.className = 'gallery-error';
17236        errorElement.innerHTML = `
17237          <h3>Error Loading Gallery</h3>
17238          <p>${message}</p>
17239        `;
17240        container.appendChild(errorElement);
17241        container.style.opacity = '1';
17242      } catch (error) {
17243        console.error('EnhancedGallerySystem: Error showing error message:', error);
17244      }
17245    }
17246    
17247    /**
17248     * Utility function to convert title to slug
17249     */
17250    slugify(text) {
17251      return text
17252        .toLowerCase()
17253        .replace(/s+/g, '-')
17254        .replace(/[^w-]+/g, '')
17255        .replace(/--+/g, '-')
17256        .trim();
17257    }
17258  }
17259
17260  // Create a singleton instance
17261  window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem();
17262
17263  /**
17264   * Enhanced loadGallery function using the EnhancedGallerySystem
17265   * This replaces the existing loadGallery function
17266   */
17267const enhancedLoadGallery = async function(id, event, options) {
17268    const originalLoadGallery = window.originalLoadGallery;
17269    const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true;
17270
17271    stopAutoAdvanceTimer();
17272    if (typeof dismissGalleryPasswordOverlay === 'function') {
17273      dismissGalleryPasswordOverlay();
17274    }
17275
17276    if (event && event.target && event.target.isConnected === false) {
17277      console.log('enhancedLoadGallery: Ignoring navigation from detached element');
17278      return;
17279    }
17280
17281    if (event) {
17282      event.preventDefault();
17283      event.stopPropagation();
17284    }
17285
17286    let galleryContainer = document.querySelector('.gallery-container');
17287
17288    if (!galleryContainer) {
17289      galleryContainer = document.getElementById('galleryFrame')?.parentElement ||
17290                        document.getElementById('main-content') ||
17291                        document.getElementById('content');
17292
17293      if (!galleryContainer) {
17294        console.error('Gallery container not found, falling back to original function');
17295        if (typeof originalLoadGallery === 'function') {
17296          return originalLoadGallery(id, event, options);
17297        } else {
17298          console.error('Gallery container not found and no fallback available');
17299          return;
17300        }
17301      }
17302    }
17303
17304    // Bail out before hiding/clearing when the same gallery is already loading or rendered.
17305    // Post-save passes { forceReload: true } to bypass this path without affecting navigation.
17306    const egEarly = window.EnhancedGallerySystem;
17307    if (egEarly && !forceGalleryReload) {
17308      if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) {
17309        const pageViewStillShowing = window._activeContentType === 'page' ||
17310          !!document.querySelector('.gallery-container .page-container, .page-container');
17311        if (pageViewStillShowing) {
17312          console.log('enhancedLoadGallery: abandoning standalone in-flight load for page view', id);
17313          egEarly._loadInProgress = null;
17314          egEarly._loadingGalleryId = null;
17315        } else {
17316          console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id);
17317          await egEarly._loadInProgress;
17318          if (galleryContainer) galleryContainer.style.opacity = '1';
17319          return;
17320        }
17321      }
17322      const hasRenderedGallery = !!document.querySelector(
17323        '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry'
17324      );
17325      // Embedded galleries on a page share currentGalleryId; that is not a standalone gallery view.
17326      const pageViewStillShowing = window._activeContentType === 'page' ||
17327        !!document.querySelector('.gallery-container .page-container, .page-container');
17328      if (hasRenderedGallery && egEarly.currentGalleryId === id && !pageViewStillShowing) {
17329        console.log('enhancedLoadGallery: gallery already rendered, skipping reload');
17330        if (galleryContainer) galleryContainer.style.opacity = '1';
17331        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17332          window.syncGalleryOptionsButtonVisibility();
17333        }
17334        return;
17335      }
17336    } else if (forceGalleryReload) {
17337      console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip');
17338      if (egEarly?.lastInitializationTime?.delete) {
17339        egEarly.lastInitializationTime.delete(id);
17340      }
17341      if (egEarly) {
17342        egEarly._loadInProgress = null;
17343        egEarly._loadingGalleryId = null;
17344      }
17345    }
17346
17347    if (galleryContainer) {
17348      galleryContainer.style.opacity = '0';
17349    }
17350
17351    console.log('Loading gallery with ID (enhancedLoadGallery):', id);
17352
17353    const gallery = findGalleryById(galleries, id);
17354
17355    if (!gallery) {
17356      console.warn('No gallery found with ID:', id);
17357      if (galleryContainer) {
17358        galleryContainer.innerHTML = `
17359          <div class="gallery-error">
17360            <h3>Gallery Not Found</h3>
17361            <p>The requested gallery could not be found.</p>
17362          </div>
17363        `;
17364        galleryContainer.style.opacity = '1';
17365      }
17366      return;
17367    }
17368
17369    if (isPageMenuItem(gallery)) {
17370      return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event);
17371    }
17372
17373    window._activeContentType = 'gallery';
17374
17375    if (gallery.isSubmenu) {
17376      console.log('Gallery is submenu, skipping URL update');
17377      toggleSubmenu(id, event || { stopPropagation: () => {} });
17378      if (galleryContainer){
17379      galleryContainer.style.opacity = '1';
17380      }
17381      return;
17382    }
17383
17384    const contentLoadGeneration = beginHydraContentLoad('gallery', gallery.id, gallery.pageId);
17385    if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') {
17386      window.PageManager.clearCurrentPage();
17387    }
17388
17389    console.log('Found gallery:', gallery.title);
17390
17391    // Check for gallery-level password protection (only for visitors, not in edit mode)
17392    if (!isInEditMode() && gallery.passwordProtected) {
17393      // Use PageManager's gallery password protection methods
17394      if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && 
17395          typeof window.PageManager.createGalleryPasswordOverlay === 'function') {
17396        
17397        if (!window.PageManager.checkGalleryPasswordAccess(gallery)) {
17398          console.log('Gallery is password protected, showing overlay');
17399          window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => {
17400            if (accessGranted) {
17401              // Reload the gallery now that access is granted
17402              enhancedLoadGallery(id, event);
17403            }
17404          });
17405          return; // Exit early, will reload after password is entered
17406        }
17407      }
17408    }
17409
17410    window.activeGalleryId = id;
17411    activeGalleryId = id;
17412
17413    // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery.
17414    // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(),
17415    // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original
17416    // loadGallery) and clear the container so page content does not block gallery rendering.
17417    if (typeof window.removeGalleryScriptsWithPause === 'function') {
17418      await window.removeGalleryScriptsWithPause();
17419    }
17420    if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17421      console.log('enhancedLoadGallery: Aborting stale load after cleanup for', gallery.id);
17422      return;
17423    }
17424    if (galleryContainer) {
17425      galleryContainer.innerHTML = '';
17426    }
17427
17428    const bodyEl = document.body;
17429    if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) {
17430        bodyEl.classList.add('menu-hidden-on-page');
17431    } else {
17432        bodyEl.classList.remove('menu-hidden-on-page');
17433    }
17434    applyMenuOverlayClassForGallery(gallery);
17435
17436    try {
17437      if (typeof updateActiveStates === 'function') updateActiveStates();
17438      if (typeof updateMobileTitle === 'function') updateMobileTitle();
17439      if (typeof closeMobileMenu === 'function') closeMobileMenu();
17440      // Close the gallery options panel when navigating to a different gallery
17441      if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) {
17442        window.closeOptionsPanel();
17443      }
17444      const staleOptionsOverlay = document.getElementById('optionsOverlay');
17445      if (staleOptionsOverlay) {
17446        staleOptionsOverlay.remove();
17447      }
17448      window.optionsVisible = false;
17449      if (typeof updateURLWithGallerySlug === 'function') {
17450        updateURLWithGallerySlug(gallery);
17451      }
17452    } catch (uiError) {
17453      console.error('Error updating UI state:', uiError);
17454    }
17455
17456    try {
17457      // Add detailed logging about EnhancedGallerySystem availability
17458      console.log('🔍 Checking EnhancedGallerySystem availability:', {
17459        exists: !!window.EnhancedGallerySystem,
17460        hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17461        initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized),
17462        isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading)
17463      });
17464      
17465      if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') {
17466        console.log('✅ EnhancedGallerySystem available, calling loadGallery');
17467        const result = await window.EnhancedGallerySystem.loadGallery(
17468          gallery,
17469          galleryContainer,
17470          {
17471            ...(options || {}),
17472            ...(forceGalleryReload ? { forceReload: true } : {}),
17473            contentLoadGeneration
17474          }
17475        );
17476        if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17477          console.log('enhancedLoadGallery: Ignoring stale gallery completion for', gallery.id);
17478          return;
17479        }
17480        console.log('📊 EnhancedGallerySystem.loadGallery result:', result);
17481
17482        if (!result && typeof originalLoadGallery === 'function') {
17483          console.log('⚠️ Enhanced gallery loading failed, falling back to original function');
17484          return originalLoadGallery(id, event, options);
17485        }
17486        if (galleryContainer) {
17487          galleryContainer.style.opacity = '1';
17488        }
17489        if (typeof window.syncGalleryOptionsButtonVisibility === 'function') {
17490          window.syncGalleryOptionsButtonVisibility();
17491          setTimeout(window.syncGalleryOptionsButtonVisibility, 350);
17492        }
17493      } else {
17494        console.error('❌ EnhancedGallerySystem or its loadGallery method is not available.', {
17495          EnhancedGallerySystem: !!window.EnhancedGallerySystem,
17496          loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'),
17497          useCdnProxy: window.useCdnProxy || false
17498        });
17499        
17500        // Fallback: Try using createGalleryContext directly
17501        console.log('🔄 Attempting fallback: createGalleryContext');
17502        if (typeof createGalleryContext === 'function') {
17503          createGalleryContext(gallery, galleryContainer);
17504          if (galleryContainer) {
17505            galleryContainer.style.opacity = '1';
17506          }
17507          return;
17508        }
17509        
17510        if (galleryContainer) {
17511            galleryContainer.innerHTML = `<div class="gallery-error">Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}</div>`;
17512            galleryContainer.style.opacity = '1';
17513        }
17514        return;
17515      }
17516    } catch (error) {
17517      console.error('Error loading gallery:', error);
17518      if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) {
17519        console.log('enhancedLoadGallery: Ignoring stale gallery error for', gallery.id);
17520        return;
17521      }
17522      if (galleryContainer) {
17523        try {
17524          galleryContainer.innerHTML = `
17525            <div class="gallery-error">
17526              <h3>Error Loading Gallery</h3>
17527              <p>There was a problem loading the gallery. Please try again later.</p>
17528              <p class="error-details">${error.message}</p>
17529            </div>
17530          `;
17531          galleryContainer.style.opacity = '1';
17532        } catch (containerError) {
17533          console.error('Error showing error message in container:', containerError);
17534        }
17535      }
17536      if (typeof originalLoadGallery === 'function') {
17537        console.log('Falling back to original loadGallery after error');
17538        return originalLoadGallery(id, event, options);
17539      }
17540    }
17541  };
17542
17543  /**
17544   * Initialization function to set up the gallery system
17545   */
17546  function initializeGallerySystem() {
17547    console.log('Initializing enhanced gallery system');
17548    
17549    // Store reference to original function before replacing
17550    if (typeof window.loadGallery === 'function') {
17551      window.originalLoadGallery = window.loadGallery;
17552    }
17553    
17554    // Replace the existing loadGallery function with our optimized version
17555    window.loadGallery = enhancedLoadGallery;
17556    
17557    // Add this CSS for loading/error states
17558    addGallerySystemStyles();
17559    
17560    // Preload styles only at boot — scripts load when first gallery opens (with container ready)
17561    window.EnhancedGallerySystem.preloadStylesOnly().catch(error => {
17562      console.warn('Gallery styles preloading failed:', error);
17563    });
17564  }
17565
17566  /**
17567   * Add CSS styles for loading indicators and error messages
17568   */
17569  function addGallerySystemStyles() {
17570    const styleId = 'enhanced-gallery-system-styles';
17571    
17572    // Skip if already added
17573    if (document.getElementById(styleId)) {
17574      return;
17575    }
17576    
17577    const style = document.createElement('style');
17578    style.id = styleId;
17579    style.textContent = `
17580      /* Loading indicator */
17581      .gallery-loading {
17582        display: flex;
17583        flex-direction: column;
17584        justify-content: center;
17585        align-items: center;
17586        width: 100%;
17587        height: 100%;
17588        min-height: 200px;
17589        opacity: 0 !important;
17590      }
17591      
17592      .gallery-loading .spinner {
17593        width: 50px;
17594        height: 50px;
17595        border: 3px solid rgba(0, 0, 0, 0.1);
17596        border-radius: 50%;
17597        border-top-color: #4682B4;
17598        animation: spin 1s ease-in-out infinite;
17599        margin-bottom: 20px;
17600        opacity: 0 !important;
17601      }
17602      
17603      @keyframes spin {
17604        to { transform: rotate(360deg); }
17605      }
17606      
17607      .gallery-loading .loading-text {
17608        font-family: Arial, sans-serif;
17609        font-size: 16px;
17610        color: #555;
17611        opacity: 0;
17612      }
17613      
17614      /* Error message */
17615      .gallery-error {
17616        display: flex;
17617        flex-direction: column;
17618        justify-content: center;
17619        align-items: center;
17620        width: 100%;
17621        height: 100%;
17622        min-height: 200px;
17623        padding: 30px;
17624        text-align: center;
17625      }
17626      
17627      .gallery-error h3 {
17628        font-family: Arial, sans-serif;
17629        font-size: 24px;
17630        color:rgba(211, 47, 47, 0.25);
17631        margin-bottom: 20px;
17632      }
17633      
17634      .gallery-error p {
17635        font-family: Arial, sans-serif;
17636        font-size: 16px;
17637        color: #555;
17638        max-width: 600px;
17639        margin-bottom: 10px;
17640      }
17641      
17642      .gallery-error .error-details {
17643        font-size: 14px;
17644        color: #888;
17645        font-style: italic;
17646      }
17647    `;
17648    
17649    document.head.appendChild(style);
17650  }
17651
17652  // Initialize gallery system immediately if possible
17653  initializeGallerySystem();
17654})();
17655
17656
17657/**
17658 * Enhanced Gallery Link Interceptor
17659 * Handles clicks on gallery links to prevent full page reloads
17660 */
17661(function() {
17662  // Store any loaded gallery data for quick reference
17663  const GALLERY_DATA_CACHE = {
17664    initialized: false,
17665    galleries: [],
17666    lastUpdate: 0
17667  };
17668
17669  // Initialize when DOM is ready to catch early clicks
17670  document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor);
17671  
17672  // Also initialize when window loads to ensure complete gallery data
17673  window.addEventListener('load', function() {
17674    // Use a small delay to ensure galleries have fully initialized
17675    setTimeout(initGalleryLinkInterceptor, 200);
17676  });
17677  
17678  // Additional init on first user interaction
17679  document.addEventListener('click', function initOnFirstClick() {
17680    initGalleryLinkInterceptor();
17681    // Remove this listener after first click
17682    document.removeEventListener('click', initOnFirstClick);
17683  }, { once: true });
17684  
17685  // Main initialization function
17686  function initGalleryLinkInterceptor() {
17687    // Prevent multiple initializations
17688    if (window._galleryLinkInterceptorInitialized) {
17689      // If already initialized, just refresh the gallery data
17690      refreshGalleryCache();
17691      return;
17692    }
17693    
17694    window._galleryLinkInterceptorInitialized = true;
17695    console.log('Initializing enhanced gallery link interceptor');
17696    
17697    // Update our cache with current galleries data
17698    refreshGalleryCache();
17699    
17700    // Get primary containers that might contain links
17701    const galleryContainer = document.querySelector('.gallery-container');
17702    const mainContainer = document.querySelector('.container') || document.body;
17703    
17704    if (!galleryContainer && !mainContainer) {
17705      console.warn('No gallery containers found, link interceptor partially initialized');
17706    }
17707    
17708    // Add global click handler to catch all link clicks
17709    document.addEventListener('click', handleGlobalLinkClick, true);
17710    
17711    // Try to intercept links from iframes - critical for gallery content
17712    interceptAllIframeLinks();
17713    
17714    // Set up a mutation observer to handle dynamically added content
17715    setupMutationObserver();
17716    
17717    // Log initialization with site details
17718    const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17719    console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`);
17720    
17721    // Create global helper for debugging
17722    window.debugGalleryLinks = function() {
17723      return {
17724        inPreviewMode: isPreviewMode,
17725        interceptorInitialized: window._galleryLinkInterceptorInitialized,
17726        cachedGalleries: GALLERY_DATA_CACHE.galleries.length,
17727        urlPath: window.location.pathname,
17728        siteGuid: getSiteGuidFromPath()
17729      };
17730    };
17731  }
17732  
17733  // Update the gallery cache when gallery data changes
17734function refreshGalleryCache() {
17735  // Initialize GALLERY_DATA_CACHE if it doesn't exist
17736  if (!window.GALLERY_DATA_CACHE) {
17737    window.GALLERY_DATA_CACHE = {
17738      initialized: false,
17739      galleries: [],
17740      lastUpdate: 0
17741    };
17742  }
17743  
17744  // Check for global galleries variable first
17745  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
17746    // Use the global galleries array
17747    window.GALLERY_DATA_CACHE.galleries = galleries;
17748    window.GALLERY_DATA_CACHE.initialized = true;
17749    window.GALLERY_DATA_CACHE.lastUpdate = Date.now();
17750    window.galleries = galleries;
17751
17752    console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`);
17753    return;
17754  }
17755    
17756    // Debug log
17757    console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`);
17758  }
17759  
17760  // Global click handler - catches all link clicks
17761  function handleGlobalLinkClick(e) {
17762    // Find if a link was clicked
17763    const link = e.target.closest('a');
17764    if (!link || !link.href) return;
17765    
17766    // Skip if the link uses special targets or modifiers
17767    if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
17768    
17769    // Skip for hash links or javascript: links
17770    if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
17771    
17772    try {
17773      // Parse the URL to work with it
17774      const url = new URL(link.href);
17775      
17776      // Only intercept links to the same domain
17777      if (url.hostname !== window.location.hostname) return;
17778      
17779      // Skip API and resource URLs
17780      if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return;
17781      
17782      console.log('Link intercepted:', url.pathname);
17783      
17784      // For preview URLs, we need to handle the path specially
17785      const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
17786      const siteGuid = getSiteGuidFromPath();
17787      
17788      console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid);
17789
17790      // Site Graph card links carry data-sg-target-id — use SPA navigation (not preview iframe fallback)
17791      if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) {
17792        let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries))
17793          ? galleries
17794          : window.galleries;
17795        const sgItem = Array.isArray(galleriesData)
17796          ? findGalleryById(galleriesData, link.dataset.sgTargetId)
17797          : null;
17798        if (sgItem) {
17799          e.preventDefault();
17800          e.stopPropagation();
17801          if (sgItem.isPage && window.loadPage) {
17802            window.loadPage(sgItem.id);
17803          } else if (window.loadGallery) {
17804            window.loadGallery(sgItem.id);
17805          }
17806          let targetUrl = url.href;
17807          if (isPreviewMode && siteGuid) {
17808            const pathname = url.pathname.startsWith(`/${siteGuid}/`) ?
17809              url.pathname :
17810              `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17811            const newUrl = new URL(url);
17812            newUrl.pathname = pathname;
17813            targetUrl = newUrl.href;
17814          }
17815          window.history.pushState(
17816            { galleryId: sgItem.id },
17817            sgItem.title || document.title,
17818            targetUrl
17819          );
17820          document.title = formatGalleryDocumentTitle(sgItem.title);
17821          window.scrollTo(0, 0);
17822          return;
17823        }
17824      }
17825      
17826      // Check if this is internal navigation first
17827      let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
17828      
17829      console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none');
17830      
17831      if (matchingItem) {
17832        // Prevent default navigation (full page reload)
17833        e.preventDefault();
17834        e.stopPropagation();
17835        
17836        console.log('Found matching gallery for link:', matchingItem.title);
17837
17838        const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function'
17839          ? window.isNestedGalleryLinkModeEnabled()
17840          : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes);
17841        const isGridGalleryLink = !!(link.closest('.masonry-item') ||
17842          (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') &&
17843           link.querySelector('img') &&
17844           !link.closest('#neon-lightbox, .neon-lightbox')));
17845
17846        if (!matchingItem.isPage &&
17847            nestedLinkModeActive &&
17848            isGridGalleryLink &&
17849            typeof window.openNestedGalleryLightbox === 'function') {
17850          void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => {
17851            if (!opened) {
17852              console.warn('Nested gallery lightbox failed — staying on parent gallery (no loadGallery fallback)');
17853            }
17854          });
17855          return;
17856        }
17857        
17858        // Use the existing navigation functions
17859        if (matchingItem.isPage && window.loadPage) {
17860          window.loadPage(matchingItem.id);
17861        } else if (window.loadGallery) {
17862          window.loadGallery(matchingItem.id);
17863        }
17864        
17865        // Construct proper URL for preview mode
17866        let targetUrl = url.href;
17867        if (isPreviewMode && siteGuid) {
17868          // Ensure we keep the GUID in the URL path
17869          const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
17870            url.pathname : 
17871            `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
17872          
17873          const newUrl = new URL(url);
17874          newUrl.pathname = pathname;
17875          targetUrl = newUrl.href;
17876        }
17877        
17878        // Update URL without reload
17879        window.history.pushState(
17880          { galleryId: matchingItem.id },
17881          matchingItem.title || document.title,
17882          targetUrl
17883        );
17884        
17885        // Update page title
17886        document.title = formatGalleryDocumentTitle(matchingItem.title);
17887        
17888        // Scroll to top
17889        window.scrollTo(0, 0);
17890        
17891        return;
17892      } 
17893      // Special case for preview mode - even if no matching gallery found
17894      else if (isPreviewMode && siteGuid) {
17895        // We're in preview mode with a GUID - intercept the navigation
17896        // to maintain the GUID in the URL. Only do this when #galleryFrame
17897        // can load content; otherwise allow SPA onclick handlers to run.
17898        
17899        console.log('No direct match but in preview mode - handling navigation');
17900
17901        const frame = document.getElementById('galleryFrame');
17902
17903        if (!frame) {
17904          console.warn('Gallery frame not found - allowing default/SPA handlers');
17905        } else {
17906          // Prevent default navigation
17907          e.preventDefault();
17908          e.stopPropagation();
17909          
17910          // Construct a URL that preserves the GUID
17911          let newPathname = url.pathname;
17912          if (!newPathname.startsWith(`/${siteGuid}/`)) {
17913            // Add the GUID to the path if not already there
17914            newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
17915          }
17916          
17917          // Create the preview-friendly URL for browser history
17918          const browserUrl = new URL(window.location.origin);
17919          browserUrl.pathname = newPathname;
17920          
17921          console.log('Constructing preview URL with GUID:', browserUrl.href);
17922          
17923          // IMPORTANT: For gallery navigation, we need to create a special URL
17924          // that tells the gallery system to load the content directly
17925          console.log('Loading preview path in iframe:', newPathname);
17926          
17927          // First update browser URL to show the correct path
17928          window.history.pushState(
17929            { customPath: newPathname },
17930            document.title,
17931            browserUrl.href
17932          );
17933          
17934          // Create direct path to the gallery content
17935          const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
17936          
17937          // For direct gallery paths, we need to load the gallery with special parameters
17938          // This tells the gallery system to load content from this path
17939          const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`;
17940          
17941          // Two approaches to loading the content:
17942          
17943          // 1. Use the gallery wrapper approach
17944          const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
17945            `?siteId=${siteGuid}` +
17946            `&path=${encodeURIComponent(cleanPath)}` + 
17947            `&directPath=true` +
17948            `&t=${Date.now()}`; // Cache buster
17949          
17950          console.log('Loading gallery via wrapper URL:', wrapperUrl);
17951          frame.src = wrapperUrl;
17952          
17953          // Clear any active gallery ID since we're navigating directly
17954          if (window.activeGalleryId) {
17955            console.log('Clearing active gallery ID during direct navigation');
17956            window.activeGalleryId = null;
17957          }
17958          
17959          return; // Critical: prevent further navigation
17960        }
17961      }
17962      
17963      console.log('No matching gallery or special handling - allow normal navigation');
17964    } catch (error) {
17965      console.error('Error processing link click:', error);
17966    }
17967  }
17968  
17969  // Handle all iframes on the page
17970  function interceptAllIframeLinks() {
17971    // Look for any iframe that might contain gallery content
17972    const iframes = document.querySelectorAll('iframe');
17973    
17974    iframes.forEach(iframe => {
17975      if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) {
17976        // Primary gallery iframe - high priority
17977        setupIframeIntercept(iframe, true);
17978      } else {
17979        // Any other iframe - still try to intercept
17980        setupIframeIntercept(iframe, false);
17981      }
17982    });
17983    
17984    console.log(`Attempted to intercept links in ${iframes.length} iframes`);
17985  }
17986  
17987  // Setup link interception for a specific iframe
17988  function setupIframeIntercept(iframe, isPrimary) {
17989    try {
17990      // Skip if iframe is not fully loaded yet
17991      if (!iframe.contentWindow || !iframe.contentDocument) {
17992        // Add load event to try again when loaded
17993        iframe.addEventListener('load', function() {
17994          setupIframeIntercept(iframe, isPrimary);
17995        });
17996        return;
17997      }
17998      
17999      // Try to access the iframe content - might fail due to same-origin policy
18000      try {
18001        const iframeDoc = iframe.contentDocument;
18002        const iframeWin = iframe.contentWindow;
18003        
18004        // Add event listener for all link clicks in the iframe
18005        iframeDoc.addEventListener('click', function(e) {
18006          const link = e.target.closest('a');
18007          if (!link || !link.href) return;
18008          
18009          // Skip special cases
18010          if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return;
18011          if (link.href.includes('#') || link.href.startsWith('javascript:')) return;
18012          
18013          try {
18014            const url = new URL(link.href);
18015            
18016            // Check if same domain or relative URL
18017            const isSameDomain = url.hostname === window.location.hostname || 
18018                                url.hostname === iframe.src.hostname || 
18019                                !url.hostname;
18020            
18021            if (isSameDomain) {
18022              const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
18023              const siteGuid = getSiteGuidFromPath();
18024              
18025              // First try to find a matching gallery/page
18026              const matchingItem = parent.findGalleryByPath ? 
18027                parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : 
18028                findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
18029              
18030              if (matchingItem) {
18031                // We found a match - prevent default and navigate
18032                e.preventDefault();
18033                e.stopPropagation();
18034                
18035                console.log('Iframe: Found matching content for link:', matchingItem.title);
18036                
18037                // Access parent window functions
18038                if (matchingItem.isPage && parent.loadPage) {
18039                  parent.loadPage(matchingItem.id);
18040                } else if (parent.loadGallery) {
18041                  parent.loadGallery(matchingItem.id);
18042                }
18043                
18044                // Update URL in parent window
18045                let targetUrl = url.href;
18046                if (isPreviewMode && siteGuid) {
18047                  // Ensure GUID is preserved in URL
18048                  const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
18049                    url.pathname : 
18050                    `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
18051                  
18052                  const newUrl = new URL(window.location.origin + pathname);
18053                  targetUrl = newUrl.href;
18054                }
18055                
18056                // Update browser URL
18057                parent.history.pushState(
18058                  { galleryId: matchingItem.id },
18059                  matchingItem.title || document.title,
18060                  targetUrl
18061                );
18062                
18063                // Update parent window title
18064                parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent);
18065                
18066                // Scroll parent to top
18067                parent.scrollTo(0, 0);
18068                return;
18069              } 
18070              // Special handling for preview mode - critical fix here!
18071              else if (isPreviewMode && siteGuid) {
18072                // Special handling for preview URLs
18073                e.preventDefault();
18074                e.stopPropagation();
18075                
18076                console.log('Iframe: No direct match but in preview mode - handling navigation');
18077                
18078                // Construct a URL that preserves the GUID
18079                let newPathname = url.pathname;
18080                if (!newPathname.startsWith(`/${siteGuid}/`)) {
18081                  newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
18082                }
18083                
18084                // Create a URL for the browser history
18085                const browserUrl = new URL(window.location.origin);
18086                browserUrl.pathname = newPathname;
18087                
18088                console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href);
18089                
18090                // Update browser URL in parent
18091                parent.history.pushState(
18092                  { customPath: newPathname },
18093                  document.title,
18094                  browserUrl.href
18095                );
18096                
18097                // Handle the navigation in the parent window
18098                if (isPrimary && parent.document.getElementById('galleryFrame')) {
18099                  const galleryFrame = parent.document.getElementById('galleryFrame');
18100                  
18101                  // Create clean path for the gallery system
18102                  const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
18103                  
18104                  // Create wrapper URL for gallery content
18105                  const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
18106                    `?siteId=${siteGuid}` +
18107                    `&path=${encodeURIComponent(cleanPath)}` + 
18108                    `&directPath=true` +
18109                    `&t=${Date.now()}`; // Cache buster
18110                  
18111                  console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl);
18112                  galleryFrame.src = wrapperUrl;
18113                  
18114                  // Clear any active gallery ID since we're navigating directly
18115                  if (parent.activeGalleryId) {
18116                    console.log('Iframe: Clearing active gallery ID during direct navigation');
18117                    parent.activeGalleryId = null;
18118                  }
18119                } else {
18120                  // Fallback - navigate normally in case we can't handle it
18121                  console.log('Iframe: Gallery frame not found or not primary, navigating normally');
18122                  window.location.href = browserUrl.href;
18123                }
18124                return;
18125              }
18126            }
18127          } catch (iframeError) {
18128            console.error('Error handling iframe link:', iframeError);
18129          }
18130        });
18131        
18132        if (isPrimary) {
18133          console.log('Successfully intercepted links in primary gallery iframe');
18134        }
18135      } catch (sameOriginError) {
18136        // This is expected for cross-origin iframes
18137        if (isPrimary) {
18138          console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.');
18139          
18140          // Apply a fallback method - add postMessage listener
18141          setupPostMessageListener(iframe);
18142        }
18143      }
18144    } catch (error) {
18145      console.warn('Error setting up iframe link interception:', error);
18146    }
18147  }
18148  
18149  // Fallback: use postMessage for cross-origin iframes
18150  function setupPostMessageListener(iframe) {
18151    // Listen for messages from the iframe
18152    window.addEventListener('message', function(event) {
18153      try {
18154        // Verify the message is from our iframe
18155        if (event.source !== iframe.contentWindow) return;
18156        
18157        const data = event.data;
18158        
18159        // Check if this is a link click event
18160        if (data && data.type === 'linkClick' && data.href) {
18161          console.log('Received link click message from iframe:', data.href);
18162          
18163          // Create a URL object to work with the link
18164          const url = new URL(data.href);
18165          
18166          // Check if it's a local navigation
18167          if (url.hostname === window.location.hostname) {
18168            const isPreviewMode = window.location.hostname === 'preview.neonsky.app';
18169            const siteGuid = getSiteGuidFromPath();
18170            
18171            // Try to find a matching gallery
18172            const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid);
18173            
18174            if (matchingItem) {
18175              console.log('Found matching gallery for postMessage link:', matchingItem.title);
18176              
18177              // Navigate using the appropriate function
18178              if (matchingItem.isPage && window.loadPage) {
18179                window.loadPage(matchingItem.id);
18180              } else if (window.loadGallery) {
18181                window.loadGallery(matchingItem.id);
18182              }
18183              
18184              // Update URL without reload
18185              let targetUrl = url.href;
18186              if (isPreviewMode && siteGuid) {
18187                // Ensure GUID is preserved
18188                const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 
18189                  url.pathname : 
18190                  `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`;
18191                
18192                const newUrl = new URL(window.location.origin + pathname);
18193                targetUrl = newUrl.href;
18194              }
18195              
18196              window.history.pushState(
18197                { galleryId: matchingItem.id },
18198                matchingItem.title || document.title,
18199                targetUrl
18200              );
18201              
18202              // Update page title
18203              document.title = formatGalleryDocumentTitle(matchingItem.title);
18204              
18205              return;
18206            } 
18207            // Special handling for preview mode - Fixed!
18208            else if (isPreviewMode && siteGuid) {
18209              console.log('No direct match but in preview mode - handling navigation via postMessage');
18210              
18211              // Construct URL that preserves the GUID
18212              let newPathname = url.pathname;
18213              if (!newPathname.startsWith(`/${siteGuid}/`)) {
18214                newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`;
18215              }
18216              
18217              // Create browser URL
18218              const browserUrl = new URL(window.location.origin);
18219              browserUrl.pathname = newPathname;
18220              
18221              // Update browser URL 
18222              window.history.pushState(
18223                { customPath: newPathname },
18224                document.title,
18225                browserUrl.href
18226              );
18227              
18228              // Get the main gallery frame - NOT the iframe that sent the message
18229              const galleryFrame = document.getElementById('galleryFrame');
18230              if (galleryFrame) {
18231                // Create clean path without GUID
18232                const cleanPath = newPathname.replace(`/${siteGuid}/`, '/');
18233                
18234                // Create wrapper URL
18235                const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 
18236                  `?siteId=${siteGuid}` +
18237                  `&path=${encodeURIComponent(cleanPath)}` + 
18238                  `&directPath=true` +
18239                  `&t=${Date.now()}`; // Cache buster
18240                
18241                console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl);
18242                galleryFrame.src = wrapperUrl;
18243                
18244                // Clear any active gallery ID
18245                if (window.activeGalleryId) {
18246                  window.activeGalleryId = null;
18247                }
18248                
18249                return;
18250              } else {
18251                console.warn('Gallery frame not found - cannot load content via postMessage');
18252              }
18253            }
18254          }
18255        }
18256      } catch (error) {
18257        console.error('Error processing postMessage from iframe:', error);
18258      }
18259    });
18260    
18261    // Inject a script into the iframe if possible to send messages back
18262    try {
18263      // We might be able to inject a script tag even in cross-origin scenarios
18264      // if the iframe is not yet loaded or using document.write
18265      iframe.addEventListener('load', function() {
18266        try {
18267          const script = iframe.contentDocument.createElement('script');
18268          script.textContent = `
18269            // Link interception script injected from parent
18270            document.addEventListener('click', function(e) {
18271              const link = e.target.closest('a');
18272              if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
18273                // Send message to parent window
18274                window.parent.postMessage({
18275                  type: 'linkClick',
18276                  href: link.href,
18277                  pathname: link.pathname
18278                }, '*');
18279                
18280                // Prevent default navigation
18281                e.preventDefault();
18282              }
18283            });
18284            console.log('Gallery link interceptor injected from parent');
18285          `;
18286          iframe.contentDocument.head.appendChild(script);
18287        } catch (e) {
18288          // This will likely fail for cross-origin iframes
18289          console.log('Could not inject script into iframe - expected for cross-origin frames');
18290        }
18291      });
18292    } catch (error) {
18293      console.warn('Error trying to inject script into iframe:', error);
18294    }
18295  }
18296  
18297  // Observer for new content/iframes
18298  function setupMutationObserver() {
18299    if (!window.MutationObserver) return;
18300    
18301    const observer = new MutationObserver(function(mutations) {
18302      let newIframesFound = false;
18303      
18304      mutations.forEach(function(mutation) {
18305        if (mutation.type === 'childList' && mutation.addedNodes.length) {
18306          // Check if any new iframes were added
18307          mutation.addedNodes.forEach(function(node) {
18308            // Check if the node is an iframe or contains iframes
18309            if (node.nodeName === 'IFRAME') {
18310              newIframesFound = true;
18311              setupIframeIntercept(node, node.id === 'galleryFrame');
18312            } else if (node.querySelectorAll) {
18313              const iframes = node.querySelectorAll('iframe');
18314              if (iframes.length > 0) {
18315                newIframesFound = true;
18316                iframes.forEach(function(iframe) {
18317                  setupIframeIntercept(iframe, iframe.id === 'galleryFrame');
18318                });
18319              }
18320            }
18321          });
18322        }
18323      });
18324      
18325      // If new iframes were found, update our cache
18326      if (newIframesFound) {
18327        refreshGalleryCache();
18328      }
18329    });
18330    
18331    // Observe the entire document
18332    observer.observe(document.body, {
18333      childList: true,
18334      subtree: true
18335    });
18336  }
18337  
18338  // Helper to check if a URL is a resource rather than a page
18339  function isResourceUrl(path) {
18340    const resourceExtensions = [
18341      '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', 
18342      '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2'
18343    ];
18344    return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext));
18345  }
18346  
18347  // Get the site GUID from the path in preview mode
18348  function getSiteGuidFromPath() {
18349    if (window.location.hostname === 'preview.neonsky.app') {
18350      const pathParts = window.location.pathname.split('/').filter(
18350Boolean);
18351      if (pathParts.length > 0) {
18352        return pathParts[0];
18353      }
18354    }
18355    return null;
18356  }
18357  
18358
18359/**
18360 * Enhanced gallery path matching with improved accuracy:
18361 * 
18362 * This function finds the appropriate gallery based on URL path with better
18363 * prioritization and more precise matching criteria.
18364 * 
18365 * @param {string} path - The URL path to match
18366 * @param {boolean} isPreviewMode - Whether we're in preview mode
18367 * @param {string} siteGuid - The site GUID (for preview mode)
18368 * @returns {Object|null} The matching gallery or null if none found
18369 */
18370function findGalleryByPath(path, isPreviewMode, siteGuid) {
18371  // Ensure path is a string
18372  if (typeof path !== 'string') {
18373    console.warn('Invalid path provided to findGalleryByPath:', path);
18374    return null;
18375  }
18376
18377  // Determine which galleries array to use - check global variable first
18378  let galleriesData;
18379  let galleryCount = 0;
18380  
18381  if (typeof galleries !== 'undefined' && Array.isArray(galleries)) {
18382    galleriesData = galleries;
18383    galleryCount = galleries.length;
18384  } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) {
18385    galleriesData = window.galleries;
18386    galleryCount = window.galleries.length;
18387  } else if (window.GALLERY_DATA_CACHE && 
18388             window.GALLERY_DATA_CACHE.initialized && 
18389             Array.isArray(window.GALLERY_DATA_CACHE.galleries)) {
18390    galleriesData = window.GALLERY_DATA_CACHE.galleries;
18391    galleryCount = window.GALLERY_DATA_CACHE.galleries.length;
18392  } else {
18393    console.warn('No galleries array available');
18394    return null;
18395  }
18396  
18397  console.log(`Searching for match among ${galleryCount} galleries`);
18398
18399  // Strip leading/trailing slashes without regex — template-literal embedding mangles / escapes
18400  // (e.g. /^/|/$/g becomes /^/|/$/g → 0), which left "/" unnormalized and broke home matching.
18401  function stripEdgeSlashes(value) {
18402    return String(value || '').split('/').filter(Boolean).join('/');
18403  }
18404  
18405  // Normalize the path for comparison (handle trailing/leading slashes)
18406  let normalizedPath = stripEdgeSlashes(path).toLowerCase();
18407  const originalNormalizedPath = normalizedPath; // Keep original for logging
18408  
18409  // If this is a preview URL, we need to handle the path differently
18410  if (isPreviewMode && siteGuid) {
18411    // Check if the path starts with the GUID
18412    if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) {
18413      // Remove the GUID prefix
18414      normalizedPath = normalizedPath.substring(siteGuid.length + 1);
18415    } else if (normalizedPath === siteGuid.toLowerCase()) {
18416      // This is just the GUID with no additional path
18417      normalizedPath = '';
18418    }
18419  }
18420  
18421  // Create a dash-normalized version (all sequences of dashes become single dashes)
18422  // This handles both double-dash patterns and any other dash inconsistencies
18423  const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-');
18424  
18425  console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`);
18426  
18427  // For empty paths, try to find the homepage
18428  if (!normalizedPath) {
18429    // Look for a gallery marked as home page
18430    const homePage = galleriesData.find(g => g.isHomePage === true);
18431    if (homePage) {
18432      console.log('Empty path - returning homepage:', homePage.title);
18433      return homePage;
18434    }
18435  }
18436
18437  // Array to store potential matches with their confidence levels
18438  const potentialMatches = [];
18439  
18440  // Process each gallery to find potential matches with confidence levels
18441  galleriesData.forEach(gallery => {
18442    // Skip invalid galleries
18443    if (!gallery || !gallery.id) return;
18444    
18445    // Store matching details for debugging
18446    const matchInfo = {
18447      gallery,
18448      confidence: 0,
18449      matchType: null,
18450      usedNormalizedDashes: false
18451    };
18452    
18453    // 1. EXACT SLUG MATCH (highest confidence)
18454    if (gallery.slug) {
18455      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18456      
18457      // Try original path
18458      if (gallerySlug === normalizedPath) {
18459        matchInfo.confidence = 100; // Max confidence
18460        matchInfo.matchType = 'exact_slug';
18461        potentialMatches.push({...matchInfo});
18462        return; // Continue to next gallery
18463      }
18464      
18465      // Try dash-normalized path
18466      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18467      if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) {
18468        matchInfo.confidence = 95; // Very high confidence
18469        matchInfo.matchType = 'exact_slug_normalized_dashes';
18470        matchInfo.usedNormalizedDashes = true;
18471        potentialMatches.push({...matchInfo});
18472        return; // Continue to next gallery
18473      }
18474    }
18475    
18476    // 2. EXACT URL PATH MATCH (very high confidence)
18477    if (gallery.url) {
18478      let galleryPath = gallery.url.toLowerCase();
18479      
18480      if (galleryPath.startsWith('http')) {
18481        try {
18482          const urlObj = new URL(galleryPath);
18483          galleryPath = urlObj.pathname;
18484        } catch (e) {
18485          // If URL parsing fails, use the string as is
18486        }
18487      }
18488      
18489      galleryPath = stripEdgeSlashes(galleryPath);
18490      
18491      // Try original path
18492      if (galleryPath === normalizedPath) {
18493        matchInfo.confidence = 90; // Very high confidence
18494        matchInfo.matchType = 'exact_url_path';
18495        potentialMatches.push({...matchInfo});
18496        return; // Continue to next gallery
18497      }
18498      
18499      // Try dash-normalized path
18500      const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-');
18501      if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) {
18502        matchInfo.confidence = 85; // High confidence
18503        matchInfo.matchType = 'exact_url_path_normalized_dashes';
18504        matchInfo.usedNormalizedDashes = true;
18505        potentialMatches.push({...matchInfo});
18506        return; // Continue to next gallery
18507      }
18508    }
18509    
18510    // 3. TITLE SLUG MATCH (high confidence)
18511    if (gallery.title) {
18512      const titleSlug = gallery.title.toLowerCase()
18513        .replace(/s+/g, '-')
18514        .replace(/[^w-]+/g, '')
18515        .replace(/--+/g, '-')
18516        .trim();
18517      
18518      // Try original path
18519      if (titleSlug === normalizedPath) {
18520        matchInfo.confidence = 80; // High confidence
18521        matchInfo.matchType = 'title_slug_match';
18522        potentialMatches.push({...matchInfo});
18523        return; // Continue to next gallery
18524      }
18525      
18526      // Try dash-normalized path
18527      const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-');
18528      if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) {
18529        matchInfo.confidence = 75; // Good confidence
18530        matchInfo.matchType = 'title_slug_match_normalized_dashes';
18531        matchInfo.usedNormalizedDashes = true;
18532        potentialMatches.push({...matchInfo});
18533        return; // Continue to next gallery
18534      }
18535    }
18536    
18537    // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA
18538    if (gallery.slug && normalizedPath.length > 5) {
18539      const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase();
18540      const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-');
18541      
18542      // Only if paths are substantial (to avoid false positives)
18543      if (normalizedPath.length >= 5 && gallerySlug.length >= 5) {
18544        // Try matching significant portions with dash normalization
18545        
18546        // If the normalized paths contain each other with substantial overlap
18547        if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || 
18548            gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) {
18549          
18550          // Calculate overlap ratio based on the shorter string
18551          const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / 
18552                               Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length);
18553          
18554          // Only consider matches with substantial overlap (over 70%)
18555          if (overlapRatio >= 0.7) {
18556            const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap
18557            
18558            matchInfo.confidence = score;
18559            matchInfo.matchType = 'partial_match_normalized';
18560            matchInfo.usedNormalizedDashes = true;
18561            matchInfo.overlapRatio = overlapRatio;
18562            potentialMatches.push({...matchInfo});
18563          }
18564        }
18565      }
18566    }
18567  });
18568  
18569  // No potential matches found
18570  if (potentialMatches.length === 0) {
18571    console.log('No gallery match found for path:', path);
18572    return null;
18573  }
18574  
18575  // Sort potential matches by confidence (highest first)
18576  potentialMatches.sort((a, b) => b.confidence - a.confidence);
18577  
18578  // Get the best match
18579  const bestMatch = potentialMatches[0];
18580  
18581  // Log detailed debugging info about the match
18582  console.log(`Best match for "${normalizedPath}":`, {
18583    title: bestMatch.gallery.title,
18584    id: bestMatch.gallery.id,
18585    matchType: bestMatch.matchType,
18586    confidence: bestMatch.confidence,
18587    usedNormalizedDashes: bestMatch.usedNormalizedDashes,
18588    totalMatches: potentialMatches.length
18589  });
18590  
18591  // If we have multiple close matches, log them for debugging
18592  if (potentialMatches.length > 1 && 
18593      potentialMatches[1].confidence >= bestMatch.confidence - 10) {
18594    console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({
18595      title: m.gallery.title,
18596      id: m.gallery.id,
18597      matchType: m.matchType,
18598      confidence: m.confidence
18599    }
18599)));
18600  }
18601  
18602  // Return the best matching gallery
18603  return bestMatch.gallery;
18604}
18605  
18606  // Make helpful functions available globally
18607  window.findGalleryByPath = findGalleryByPath;
18608  window.findGalleryById = findGalleryById;
18609  window.refreshGalleryCache = refreshGalleryCache;
18610  window.getSiteGuidFromPath = getSiteGuidFromPath;
18611  if (typeof getSiteGalleriesArray === 'function') {
18612    window.getSiteGalleriesArray = getSiteGalleriesArray;
18613  }
18614})();
18615
18616
18617
18618    </script>
18618
18619  </div>
18620  
18621<!-- Sidebar element form for adding new elements -->
18622<div class="sidebar-element-form pe-edit-shell" id="sidebarElementForm" style="display: none;" data-nosnippet>
18623<p class="pe-edit-shell__title">Add sidebar element</p>
18624<p class="pe-hint">Select the type of element to add:</p>
18625
18626<div class="radio-group">
18627  <label>
18628    <input type="radio" name="sidebarElementType" value="text" checked> Text Block
18629  </label>
18630  <label>
18631    <input type="radio" name="sidebarElementType" value="image"> Image
18632  </label>
18633  <label>
18634    <input type="radio" name="sidebarElementType" value="social"> Social Icons
18635  </label>
18636  <label>
18637    <input type="radio" name="sidebarElementType" value="spacer"> Spacer
18638  </label>
18639</div>
18640
18641<div class="pe-footer-actions">
18642<button type="button" class="pe-btn pe-btn--primary" onclick="addSidebarElement()">Add element</button>
18643<button type="button" class="pe-btn" onclick="toggleSidebarElementForm()">Cancel</button>
18644</div>
18645</div>
18646
18647 <div class="add-form pe-edit-shell" id="addForm" data-nosnippet>
18648  <p class="pe-edit-shell__title">Add to menu</p>
18649  <input type="text" id="galleryTitle" placeholder="Gallery Title">
18650 <div class="radio-group">
18651   <label>
18652    <input type="radio" name="galleryType" value="integrated" checked> Gallery
18653  </label>
18654    <label>
18655    <input type="radio" name="galleryType" value="page"> Page
18656  </label>
18657  <label>
18658    <input type="radio" name="galleryType" value="folder"> Folder
18659  </label>
18660  <label>
18661    <input type="radio" name="galleryType" value="spacer"> Spacer
18662  </label>
18663    <label>
18664    <input type="radio" name="galleryType" value="external"> External URL
18665  </label>
18666</div>
18667  <div id="urlInputContainer">
18668    <input type="text" id="galleryUrl" placeholder="Gallery URL">
18669  </div>
18670  <div id="galleryParentContainer" style="display: none;" aria-hidden="true">
18671    <label for="galleryParent">Parent Menu:</label>
18672    <select id="galleryParent">
18673      <option value="">None (Top Level)</option>
18674    </select>
18675  </div>
18676  <div class="pe-footer-actions">
18677    <button type="button" class="pe-btn pe-btn--primary" onclick="addGallery()">Save</button>
18678  </div>
18679    
18679<script>
18680
18681
18682      // Add event listeners to the radio buttons
18683      document.addEventListener('DOMContentLoaded', () => {
18684        const radioButtons = document.querySelectorAll('input[name="galleryType"]');
18685        radioButtons.forEach(radio => {
18686          radio.addEventListener('change', function() {
18687            const urlContainer = document.getElementById('urlInputContainer');
18688            if (this.value === 'external') {
18689              urlContainer.style.display = 'block';
18690            } else {
18691              urlContainer.style.display = 'none';
18692            }
18693          });
18694        });
18695      });
18696    </script>
18696
18697  </div>
18698
18699<div class="metadata-editor pe-edit-shell" id="metadataEditor" style="display: none; max-height: 90vh; overflow-y: auto;" data-nosnippet>
18700  <h3 class="pe-edit-shell__title">Site Metadata</h3>
18701  
18702  <div class="form-group">
18703    <label for="metaTitle">Page Title (Search Results & Browsers)</label>
18704    <input type="text" id="metaTitle" class="form-control" placeholder="Enter site title for search results">
18705    <small>Recommended length: 50-60 characters</small>
18706  </div>
18707  
18708  <div class="form-group">
18709    <label for="metaDescription">Meta Description</label>
18710    <textarea id="metaDescription" class="form-control" rows="3" placeholder="Enter site description for search results"></textarea>
18711    <small>Recommended length: 150-160 characters</small>
18712  </div>
18713
18714  <div class="form-group">
18715    <label for="ogImageUrl">Social preview image (Open Graph / X / iMessage)</label>
18716    <input type="hidden" id="ogImageUrl" class="form-control" value="">
18717    <button type="button" id="uploadOgImageBtn" class="pe-btn upload-image-btn">
18718      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18719        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18720        <polyline points="17 8 12 3 7 8"></polyline>
18721        <line x1="12" y1="3" x2="12" y2="15"></line>
18722      </svg>
18723      <span id="uploadOgImageBtnText">Upload image</span>
18724    </button>
18725    <div id="ogImagePreviewContainer"></div>
18726    <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>
18727  </div>
18728  
18729  <div class="form-group">
18730    <label for="googleAnalytics">Google Analytics Code</label>
18731    <input type="text" id="googleAnalytics" class="form-control" placeholder="Enter GA-XXXXXXXXXX">
18732    <small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X</small>
18733  </div>
18734  
18735  <div class="form-group">
18736    <label for="contentLanguage">Content Language</label>
18737    <select id="contentLanguage" class="form-control">
18738      <option value="en">English (en)</option>
18739      <option value="es">Spanish / Español (es)</option>
18740      <option value="fr">French / Français (fr)</option>
18741      <option value="ne">Nepali / नेपाली (ne)</option>
18742      <option value="de">German / Deutsch (de)</option>
18743      <option value="it">Italian / Italiano (it)</option>
18744      <option value="pt">Portuguese / Português (pt)</option>
18745      <option value="ru">Russian / Русский (ru)</option>
18746      <option value="zh">Chinese / 中文 (zh)</option>
18747      <option value="ja">Japanese / 日本語 (ja)</option>
18748      <option value="ko">Korean / 한국어 (ko)</option>
18749      <option value="ar">Arabic / العربية (ar)</option>
18750      <option value="hi">Hindi / हिन्दी (hi)</option>
18751    </select>
18752    <small>Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.</small>
18753  </div>
18754  
18755  <div class="form-group">
18756    <label>
18757      <input type="checkbox" id="noIndexToggle"> 
18758      Hide site from search engines (noindex)
18759    </label>
18760    <small>Use this if the site is under development or shouldn't appear in search results</small>
18761  </div>
18762
18763  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18764    <h4 style="margin-bottom: 8px;">Schema / Identity</h4>
18765    <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>
18766    <label style="display: block; margin-bottom: 6px;">This site represents:</label>
18767    <div style="display: flex; gap: 20px; margin-bottom: 4px;">
18768      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18769        <input type="radio" name="schemaType" id="schemaTypePerson" value="Person" onchange="updateSchemaFields()"> Person / Individual
18770      </label>
18771      <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;">
18772        <input type="radio" name="schemaType" id="schemaTypeOrganization" value="Organization" onchange="updateSchemaFields()"> Organization / Business
18773      </label>
18774    </div>
18775    <small>Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.</small>
18776  </div>
18777
18778  <div class="form-group" id="schemaJobTitleGroup" style="display: none;">
18779    <label for="schemaJobTitle">Job Title</label>
18780    <input type="text" id="schemaJobTitle" class="form-control" placeholder="">
18781    <small>Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").</small>
18782  </div>
18783
18784  <div class="form-group" id="schemaCityGroup" style="display: none;">
18785    <label for="schemaCity">City / Location</label>
18786    <input type="text" id="schemaCity" class="form-control" placeholder="">
18787    <small>Improves visibility in location-based searches.</small>
18788  </div>
18789
18790  <div class="form-group" id="schemaEmailGroup" style="display: none;">
18791    <label for="schemaEmail">Contact Email (for structured data)</label>
18792    <input type="text" id="schemaEmail" class="form-control" placeholder="e.g. [email protected]">
18793    <small>Embedded in structured data only — not displayed on the page.</small>
18794  </div>
18795
18796  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18797    <h4 style="margin-bottom: 8px;">
18797Image licensing (optional)</h4>
18798    <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>
18799    <label for="imageLicenseUrl">License page URL</label>
18800    <input type="url" id="imageLicenseUrl" class="form-control" placeholder="https://example.com/licensing">
18801    <small>A public page that states how images may be used (your terms, or a Creative Commons license).</small>
18802  </div>
18803
18804  <div class="form-group">
18805    <label for="acquireLicensePageUrl">Acquire license page URL</label>
18806    <input type="url" id="acquireLicensePageUrl" class="form-control" placeholder="https://example.com/contact">
18807    <small>A public page where someone can request or buy a license. Both URLs are required, or leave both empty.</small>
18808  </div>
18809
18810  <div class="form-group">
18811    <label>
18812      <input type="checkbox" id="metadataCopyrightEnabled">
18813      Enable sidebar copyright footer
18814    </label>
18815    <small>When enabled, the copyright text shows at the bottom of the sidebar.</small>
18816  </div>
18817
18818  <div class="form-group" id="metadataCopyrightTextGroup">
18819    <label for="metadataCopyrightText">Copyright Text</label>
18820    <input type="text" id="metadataCopyrightText" class="form-control" placeholder="© 2025 Your Name">
18821    <small>This text appears in the sidebar footer and inside the copyright overlay.</small>
18822  </div>
18823  
18824  <div class="form-group">
18825    <label for="faviconUrl">Favicon</label>
18826    <input type="hidden" id="faviconUrl" class="form-control">
18827    <button type="button" id="uploadFaviconBtn" class="pe-btn upload-image-btn">
18828      <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
18829        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
18830        <polyline points="17 8 12 3 7 8"></polyline>
18831        <line x1="12" y1="3" x2="12" y2="15"></line>
18832      </svg>
18833      <span id="uploadFaviconBtnText">Upload Favicon</span>
18834    </button>
18835    <div id="faviconPreviewContainer"></div>
18836    <small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)</small>
18837  </div>
18838
18839  <div class="form-group" id="customPublicSiteBypassGroup" hidden style="display: none; border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18840    <h4 style="margin-bottom: 12px;">Custom HTML site (full bypass)</h4>
18841    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18842      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.
18843      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.
18844      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).
18845    </p>
18846    <label>
18847      <input type="checkbox" id="customPublicSiteEnabled" />
18848      Serve uploaded HTML as the public site (bypass Hydra shell on <code>/</code>)
18849    </label>
18850    <input type="hidden" id="customPublicSiteHtmlR2Key" value="" />
18851    <input type="hidden" id="customPublicSiteHtmlFilenameStore" value="" />
18852    <div class="form-group" style="margin-top: 10px;">
18853      <button type="button" id="uploadCustomPublicHtmlBtn" class="pe-btn">Upload HTML file</button>
18854      <button type="button" id="removeCustomPublicHtmlBtn" class="pe-btn">Clear file</button>
18855    </div>
18856    <p id="customPublicSiteHtmlFilenameLabel" style="font-size: 13px; color: #666; margin-top: 8px;">No HTML file uploaded yet.</p>
18857  </div>
18858
18859  <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;">
18860    <h4 style="margin-bottom: 8px;">Site-wide password (browser login)</h4>
18861    <p style="font-size: 13px; color: #555; margin-bottom: 12px;">
18862      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
18863      <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.
18864    </p>
18865    <label>
18866      <input type="checkbox" id="siteWideBasicAuthEnabled" onchange="updateSiteWideBasicAuthFieldState()" />
18867      Require HTTP Basic password for the entire site
18868    </label>
18869    <div id="siteWideBasicAuthFields" style="display: none; margin-top: 12px;">
18870      <div class="form-group">
18871        <label for="siteWideBasicAuthUsername">Username</label>
18872        <input type="text" id="siteWideBasicAuthUsername" class="form-control" autocomplete="username" />
18873      </div>
18874      <div class="form-group">
18875        <label for="siteWideBasicAuthPassword">Password</label>
18876        <input type="password" id="siteWideBasicAuthPassword" class="form-control" autocomplete="new-password" />
18877        <small>Stored in your site configuration. Use a strong password; anyone with it can view the site.</small>
18878      </div>
18879      <div class="form-group">
18880        <label for="siteWideBasicAuthRealm">Realm (optional)</label>
18881        <input type="text" id="siteWideBasicAuthRealm" class="form-control" placeholder="Site" />
18882        <small>Label shown in the browser login dialog.</small>
18883      </div>
18884    </div>
18885  </div>
18886  
18887  <div class="form-actions pe-footer-actions">
18888    <button type="button" class="pe-btn pe-btn--primary" onclick="saveMetadata()">Save Metadata</button>
18889    <button type="button" class="pe-btn" onclick="toggleMetadataEditor()">Cancel</button>
18890  </div>
18891</div>
18892
18893<script>
18894var customPublicSiteOptionHeld = false;
18895var customPublicSiteOptionKeyBound = false;
18896
18897function isMetadataEditorOpen() {
18898  var editor = document.getElementById('metadataEditor');
18899  if (!editor) return false;
18900  if (window.currentOpenForm === 'metadataEditor') return true;
18901  return editor.style.display === 'block';
18902}
18903
18904function syncCustomPublicSiteBypassVisibility() {
18905  var group = document.getElementById('customPublicSiteBypassGroup');
18906  if (!group) return;
18907  var show = isMetadataEditorOpen() && customPublicSiteOptionHeld;
18908  group.hidden = !show;
18909  group.style.display = show ? 'block' : 'none';
18910}
18911
18912function bindCustomPublicSiteOptionKeyListeners() {
18913  if (customPublicSiteOptionKeyBound) return;
18914  customPublicSiteOptionKeyBound = true;
18915
18916  document.addEventListener('keydown', function(e) {
18917    if (!e.altKey) return;
18918    customPublicSiteOptionHeld = true;
18919    syncCustomPublicSiteBypassVisibility();
18920  }, true);
18921
18922  document.addEventListener('keyup', function(e) {
18923    var isOptionRelease =
18924      e.key === 'Alt' ||
18925      e.key === 'Option' ||
18926      e.code === 'AltLeft' ||
18927      e.code === 'AltRight';
18928    if (!isOptionRelease) return;
18929    customPublicSiteOptionHeld = false;
18930    syncCustomPublicSiteBypassVisibility();
18931  }, true);
18932}
18933
18934window.syncCustomPublicSiteBypassVisibility = syncCustomPublicSiteBypassVisibility;
18935window.bindCustomPublicSiteOptionKeyListeners = bindCustomPublicSiteOptionKeyListeners;
18936
18937function updateSiteWideBasicAuthFieldState() {
18938  const en = document.getElementById('siteWideBasicAuthEnabled');
18939  const fields = document.getElementById('siteWideBasicAuthFields');
18940  if (fields) fields.style.display = en && en.checked ? 'block' : 'none';
18941}
18942
18943function updateSchemaFields() {
18944  const isPerson = document.getElementById('schemaTypePerson') && document.getElementById('schemaTypePerson').checked;
18945  const isOrg = document.getElementById('schemaTypeOrganization') && document.getElementById('schemaTypeOrganization').checked;
18946  const anySelected = isPerson || isOrg;
18947  const jobTitleGroup = document.getElementById('schemaJobTitleGroup');
18948  const cityGroup = document.getElementById('schemaCityGroup');
18949  const emailGroup = document.getElementById('schemaEmailGroup');
18950  if (jobTitleGroup) jobTitleGroup.style.display = isPerson ? 'block' : 'none';
18951  if (cityGroup) cityGroup.style.display = anySelected ? 'block' : 'none';
18952  if (emailGroup) emailGroup.style.display = anySelected ? 'block' : 'none';
18953}
18954
18955function uploadCustomPublicHtmlFile() {
18956  const siteId = window.Parameters && window.Parameters.siteId ? window.Parameters.siteId : (window.siteId || '');
18957  if (!siteId) {
18958    alert('Site ID not found. Refresh the page and try again.');
18959    return;
18960  }
18961  const authToken = window.ImageUploader && window.ImageUploader.getAuthToken ? window.ImageUploader.getAuthToken() : null;
18962  if (!authToken) {
18963    alert('Please log in first (press + on the page).');
18964    return;
18965  }
18966  const input = document.createElement('input');
18967  input.type = 'file';
18968  input.accept = '.html,.htm,text/html';
18969  input.addEventListener('change', function() {
18970    const file = input.files && input.files[0];
18971    if (!file) return;
18972    const formData = new FormData();
18973    formData.append('file', file);
18974    formData.append('guid', siteId);
18975    formData.append('purpose', 'custom-public-site');
18976    formData.append('pageId', '_site');
18977    formData.append('elementId', 'custom-public-html');
18978    formData.append('folderPath', '');
18979    var relUpload = typeof window.getApiUrl === 'function' ? window.getApiUrl('/api/upload-file') : '/api/upload-file';
18980    var uploadUrl = window.location && window.location.origin ? new URL(relUpload, window.location.origin).href : relUpload;
18981    fetch(uploadUrl, {
18982      method: 'POST',
18983      headers: { 'Authorization': authToken, 'X-User-Email': localStorage.getItem('hydra_auth_email') || '' },
18984      body: formData
18985    }).then(function(r) {
18986      return r.text().then(function(text) {
18987        var ct = (r.headers.get('content-type') || '').toLowerCase();
18988        if (ct.indexOf('application/json') === -1 && /<!DOCTYPE/i.test(text)) {
18989          throw new Error(
18990            'Server returned HTML instead of JSON — POST /api/upload-file is not reaching the Hydra worker. ' +
18991            'In Cloudflare, ensure this hostname has a Worker route (e.g. neonsky.app/*) so API traffic is not sent to another host or Pages.'
18992          );
18993        }
18994        if (r.status === 403 && ct.indexOf('application/json') === -1) {
18995          throw new Error(
18996            '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.'
18997          );
18998        }
18999        try {
19000          return JSON.parse(text);
19001        } catch (parseErr) {
19002          throw new Error('Server did not return JSON: ' + text.slice(0, 120) + (text.length > 120 ? '...' : ''));
19003        }
19004      });
19005    }).then(function(res) {
19006      if (res.success && res.key) {
19007        var keyEl = document.getElementById('customPublicSiteHtmlR2Key');
19008        var fnEl = document.getElementById('customPublicSiteHtmlFilenameStore');
19009        var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
19010        if (keyEl) keyEl.value = res.key;
19011        if (fnEl) fnEl.value = file.name;
19012        if (lbl) lbl.textContent = 'Current file: ' + file.name + ' — click Save Metadata below to apply.';
19013        window.siteMetadata = window.siteMetadata || {};
19014        window.siteMetadata.customPublicSiteHtmlR2Key = res.key;
19015        window.siteMetadata.customPublicSiteHtmlFilename = file.name;
19016      } else {
19017        alert(res.error || res.details || 'Upload failed');
19018      }
19019    }).catch(function(e) {
19020      alert('Upload failed: ' + (e && e.message ? e.message : 'Unknown error'));
19021    });
19022  });
19023  input.click();
19024}
19025
19026function clearCustomPublicHtmlFile() {
19027  if (!confirm('Clear the uploaded HTML file reference? (You can upload again later.)')) return;
19028  var k = document.getElementById('customPublicSiteHtmlR2Key');
19029  var fn = document.getElementById('customPublicSiteHtmlFilenameStore');
19030  var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel');
19031  if (k) k.value = '';
19032  if (fn) fn.value = '';
19033  if (lbl) lbl.textContent = 'No HTML file uploaded yet.';
19034  if (window.siteMetadata) {
19035    window.siteMetadata.customPublicSiteHtmlR2Key = '';
19036    window.siteMetadata.customPublicSiteHtmlFilename = '';
19037  }
19038}
19039
19040// Handle favicon upload button click
19041document.addEventListener('DOMContentLoaded', function() {
19042  const uploadBtn = document.getElementById('uploadFaviconBtn');
19043  if (uploadBtn) {
19044    uploadBtn.addEventListener('click', function() {
19045      uploadFavicon();
19046    });
19047  }
19048  const uploadOgBtn = document.getElementById('uploadOgImageBtn');
19049  if (uploadOgBtn) {
19050    uploadOgBtn.addEventListener('click', function() {
19051      uploadSiteOgImage();
19052    });
19053  }
19054  var upHtml = document.getElementById('uploadCustomPublicHtmlBtn');
19055  if (upHtml) upHtml.addEventListener('click', uploadCustomPublicHtmlFile);
19056  var rmHtml = document.getElementById('removeCustomPublicHtmlBtn');
19057  if (rmHtml) rmHtml.addEventListener('click', clearCustomPublicHtmlFile);
19058  bindCustomPublicSiteOptionKeyListeners();
19059  syncCustomPublicSiteBypassVisibility();
19060  
19061  // Handle content language dropdown change
19062  const contentLanguageSelect = document.getElementById('contentLanguage');
19063  if (contentLanguageSelect) {
19064    contentLanguageSelect.addEventListener('change', function() {
19065      console.warn('Content language changed to:', this.value);
19066      // Update global variable immediately
19067      window.siteMetadata.contentLanguage = this.value;
19068      console.warn('Updated window.siteMetadata.contentLanguage:', window.siteMetadata.contentLanguage);
19069    });
19070  }
19071
19072  const copyrightToggle = document.getElementById('metadataCopyrightEnabled');
19073  if (copyrightToggle) {
19074    copyrightToggle.addEventListener('change', updateMetadataCopyrightFieldState);
19075  }
19076
19077  updateMetadataCopyrightFieldState();
19078});
19079
19080// Function to handle favicon upload using ImageUploader
19081function uploadFavicon() {
19082  console.log('Starting favicon upload...');
19083  
19084  // Check if ImageUploader is available
19085  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
19086    alert('Image uploader not available. Please enter a favicon URL manually.');
19087    return;
19088  }
19089  
19090  try {
19091    // Create options for the ImageUploader
19092    const options = {
19093      title: 'Upload Favicon',
19094      containerType: 'favicon',
19095      maxWidth: 64,  // Favicons are typically small
19096      maxHeight: 64,
19097      quality: 0.9
19098    };
19099    
19100    // Call the ImageUploader
19101    window.ImageUploader.open(options, function(imageResult, imageInfo) {
19102      console.log('Favicon upload callback received:', imageInfo);
19103      
19104      // Get the favicon URL input field
19105      let faviconInput = document.getElementById('faviconUrl');
19106      console.warn('=== UPLOAD CALLBACK DEBUG ===');
19107      console.warn('Favicon input element found:', !!faviconInput);
19108      console.warn('Favicon input element type:', faviconInput ? faviconInput.type : 'N/A');
19109      console.warn('Favicon input element id:', faviconInput ? faviconInput.id : 'N/A');
19110      
19111      // If element not found, wait a bit and try again (metadata editor might not be open yet)
19112      if (!faviconInput) {
19113        console.warn('Favicon input not found, waiting 100ms and trying again...');
19114        setTimeout(() => {
19115          faviconInput = document.getElementById('faviconUrl');
19116          console.warn('Retry - Favicon input element found:', !!faviconInput);
19117          if (faviconInput) {
19118            console.warn('Retry - Setting favicon input value to:', imageInfo.url);
19119            faviconInput.value = imageInfo.url;
19120            console.warn('Retry - Favicon input value after setting:', faviconInput.value);
19121          } else {
19122            console.error('Retry - Favicon input field still not found');
19123          }
19124        }, 100);
19125        return;
19126      }
19127      
19128      // Set the uploaded image URL
19129      if (imageInfo && imageInfo.type === 'r2' && imageInfo.url) {
19130        // R2 storage - use the URL
19131        console.warn('Setting favicon input value to:', imageInfo.url);
19132        faviconInput.value = imageInfo.url;
19133        console.warn('Favicon input value after setting:', faviconInput.value);
19134        
19135        // Also store in global variable as backup
19136        window.uploadedFaviconUrl = imageInfo.url;
19137        console.warn('Stored favicon URL in global variable:', window.uploadedFaviconUrl);
19138        
19139        console.log('Favicon URL set to:', imageInfo.url);
19140        
19141        // Update button text
19142        const btnText = document.getElementById('uploadFaviconBtnText');
19143        if (btnText) {
19144          btnText.textContent = 'Change Favicon';
19145        }
19146        
19147        // Show preview
19148        showFaviconPreview(imageInfo.url);
19149      } else if (imageResult) {
19150        // Legacy base64 storage
19151        faviconInput.value = imageResult;
19152        console.log('Favicon set to base64 data');
19153        
19154        // Update button text
19155        const btnText = document.getElementById('uploadFaviconBtnText');
19156        if (btnText) {
19157          btnText.textContent = 'Change Favicon';
19158        }
19159        
19160        // Show preview
19161        showFaviconPreview(imageResult);
19162      } else {
19163        console.error('No image data received from uploader');
19164        alert('Upload failed. Please try again.');
19165        return;
19166      }
19167      
19168      // Show success message
19169      console.log('Favicon uploaded successfully');
19170    });
19171    
19172  } catch (error) {
19173    console.error('Error opening favicon uploader:', error);
19174    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
19175  }
19176}
19177
19178// Function to show a preview of the uploaded favicon
19179function showFaviconPreview(imageUrl) {
19180  const container = document.getElementById('faviconPreviewContainer');
19181  if (!container) {
19182    console.error('Favicon preview container not found');
19183    return;
19184  }
19185  
19186  container.innerHTML = 
19187    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
19188      '<img src="' + imageUrl + '" alt="Favicon Preview" style="width: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;">' +
19189      '<div style="flex: 1;">' +
19190        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current Favicon</div>' +
19191        '<div style="font-size: 12px; color: #666; margin-top: 2px;">Preview (32x32px)</div>' +
19192      '</div>' +
19193      '<button type="button" onclick="removeFavicon()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
19194        'Remove' +
19195      '</button>' +
19196    '</div>';
19197}
19198
19199// Function to remove the favicon
19200function removeFavicon() {
19201  const faviconInput = document.getElementById('faviconUrl');
19202  if (faviconInput) {
19203    faviconInput.value = '';
19204  }
19205  
19206  const container = document.getElementById('faviconPreviewContainer');
19207  if (container) {
19208    container.innerHTML = '';
19209  }
19210  
19211  const btnText = document.getElementById('uploadFaviconBtnText');
19212  if (btnText) {
19213    btnText.textContent = 'Upload Favicon';
19214  }
19215  
19216  console.log('Favicon removed');
19217}
19218
19219function uploadSiteOgImage() {
19220  if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') {
19221    alert('Image uploader not available. Refresh the page and try again.');
19222    return;
19223  }
19224  try {
19225    window.ImageUploader.open({
19226      title: 'Upload social preview image',
19227      containerType: 'page',
19228      quality: 0.92
19229    }, function(imageResult, imageInfo) {
19230      const inp = document.getElementById('ogImageUrl');
19231      const setUrl = function(url) {
19232        if (inp) inp.value = url;
19233        const btnText = document.getElementById('uploadOgImageBtnText');
19234        if (btnText) btnText.textContent = 'Change image';
19235        showOgImagePreview(url);
19236        window.siteMetadata = window.siteMetadata || {};
19237        window.siteMetadata.ogImageUrl = url;
19238      };
19239      if (imageInfo && imageInfo.url) {
19240        setUrl(imageInfo.url);
19241      } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) {
19242        setUrl(imageResult);
19243      } else {
19244        alert('Upload failed. Please try again.');
19245      }
19246    });
19247  } catch (error) {
19248    console.error('Error opening OG image uploader:', error);
19249    alert('Error opening uploader: ' + (error.message || 'Unknown error'));
19250  }
19251}
19252
19253function showOgImagePreview(imageUrl) {
19254  const container = document.getElementById('ogImagePreviewContainer');
19255  if (!container) return;
19256  container.innerHTML =
19257    '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' +
19258      '<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;">' +
19259      '<div style="flex: 1;">' +
19260        '<div style="font-size: 14px; color: #333; font-weight: 500;">Current social preview image</div>' +
19261        '<div style="font-size: 12px; color: #666; margin-top: 2px;">This image overrides the first page/gallery image for social previews</div>' +
19262      '</div>' +
19263      '<button type="button" onclick="removeOgImage()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
19264        'Remove' +
19265      '</button>' +
19266    '</div>';
19267}
19268
19269function removeOgImage() {
19270  const inp = document.getElementById('ogImageUrl');
19271  if (inp) inp.value = '';
19272  if (window.siteMetadata) window.siteMetadata.ogImageUrl = '';
19273  const container = document.getElementById('ogImagePreviewContainer');
19274  if (container) container.innerHTML = '';
19275  const btnText = document.getElementById('uploadOgImageBtnText');
19276  if (btnText) btnText.textContent = 'Upload image';
19277}
19278</script>
19278
19279
19280  <!-- Import Classic Form -->
19281<div class="import-classic-form" id="importClassicForm" style="display: none;" data-nosnippet>
19282  <h3>Import Galleries from JSON or Classic GUID</h3>
19283  <p>Paste the full site JSON into the text area below, <strong>OR</strong> enter the GUID of a classic gallery collection.</p>
19284  
19285  <div class="form-group">
19286    <label for="pastedJson">Pasted Site JSON:</label>
19287    <textarea id="pastedJson" rows="6" placeholder="Paste the entire site JSON here..."></textarea>
19288  </div>
19289
19290  <div class="import-separator">OR</div>
19291
19292  <div class="form-group">
19293    <label for="classicGuid">Classic GUID or Import ID:</label>
19294    <input type="text" id="classicGuid" placeholder="Enter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)">
19295    <small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.</small>
19296  </div>
19297
19298    <div class="form-group">
19299      <label>
19300        <input type="checkbox" id="replaceMenuCheckbox" onchange="document.getElementById('importParent').disabled = this.checked; document.getElementById('createSubmenu').disabled = this.checked;">
19301        Replace existing menu structure
19302      </label>
19303      <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>
19304  </div>
19305
19306  <div class="form-group">
19307    <label for="importParent">Import under:</label>
19308    <select id="importParent">
19309      <option value="">Top Level (No Parent)</option>
19310    </select>
19311  </div>
19312
19313  <div class="form-group">
19314    <label>
19315      <input type="checkbox" id="createSubmenu" checked>
19316      Create submenu for all imported galleries
19317    </label>
19318  </div>
19319
19320  <div class="form-group" id="submenuTitleContainer">
19321    <label for="submenuTitle">Submenu title:</label>
19322    <input type="text" id="submenuTitle" placeholder="e.g., Classic Galleries">
19323  </div>
19324
19325  <div id="importStatus" class="import-status" style="display: none;">
19326    <div class="import-progress"><div class="progress-bar"></div></div>
19327    <div class="status-message">Processing...</div>
19328  </div>
19329
19330  <div class="form-actions">
19331    <button class="btn btn-primary" onclick="importClassicGalleries()">Import from Paste/GUID</button>
19332    <button class="btn" onclick="downloadCurrentSiteJSON()">Download Current Site JSON</button>
19333    <button class="btn" onclick="toggleImportClassicForm()">Cancel</button>
19334  </div>
19335</div>
19336  <div class="tree-container">
19337    <div id="galleryTree" data-help-id="menu-tree"></div>
19338  </div>
19339
19340    <!-- Sidebar Elements Container will be inserted here by the sidebar-manager.js -->
19341  <div class="sidebar-elements-container"></div>
19342</div>
19343<div class="gallery-container" id="hydra-main-content" role="main" tabindex="-1">
19344  <iframe class="gallery-iframe" id="galleryFrame" title="Life | Simple Image Page"></iframe>
19345</div>
19346</div>
19347
19348<!-- Cookie Notice (compliance mode — only when Display Cookie Notice is enabled) -->
19349
19350
19351<script>
19352// Reveal content once layout is applied, CSS is loaded, AND gallery is initialized
19353(function() {
19354  let hasRevealed = false; // Ensure we only reveal once per page load
19355  
19356  function revealContent() {
19357    if (hasRevealed) return; // Prevent multiple calls
19358    hasRevealed = true;
19359    document.documentElement.classList.add('hydra-ready');
19360    document.body.classList.add('hydra-ready');
19361    console.log('✅ Content revealed - hydra-ready class added');
19362  }
19363  
19364  // Check if this page has a gallery container
19365  function hasGalleryContainer() {
19366    return !!document.getElementById('neon-gallery-container') || 
19367           !!document.querySelector('.gallery-wrapper') ||
19368           !!document.querySelector('.masonry-container');
19369  }
19370  
19371  // Check if gallery is already initialized
19372  function isGalleryInitialized() {
19373    return window.neonGalleryState?.initialized || 
19374           window.neonGalleryState?.domInitialized ||
19375           (window.neonGalleryUtils && window.neonGalleryUtils.isInitialized && window.neonGalleryUtils.isInitialized());
19376  }
19377  
19378  // Main reveal logic
19379  function attemptReveal() {
19380    const hasGallery = hasGalleryContainer();
19381    const galleryInitialized = isGalleryInitialized();
19382    
19383    // If no gallery, reveal immediately after CSS loads
19384    if (!hasGallery) {
19385      if (document.readyState === 'complete') {
19386        revealContent();
19387      } else {
19388        window.addEventListener('load', revealContent, { once: true });
19389      }
19390      return;
19391    }
19392    
19393    // If gallery exists and is initialized, reveal
19394    if (hasGallery && galleryInitialized) {
19395      revealContent();
19396      return;
19397    }
19398    
19399    // If gallery exists but not initialized, wait for initialization
19400    if (hasGallery && !galleryInitialized) {
19401      console.log('⏳ Waiting for gallery initialization before revealing content...');
19402      
19403      // Listen for gallery-loaded event
19404      window.addEventListener('gallery-loaded', function() {
19405        console.log('✅ Gallery-loaded event received');
19406        setTimeout(revealContent, 100); // Small delay to ensure DOM is updated
19407      }, { once: true });
19408      
19409      // Listen for page-loaded event
19410      window.addEventListener('page-loaded', function() {
19411        console.log('✅ Page-loaded event received');
19412        setTimeout(revealContent, 100);
19413      }, { once: true });
19414      
19415      // Fallback: Poll for gallery initialization (max 10 seconds)
19416      let pollAttempts = 0;
19417      const maxPollAttempts = 100; // 10 seconds (100 * 100ms)
19418      const pollInterval = setInterval(function() {
19419        pollAttempts++;
19420        if (isGalleryInitialized()) {
19421          console.log('✅ Gallery initialized detected via polling');
19422          clearInterval(pollInterval);
19423          revealContent();
19424        } else if (pollAttempts >= maxPollAttempts) {
19425          console.warn('⚠️ Gallery initialization timeout - revealing content anyway');
19426          clearInterval(pollInterval);
19427          revealContent(); // Reveal anyway to prevent blank screen
19428        }
19429      }, 100);
19430      
19431      // Safety fallback: Always reveal after 8 seconds max (prevents permanent blank screen)
19432      setTimeout(function() {
19433        if (!hasRevealed) {
19434          console.warn('⚠️ Safety timeout reached - revealing content to prevent blank screen');
19435          revealContent();
19436        }
19437      }, 8000);
19438    }
19439  }
19440  
19441  // Start the reveal process
19442  if (document.readyState === 'loading') {
19443    document.addEventListener('DOMContentLoaded', function() {
19444      // Wait a bit for scripts to initialize
19445      setTimeout(attemptReveal, 100);
19446    });
19447  } else if (document.readyState === 'interactive' || document.readyState === 'complete') {
19448    // DOM already ready, wait a bit for scripts
19449    setTimeout(attemptReveal, 100);
19450  } else {
19451    // Fallback
19452    window.addEventListener('load', attemptReveal, { once: true });
19453  }
19454})();
19455</script>
19455
19456
19457<script>
19458// ============================================
19459// LANGUAGE TRANSLATION SYSTEM
19460// ============================================
19461
19462(function() {
19463  // CRITICAL: Only initialize translation system if a languages element exists
19464  // This ensures sites without languages element have ZERO overhead
19465  const checkForLanguagesElement = function() {
19466    // Wait for SidebarManager to be initialized
19467    if (!window.SidebarManager || !window.SidebarManager.elements) {
19468      return false;
19469    }
19470    
19471    // Check if any languages element exists
19472    const hasLanguagesElement = window.SidebarManager.elements.some(el => el.type === 'languages');
19473    
19474    if (!hasLanguagesElement) {
19475      console.warn('No languages element found - translation system disabled');
19476      return false;
19477    }
19478    
19479    return true;
19480  };
19481  
19482  // Check for languages element, retry if SidebarManager not ready yet
19483  let initAttempts = 0;
19484  const maxAttempts = 10;
19485  
19486  function tryInitialize() {
19487    if (checkForLanguagesElement()) {
19488      console.warn('✅ Languages element found - initializing translation system');
19489      initializeTranslationSystem();
19490    } else {
19491      initAttempts++;
19492      if (initAttempts < maxAttempts) {
19493        // Retry in 500ms (total max wait: 5 seconds)
19494        setTimeout(tryInitialize, 500);
19495      } else {
19496        console.warn('Translation system not needed for this site - no languages element found after 5 seconds');
19497      }
19498    }
19499  }
19500  
19501  // Start checking after initial delay
19502  setTimeout(tryInitialize, 500);
19503  
19504  function initializeTranslationSystem() {
19505    console.warn('✅ Translation system initialized successfully');
19506    
19507    let currentLanguage = localStorage.getItem('hydra_language') || 'en';
19508    const originalTexts = new Map();
19509    
19510    // Expose test function for debugging
19511    window.testTranslation = function(langCode) {
19512      console.warn('🧪 Manual translation test triggered for:', langCode);
19513      console.warn('🔍 Dispatching test event...');
19514      window.dispatchEvent(new CustomEvent('hydra:language-change', {
19515        detail: { code: langCode || 'es', name: 'Test' }
19516      }));
19517      console.warn('✅ Test event dispatched - check if listener caught it above');
19518    };
19519    
19520    console.warn('✅ Event listener for "hydra:language-change" is now active');
19521    
19522    // Watch for edit mode changes and restore default language
19523    const editModeObserver = new MutationObserver(function(mutations) {
19524    mutations.forEach(function(mutation) {
19525      if (mutation.attributeName === 'class') {
19526        const isEditMode = document.body.classList.contains('edit-mode-active');
19527        if (isEditMode) {
19528          console.warn('Edit mode activated - restoring default language');
19529          // Restore original text
19530          restoreOriginalText();
19531          
19532          // Update language buttons to show default language
19533          const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19534          const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19535          
19536          document.querySelectorAll('.language-button-live').forEach(btn => {
19537            if (btn.getAttribute('data-lang-code') === defaultLang) {
19538              btn.style.background = 'var(--menu-active-color, #4682B4)';
19539            } else {
19540              btn.style.background = 'transparent';
19541            }
19542          });
19543        }
19544      }
19545    });
19546  });
19547  
19548  // Start observing body for class changes
19549  if (document.body) {
19550    editModeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
19551  }
19552  
19553  // Watch for dynamically loaded content (galleries, pages, etc.) and translate it
19554  const contentObserver = new MutationObserver(function(mutations) {
19555    // Only process if we're not in default language and not in edit mode
19556    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19557    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19558    const isEditMode = document.body.classList.contains('edit-mode-active');
19559    
19560    if (currentLanguage === defaultLang || isEditMode) {
19561      return; // Don't translate if in default language or edit mode
19562    }
19563    
19564    // Check if new text content was added
19565    let hasNewTextContent = false;
19566    mutations.forEach(function(mutation) {
19567      if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
19568        mutation.addedNodes.forEach(function(node) {
19569          if (node.nodeType === Node.ELEMENT_NODE) {
19570            // Check if the added element contains text we should translate
19571            const textElements = node.querySelectorAll('p, h1, h2, h3, .page-text');
19572            if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) {
19573              hasNewTextContent = true;
19574            }
19575          }
19576        });
19577      }
19578    });
19579    
19580    if (hasNewTextContent) {
19581      console.warn('New content detected - translating to:', currentLanguage);
19582      // Debounce to avoid translating multiple times rapidly
19583      clearTimeout(window._translationDebounce);
19584      window._translationDebounce = setTimeout(function() {
19585        translatePage(currentLanguage, defaultLang);
19586      }, 300);
19587    }
19588  });
19589  
19590  // Observe the gallery container and page content areas for new content
19591  const galleryContainer = document.querySelector('.gallery-container');
19592  if (galleryContainer) {
19593    contentObserver.observe(galleryContainer, { 
19594      childList: true, 
19595      subtree: true 
19596    });
19597  }
19598  
19599  // Also observe the main content area
19600  const mainContent = document.querySelector('.container');
19601  if (mainContent) {
19602    contentObserver.observe(mainContent, { 
19603      childList: true, 
19604      subtree: true 
19605    });
19606  }
19607  
19608  // Listen for language change events
19609  window.addEventListener('hydra:language-change', async function(e) {
19610    const { code, name } = e.detail;
19611    console.warn('🌐 Language changed to:', code, name);
19612    
19613    // CRITICAL: Ignore language changes in edit mode
19614    if (document.body.classList.contains('edit-mode-active')) {
19615      console.warn('⚠️ Edit mode active - ignoring language change');
19616      // Reset to default language
19617      const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19618      const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19619      
19620      // Update button states to show default language
19621      document.querySelectorAll('.language-button-live').forEach(btn => {
19622        if (btn.getAttribute('data-lang-code') === defaultLang) {
19623          btn.style.background = 'var(--menu-active-color, #4682B4)';
19624        } else {
19625          btn.style.background = 'transparent';
19626        }
19627      });
19628      
19629      // Show message to user
19630      console.warn('Language switching is disabled in edit mode');
19631      return;
19632    }
19633    
19634    // Update all language buttons
19635    document.querySelectorAll('.language-button-live').forEach(btn => {
19636      if (btn.getAttribute('data-lang-code') === code) {
19637        btn.style.background = 'var(--menu-active-color, #4682B4)';
19638      } else {
19639        btn.style.background = 'transparent';
19640      }
19641    });
19642    
19643    // Save preference
19644    localStorage.setItem('hydra_language', code);
19645    currentLanguage = code;
19646    
19647    // Get default language from languages element
19648    const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages');
19649    const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en';
19650    
19651    if (code === defaultLang) {
19652      // Restore original language
19653      restoreOriginalText();
19654    } else {
19655      // Translate page
19656      await translatePage(code, defaultLang);
19657    }
19658  });
19659  
19660  async function translatePage(targetLang, sourceLang) {
19661    console.warn(`🔄 Translating page from ${sourceLang} to ${targetLang}`);
19662    
19663    // Elements to translate
19664    const selectors = [
19665      '.sidebar li a:not(.edit-mode-active .sidebar li a)', // Menu items (not in edit mode)
19666      '.page-text:not(.edit-mode-active .page-text)', // Page content (not in edit mode)
19667      'h1:not(.edit-mode-active h1)', 
19668      'h2:not(.edit-mode-active h2)', 
19669      'h3:not(.edit-mode-active h3)',
19670      'p:not(.edit-mode-active p)'
19671    ];
19672    
19673    const elements = document.querySelectorAll(selectors.join(', '));
19674    
19675    for (const element of elements) {
19676      // Skip if already translated to this language
19677      if (element.dataset.translatedTo === targetLang) continue;
19678      
19679      // Skip if element is empty
19680      const text = element.textContent.trim();
19681      if (!text || text.length === 0) continue;
19682      
19683      // Store original text
19684      if (!originalTexts.has(element)) {
19685        originalTexts.set(element, text);
19686      }
19687      
19688      // Check translation cache
19689      const cacheKey = `translate_${text}_${targetLang}`;
19690      let translated = getFromCache(cacheKey);
19691      
19692      if (translated) {
19693        element.textContent = translated;
19694        element.dataset.translatedTo = targetLang;
19695      } else {
19696        // Translate
19697        try {
19698          translated = await translate(text, sourceLang, targetLang);
19699          if (translated && translated !== text) {
19700            element.textContent = translated;
19701            element.dataset.translatedTo = targetLang;
19702            saveToCache(cacheKey, translated);
19703          }
19704        } catch (error) {
19705          console.error('Translation error:', error);
19706        }
19707      }
19708    }
19709  }
19710  
19711  function restoreOriginalText() {
19712    originalTexts.forEach((originalText, element) => {
19713      if (element && element.isConnected) {
19714        element.textContent = originalText;
19715        delete element.dataset.translatedTo;
19716      }
19717    });
19718  }
19719  
19720  async function translate(text, sourceLang, targetLang) {
19721    if (!text || text.trim().length === 0) return text;
19722    
19723    // Try Browser Translation API (Free, Chrome 130+)
19724    if ('translation' in self && 'createTranslator' in self.translation) {
19725      try {
19726        console.warn('🔄 Using Browser Translation API');
19727        const translator = await self.translation.createTranslator({
19728          sourceLanguage: sourceLang,
19729          targetLanguage: targetLang
19730        });
19731        const result = await translator.translate(text);
19732        console.warn('✅ Translation successful via Browser API');
19733        return result;
19734      } catch (error) {
19735        console.warn('❌ Browser translation failed:', error);
19736      }
19737    } else {
19738      console.warn('⚠️ Browser Translation API not available');
19739      console.warn('   To enable in Chrome: chrome://flags/#translation-api');
19740      console.warn('   OR add Google Translate API key to use paid translation');
19741    }
19742    
19743    // TODO: Add Google Translate API fallback here
19744    // For now, return original text
19745    console.warn('⚠️ No translation provider configured - returning original text');
19746    return text;
19747  }
19748  
19749  // Cache management
19750  function getFromCache(key) {
19751    try {
19752      const item = localStorage.getItem(`hydra_${key}`);
19753      if (!item) return null;
19754      
19755      const { value, expiry } = JSON.parse(item);
19756      if (expiry && Date.now() > expiry) {
19757        localStorage.removeItem(`hydra_${key}`);
19758        return null;
19759      }
19760      return value;
19761    } catch {
19762      return null;
19763    }
19764  }
19765  
19766  function saveToCache(key, value) {
19767    try {
19768      const item = {
19769        value,
19770        expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
19771      };
19772      localStorage.setItem(`hydra_${key}`, JSON.stringify(item));
19773    } catch (error) {
19774      console.warn('Cache save failed:', error);
19775    }
19776  }
19777  
19778  } // End of initializeTranslationSystem
19779})();
19780</script>
19780
19781
19782
19783
19784<script>
19785window.HYDRA_HELP_ENABLED = true;
19786function loadHelpSystemScript() {
19787  if (window.HelpSystem && window.HelpSystem.openHub) return Promise.resolve();
19788  return new Promise(function(resolve, reject) {
19789    var existing = document.querySelector('script[data-hydra-help-system]');
19790    if (existing) {
19791      existing.addEventListener('load', function() { resolve(); });
19792      existing.addEventListener('error', reject);
19793      return;
19794    }
19795    var s = document.createElement('script');
19796    s.src = '/api/help-system.js?v=686c85345749';
19797    s.async = true;
19798    s.setAttribute('data-hydra-help-system', '1');
19799    s.onload = function() { resolve(); };
19800    s.onerror = reject;
19801    document.head.appendChild(s);
19802  });
19803}
19804function openHydraHelp(opts) {
19805  opts = opts || {};
19806  if (!window.HYDRA_HELP_ENABLED) return;
19807  var token = window.didToken || localStorage.getItem('hydra_auth_token');
19808  var isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true';
19809  if (!token || !isAdmin) return;
19810  var p = loadHelpSystemScript();
19811  Promise.resolve(p).then(function() {
19812    if (window.HelpSystem && window.HelpSystem.init) return window.HelpSystem.init();
19813  }).then(function() {
19814    if (window.HelpSystem && window.HelpSystem.openHub) {
19815      window.HelpSystem.openHub(opts.initialTab ? { initialTab: opts.initialTab } : undefined);
19816    }
19817  }).catch(function(err) {
19818    console.error('HelpSystem init failed:', err);
19819  });
19820}
19821window.openHydraHelp = openHydraHelp;
19822</script>
19822
19823
19824<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>
19824
19825</body>
19826</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.