Compare commits
	
		
			54 Commits
		
	
	
		
			8a04fe5aa6
			...
			v2.1.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 40a01c4701 | |||
| 
						 | 
					28e3b087b0 | ||
| 116a9574ce | |||
| 
						 | 
					6c7216c0e1 | ||
| 7c98a6ceaf | |||
| 
						 | 
					69d303c7a1 | ||
| 
						 | 
					1d1d7d8304 | ||
| 
						 | 
					205dcae2bc | ||
| 
						 | 
					b7fdcacf77 | ||
| 
						 | 
					9cdf6bbd32 | ||
| 
						 | 
					f92060603d | ||
| 
						 | 
					54e9281793 | ||
| 
						 | 
					5fb6ef18d1 | ||
| 
						 | 
					1119647884 | ||
| 
						 | 
					2cc0a213c9 | ||
| 
						 | 
					24113a4aa8 | ||
| 
						 | 
					be8ce4f62c | ||
| 
						 | 
					edca473e29 | ||
| 
						 | 
					eac90820d2 | ||
| 
						 | 
					191fa82711 | ||
| 
						 | 
					b17652e471 | ||
| 
						 | 
					65fd62e342 | ||
| 
						 | 
					6b03ee30fa | ||
| 
						 | 
					fd45ebbd53 | ||
| 
						 | 
					795f5fbd13 | ||
| f8bebb9c95 | |||
| 3198755576 | |||
| f98f2d598f | |||
| e8718e71ab | |||
| 5b65e5efe3 | |||
| 021e0c7974 | |||
| 
						 | 
					debbf07280 | ||
| df96782500 | |||
| 
						 | 
					febf4d2be5 | ||
| 
						 | 
					617545e1bb | ||
| 
						 | 
					922ce99679 | ||
| 
						 | 
					cd0428990a | ||
| 
						 | 
					d3af86be8c | ||
| 
						 | 
					c825798b13 | ||
| b5375343a8 | |||
| 
						 | 
					757e676d2d | ||
| 
						 | 
					0079c166e8 | ||
| 
						 | 
					ee6d4a1fa2 | ||
| 
						 | 
					c6c3162b83 | ||
| 
						 | 
					04c1214cd1 | ||
| 
						 | 
					b03779b487 | ||
| 
						 | 
					b5f8ceeb31 | ||
| 
						 | 
					1591886505 | ||
| 
						 | 
					a6b63c2d2b | ||
| 7a95ef0255 | |||
| 906699f023 | |||
| 
						 | 
					643a729f94 | ||
| a02da47e73 | |||
| f7f2356510 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,5 @@
 | 
				
			|||||||
.*
 | 
					.*
 | 
				
			||||||
 | 
					!.env
 | 
				
			||||||
!.sh
 | 
					!.sh
 | 
				
			||||||
!.gitignore
 | 
					!.gitignore
 | 
				
			||||||
output/
 | 
					output/
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,10 +1,18 @@
 | 
				
			|||||||
FROM python:3.11-slim
 | 
					FROM python:3.13-alpine AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY requirements.txt .
 | 
					RUN apk add --no-cache gcc musl-dev jpeg-dev zlib-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN pip install --no-cache-dir -r requirements.txt
 | 
					COPY requirements.txt .
 | 
				
			||||||
 | 
					RUN pip wheel --no-cache-dir --wheel-dir=/wheels -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FROM python:3.13-alpine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COPY --from=builder /wheels /wheels
 | 
				
			||||||
 | 
					RUN pip install --no-cache-dir --find-links=/wheels /wheels/* && rm -rf /wheels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY build.py gallery.py VERSION /app/
 | 
					COPY build.py gallery.py VERSION /app/
 | 
				
			||||||
COPY ./src/ ./src/
 | 
					COPY ./src/ ./src/
 | 
				
			||||||
@@ -12,7 +20,4 @@ COPY ./config /app/default
 | 
				
			|||||||
COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh
 | 
					COPY ./docker/.sh/entrypoint.sh /app/entrypoint.sh
 | 
				
			||||||
RUN chmod +x /app/entrypoint.sh
 | 
					RUN chmod +x /app/entrypoint.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN printf '#!/bin/sh\n/app/entrypoint.sh build\n' > /usr/local/bin/build && chmod +x /usr/local/bin/build && \
 | 
					 | 
				
			||||||
    printf '#!/bin/sh\n/app/entrypoint.sh gallery\n' > /usr/local/bin/gallery && chmod +x /usr/local/bin/gallery
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ENTRYPOINT ["/app/entrypoint.sh"]
 | 
					ENTRYPOINT ["/app/entrypoint.sh"]
 | 
				
			||||||
							
								
								
									
										22
									
								
								README.MD
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								README.MD
									
									
									
									
									
								
							@@ -18,7 +18,7 @@ The project includes two thoughtfully designed themes—one modern, one minimali
 | 
				
			|||||||
- **Typewriter** — [View Demo](https://typewriter.djeex.fr)
 | 
					- **Typewriter** — [View Demo](https://typewriter.djeex.fr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [!NOTE]  
 | 
					> [!NOTE]  
 | 
				
			||||||
> This GitHub repository is a mirror of the primary source at [git.djeex.fr/Djeex/lumeex](https://git.djeex.fr/Djeex/lumeex). The main repository includes the full history, releases, and bug-checking assisted by an LLM.
 | 
					> _This GitHub repository is a mirror of the primary source at [git.djeex.fr/Djeex/lumeex](https://git.djeex.fr/Djeex/lumeex). The main repository includes the full history and releases_.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 📌 Table of Contents
 | 
					## 📌 Table of Contents
 | 
				
			||||||
@@ -41,20 +41,26 @@ The project includes two thoughtfully designed themes—one modern, one minimali
 | 
				
			|||||||
  - Typewriter — [Demo](https://typewriter.djeex.fr)  
 | 
					  - Typewriter — [Demo](https://typewriter.djeex.fr)  
 | 
				
			||||||
- Supports Google Fonts and locally hosted fonts
 | 
					- Supports Google Fonts and locally hosted fonts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### No-Code Builder (YAML Based)
 | 
					### No-Code Builder (WebUI Manager)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Configure site info, SEO, colors, fonts, and more through simple YAML files  
 | 
					 
 | 
				
			||||||
- Reference and tag photos without any coding required  
 | 
					<div align="center">
 | 
				
			||||||
- *(Optional)* Automatically update photo references via script
 | 
					  <img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex-webui.png" alt="Lumeex Screenshot" />
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Simple Build Process
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Compiles static site from YAML configuration files (themes, templates, fonts, colors)  
 | 
					- Configure site info, SEO, colors, fonts, and more through a simple convenient WebUI
 | 
				
			||||||
 | 
					- Add and tag your photo photos without any coding required
 | 
				
			||||||
- Converts favicon automatically to all required formats  
 | 
					- Converts favicon automatically to all required formats  
 | 
				
			||||||
- Resizes social sharing thumbnails  
 | 
					- Resizes social sharing thumbnails  
 | 
				
			||||||
- *(Optional)* Automatically resizes photos to a maximum width of 1140px  
 | 
					- *(Optional)* Automatically resizes photos to a maximum width of 1140px  
 | 
				
			||||||
- *(Optional)* Converts images to WebP format for optimized performance
 | 
					- *(Optional)* Converts images to WebP format for optimized performance
 | 
				
			||||||
- Outputs a complete static website ready to deploy on any web server
 | 
					- Build your static site in one click and get a zip archive or an output folder, ready to deploy to your preferred webserver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Don't want a WebUI ?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- CLI process is documented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 🐳 Docker or 🐍 Python Installation
 | 
					## 🐳 Docker or 🐍 Python Installation
 | 
				
			||||||
For comprehensive documentation on installation, configuration options, customization, and demos, please visit:
 | 
					For comprehensive documentation on installation, configuration options, customization, and demos, please visit:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
# Please change this by your settings.
 | 
					 | 
				
			||||||
info:
 | 
					info:
 | 
				
			||||||
  title:
 | 
					  title:
 | 
				
			||||||
  subtitle:
 | 
					  subtitle:
 | 
				
			||||||
@@ -9,7 +8,7 @@ info:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
social:
 | 
					social:
 | 
				
			||||||
  instagram_url:
 | 
					  instagram_url:
 | 
				
			||||||
  thumbnail:
 | 
					  thumbnail: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
menu:
 | 
					menu:
 | 
				
			||||||
  items:
 | 
					  items:
 | 
				
			||||||
@@ -18,19 +17,17 @@ menu:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
footer:
 | 
					footer:
 | 
				
			||||||
  copyright: Copyright © 2025
 | 
					  copyright: Copyright © 2025
 | 
				
			||||||
  legal_link: '/legals/'
 | 
					  legal_link: /legals/
 | 
				
			||||||
  legal_label: Legal notice
 | 
					  legal_label: Legal notice
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Build parameters
 | 
					 | 
				
			||||||
build:
 | 
					build:
 | 
				
			||||||
  theme: modern # choose a theme in config/theme folder
 | 
					  theme: modern
 | 
				
			||||||
  convert_images: true # true to enable image conversion
 | 
					  convert_images: true
 | 
				
			||||||
  resize_images: true # true to enable image resizing
 | 
					  resize_images: true
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
# Change this by your legals
 | 
					 | 
				
			||||||
legals:
 | 
					legals:
 | 
				
			||||||
  hoster_name:
 | 
					  hoster_name:
 | 
				
			||||||
  hoster_adress:
 | 
					  hoster_address:
 | 
				
			||||||
  hoster_contact:
 | 
					  hoster_contact:
 | 
				
			||||||
  intellectual_property:
 | 
					  intellectual_property:
 | 
				
			||||||
    - paragraph: ""
 | 
					  - paragraph: ''
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,16 +35,13 @@ img, tag {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#footer {
 | 
					#footer {
 | 
				
			||||||
    box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
 | 
					    box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
 | 
				
			||||||
    margin: auto;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (max-width: 768px) {
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    #footer {
 | 
					    #footer {
 | 
				
			||||||
        box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
 | 
					        box-shadow: 0px 20px 100px -44px rgba(0, 0, 0, 0.5);
 | 
				
			||||||
        border-radius: 15px 15px 0 0;
 | 
					 | 
				
			||||||
        max-width: 1140px;
 | 
					        max-width: 1140px;
 | 
				
			||||||
        margin: 0;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .tag {
 | 
					    .tag {
 | 
				
			||||||
@@ -53,7 +50,7 @@ img, tag {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    .hero-background {
 | 
					    .hero-background {
 | 
				
			||||||
        max-width: 90%;
 | 
					        max-width: 90%;
 | 
				
			||||||
        margin: auto;
 | 
					        margin: 0 auto;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .back-button {
 | 
					    .back-button {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +1,31 @@
 | 
				
			|||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
# Modern theme for Lumeex           #
 | 
					 | 
				
			||||||
# https://git.djeex.fr/Djeex/lumeex #
 | 
					 | 
				
			||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
colors:
 | 
					colors:
 | 
				
			||||||
  primary: '#0065a1'
 | 
					  primary: '#0065A1'
 | 
				
			||||||
  primary_dark: '#005384'
 | 
					  primary_dark: '#005384'
 | 
				
			||||||
  secondary: '#00b0f0'
 | 
					  secondary: '#00B0F0'
 | 
				
			||||||
  accent: '#ffc700'
 | 
					  accent: '#FFC700'
 | 
				
			||||||
  text_dark: '#616161'
 | 
					  text_dark: '#616161'
 | 
				
			||||||
  background: '#ffffff'
 | 
					  background: '#FFFFFF'
 | 
				
			||||||
  browser_color: '#ffffff'
 | 
					  browser_color: '#FFFFFF'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
favicon:
 | 
					favicon:
 | 
				
			||||||
  path: favicon.png
 | 
					  path: favicon.png
 | 
				
			||||||
google_fonts:
 | 
					
 | 
				
			||||||
  - family: Lato
 | 
					 | 
				
			||||||
    weights:
 | 
					 | 
				
			||||||
    - '200'
 | 
					 | 
				
			||||||
    - '400'
 | 
					 | 
				
			||||||
    - '700'
 | 
					 | 
				
			||||||
  - family: Montserrat
 | 
					 | 
				
			||||||
    weights:
 | 
					 | 
				
			||||||
    - '200'
 | 
					 | 
				
			||||||
    - '400'
 | 
					 | 
				
			||||||
    - '700'
 | 
					 | 
				
			||||||
fonts:
 | 
					fonts:
 | 
				
			||||||
  primary:
 | 
					  primary:
 | 
				
			||||||
    name: Lato
 | 
					 | 
				
			||||||
    fallback: sans-serif
 | 
					    fallback: sans-serif
 | 
				
			||||||
 | 
					    name: Lato
 | 
				
			||||||
  secondary:
 | 
					  secondary:
 | 
				
			||||||
    name: Montserrat
 | 
					 | 
				
			||||||
    fallback: serif
 | 
					    fallback: serif
 | 
				
			||||||
 | 
					    name: Montserrat
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					google_fonts:
 | 
				
			||||||
 | 
					- family: 'Lato'
 | 
				
			||||||
 | 
					  weights:
 | 
				
			||||||
 | 
					  - '200'
 | 
				
			||||||
 | 
					  - '400'
 | 
				
			||||||
 | 
					  - '700'
 | 
				
			||||||
 | 
					- family: Montserrat
 | 
				
			||||||
 | 
					  weights:
 | 
				
			||||||
 | 
					  - '200'
 | 
				
			||||||
 | 
					  - '400'
 | 
				
			||||||
 | 
					  - '700'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,17 @@
 | 
				
			|||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
# Typewriter theme for Lumeex       #
 | 
					 | 
				
			||||||
# https://git.djeex.fr/Djeex/lumeex #
 | 
					 | 
				
			||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
colors:
 | 
					colors:
 | 
				
			||||||
  primary: '#0065a1'
 | 
					  accent: '#FFC700'
 | 
				
			||||||
 | 
					  background: '#FFFFFF'
 | 
				
			||||||
 | 
					  browser_color: '#FFFFFF'
 | 
				
			||||||
 | 
					  primary: '#0065A1'
 | 
				
			||||||
  primary_dark: '#005384'
 | 
					  primary_dark: '#005384'
 | 
				
			||||||
  secondary: '#00b0f0'
 | 
					  secondary: '#00B0F0'
 | 
				
			||||||
  accent: '#ffc700'
 | 
					 | 
				
			||||||
  text_dark: '#616161'
 | 
					  text_dark: '#616161'
 | 
				
			||||||
  background: '#ffffff'
 | 
					 | 
				
			||||||
  browser_color: '#ffffff'
 | 
					 | 
				
			||||||
favicon:
 | 
					favicon:
 | 
				
			||||||
  path: favicon.png
 | 
					  path: favicon.png
 | 
				
			||||||
fonts:
 | 
					fonts:
 | 
				
			||||||
  primary:
 | 
					  primary:
 | 
				
			||||||
    name: Trixie
 | 
					    name: trixie
 | 
				
			||||||
    fallback: sans-serif
 | 
					    fallback: sans-serif
 | 
				
			||||||
  secondary:
 | 
					  secondary:
 | 
				
			||||||
    name: Trixie
 | 
					    name: trixie
 | 
				
			||||||
    fallback: serif
 | 
					    fallback: serif
 | 
				
			||||||
@@ -1,32 +1,31 @@
 | 
				
			|||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
# Modern theme for Lumeex           #
 | 
					 | 
				
			||||||
# https://git.djeex.fr/Djeex/lumeex #
 | 
					 | 
				
			||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
colors:
 | 
					colors:
 | 
				
			||||||
  primary: '#0065a1'
 | 
					  primary: '#0065A1'
 | 
				
			||||||
  primary_dark: '#005384'
 | 
					  primary_dark: '#005384'
 | 
				
			||||||
  secondary: '#00b0f0'
 | 
					  secondary: '#00B0F0'
 | 
				
			||||||
  accent: '#ffc700'
 | 
					  accent: '#FFC700'
 | 
				
			||||||
  text_dark: '#616161'
 | 
					  text_dark: '#616161'
 | 
				
			||||||
  background: '#fff'
 | 
					  background: '#FFFFFF'
 | 
				
			||||||
  browser_color: '#fff'
 | 
					  browser_color: '#FFFFFF'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
favicon:
 | 
					favicon:
 | 
				
			||||||
  path: favicon.png
 | 
					  path: favicon.png
 | 
				
			||||||
google_fonts:
 | 
					
 | 
				
			||||||
  - family: Lato
 | 
					 | 
				
			||||||
    weights:
 | 
					 | 
				
			||||||
    - '200'
 | 
					 | 
				
			||||||
    - '400'
 | 
					 | 
				
			||||||
    - '700'
 | 
					 | 
				
			||||||
  - family: Montserrat
 | 
					 | 
				
			||||||
    weights:
 | 
					 | 
				
			||||||
    - '200'
 | 
					 | 
				
			||||||
    - '400'
 | 
					 | 
				
			||||||
    - '700'
 | 
					 | 
				
			||||||
fonts:
 | 
					fonts:
 | 
				
			||||||
  primary:
 | 
					  primary:
 | 
				
			||||||
    name: Lato
 | 
					 | 
				
			||||||
    fallback: sans-serif
 | 
					    fallback: sans-serif
 | 
				
			||||||
 | 
					    name: Lato
 | 
				
			||||||
  secondary:
 | 
					  secondary:
 | 
				
			||||||
    name: Montserrat
 | 
					 | 
				
			||||||
    fallback: serif
 | 
					    fallback: serif
 | 
				
			||||||
 | 
					    name: Montserrat
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					google_fonts:
 | 
				
			||||||
 | 
					- family: ''
 | 
				
			||||||
 | 
					  weights:
 | 
				
			||||||
 | 
					  - '200'
 | 
				
			||||||
 | 
					  - '400'
 | 
				
			||||||
 | 
					  - '700'
 | 
				
			||||||
 | 
					- family: Montserrat
 | 
				
			||||||
 | 
					  weights:
 | 
				
			||||||
 | 
					  - '200'
 | 
				
			||||||
 | 
					  - '400'
 | 
				
			||||||
 | 
					  - '700'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,19 @@
 | 
				
			|||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
# Typewriter theme for Lumeex       #
 | 
					 | 
				
			||||||
# https://git.djeex.fr/Djeex/lumeex #
 | 
					 | 
				
			||||||
#-----------------------------------#
 | 
					 | 
				
			||||||
colors:
 | 
					colors:
 | 
				
			||||||
  primary: '#0065a1'
 | 
					  accent: '#FFC700'
 | 
				
			||||||
 | 
					  background: '#FFFFFF'
 | 
				
			||||||
 | 
					  browser_color: '#FFFFFF'
 | 
				
			||||||
 | 
					  primary: '#0065A1'
 | 
				
			||||||
  primary_dark: '#005384'
 | 
					  primary_dark: '#005384'
 | 
				
			||||||
  secondary: '#00b0f0'
 | 
					  secondary: '#00B0F0'
 | 
				
			||||||
  accent: '#ffc700'
 | 
					 | 
				
			||||||
  text_dark: '#616161'
 | 
					  text_dark: '#616161'
 | 
				
			||||||
  background: '#fff'
 | 
					
 | 
				
			||||||
  browser_color: '#fff'
 | 
					 | 
				
			||||||
favicon:
 | 
					favicon:
 | 
				
			||||||
  path: favicon.png
 | 
					  path: favicon.png
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
fonts:
 | 
					fonts:
 | 
				
			||||||
  primary:
 | 
					  primary:
 | 
				
			||||||
    name: Trixie
 | 
					    name: trixie.woff
 | 
				
			||||||
    fallback: sans-serif
 | 
					    fallback: sans-serif
 | 
				
			||||||
  secondary:
 | 
					  secondary:
 | 
				
			||||||
    name: Trixie
 | 
					    name: trixie.woff
 | 
				
			||||||
    fallback: serif
 | 
					    fallback: serif
 | 
				
			||||||
							
								
								
									
										2
									
								
								docker/.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								docker/.env
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					PREVIEW_PORT=3000
 | 
				
			||||||
 | 
					WEBUI_PORT=5000
 | 
				
			||||||
@@ -1,33 +1,36 @@
 | 
				
			|||||||
#!/bin/bash
 | 
					#!/bin/sh
 | 
				
			||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CYAN="\033[1;36m"
 | 
					CYAN="\033[1;36m"
 | 
				
			||||||
NC="\033[0m"
 | 
					NC="\033[0m"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
copy_default_config() {
 | 
					copy_default_config() {
 | 
				
			||||||
  echo "Checking configuration directory..."
 | 
					  echo "[~] Checking configuration directory..."
 | 
				
			||||||
  if [ ! -d "/app/config" ]; then
 | 
					 | 
				
			||||||
  mkdir -p /app/config
 | 
					  mkdir -p /app/config
 | 
				
			||||||
  fi
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  echo "Checking if default config files need to be copied..."
 | 
					  echo "[~] Checking if default config files/folders need to be copied..."
 | 
				
			||||||
  files_copied=false
 | 
					  files_copied=false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for file in /app/default/*; do
 | 
					  for src in $(find /app/default -mindepth 1); do
 | 
				
			||||||
    filename=$(basename "$file")
 | 
					    relpath="${src#/app/default/}"
 | 
				
			||||||
    target="/app/config/$filename"
 | 
					    target="/app/config/$relpath"
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if [ ! -e "$target" ]; then
 | 
					    if [ ! -e "$target" ]; then
 | 
				
			||||||
      echo "Copying default config file: $filename"
 | 
					      echo "[→] Copying: $relpath"
 | 
				
			||||||
      cp -r "$file" "$target"
 | 
					      if [ -d "$src" ]; then
 | 
				
			||||||
 | 
					        mkdir -p "$target"
 | 
				
			||||||
 | 
					        cp -r "$src/" "$target/"
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        mkdir -p "$(dirname "$target")"
 | 
				
			||||||
 | 
					        cp "$src" "$target"
 | 
				
			||||||
 | 
					      fi
 | 
				
			||||||
      files_copied=true
 | 
					      files_copied=true
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
  done
 | 
					  done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if [ "$files_copied" = true ]; then
 | 
					  if [ "$files_copied" = true ]; then
 | 
				
			||||||
    echo "Default configuration files copied successfully."
 | 
					    echo "[✓] Default configuration files/folders copied successfully."
 | 
				
			||||||
  else
 | 
					  else
 | 
				
			||||||
    echo "No default files needed to be copied."
 | 
					    echo "[✓] No default files/folders needed to be copied."
 | 
				
			||||||
  fi
 | 
					  fi
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,11 +46,13 @@ start_server() {
 | 
				
			|||||||
  cat /tmp/build_logs_fifo >&2 &
 | 
					  cat /tmp/build_logs_fifo >&2 &
 | 
				
			||||||
  cat /tmp/build_logs_fifo2 >&2 &
 | 
					  cat /tmp/build_logs_fifo2 >&2 &
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  echo "Starting preview HTTP server on port 3000..."
 | 
					  PREVIEW_PORT="${PREVIEW_PORT:-3000}"
 | 
				
			||||||
 | 
					  echo "[~]Starting preview HTTP server on port 3000..."
 | 
				
			||||||
 | 
					  echo "[i] Preview host port is set to: ${PREVIEW_PORT}"
 | 
				
			||||||
  python3 -u -m http.server 3000 -d /app/output &
 | 
					  python3 -u -m http.server 3000 -d /app/output &
 | 
				
			||||||
  SERVER_PID=$!
 | 
					  SERVER_PID=$!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  echo "Starting Lumeex Flask webui..."
 | 
					  echo "[~] Starting Lumeex Flask webui..."
 | 
				
			||||||
  python3 -u -m src.py.webui.webui &
 | 
					  python3 -u -m src.py.webui.webui &
 | 
				
			||||||
  WEBUI_PID=$!
 | 
					  WEBUI_PID=$!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,15 +77,15 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
case "$1" in
 | 
					case "$1" in
 | 
				
			||||||
  build)
 | 
					  build)
 | 
				
			||||||
    echo "Running build.py..."
 | 
					    echo "[~] Running build.py..."
 | 
				
			||||||
    python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo
 | 
					    python3 -u /app/build.py 2>&1 | tee /tmp/build_logs_fifo
 | 
				
			||||||
    ;;
 | 
					    ;;
 | 
				
			||||||
  gallery)
 | 
					  gallery)
 | 
				
			||||||
    echo "Running gallery.py..."
 | 
					    echo "[~] Running gallery.py..."
 | 
				
			||||||
    python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2
 | 
					    python3 -u /app/gallery.py 2>&1 | tee /tmp/build_logs_fifo2
 | 
				
			||||||
    ;;
 | 
					    ;;
 | 
				
			||||||
  *)
 | 
					  *)
 | 
				
			||||||
    echo "Unknown command: $1"
 | 
					    echo "[!] Unknown command: $1"
 | 
				
			||||||
    exec "$@"
 | 
					    exec "$@"
 | 
				
			||||||
    ;;
 | 
					    ;;
 | 
				
			||||||
esac
 | 
					esac
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,15 @@ services:
 | 
				
			|||||||
  lumeex:
 | 
					  lumeex:
 | 
				
			||||||
    container_name: lmx
 | 
					    container_name: lmx
 | 
				
			||||||
    build: ..
 | 
					    build: ..
 | 
				
			||||||
 | 
					    env_file:
 | 
				
			||||||
 | 
					      - .env
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - PREVIEW_PORT=${PREVIEW_PORT:-3000} # port for preview server - set it in .env file
 | 
				
			||||||
 | 
					      - WEBUI_PORT=${WEBUI_PORT:-5000} # port for webui server - set it in .env file
 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - ../config:/app/config  # mount config directory
 | 
					      - ../config:/app/config  # mount config directory
 | 
				
			||||||
      - ../output:/app/output  # mount output directory
 | 
					      - ../output:/app/output  # mount output directory
 | 
				
			||||||
    ports:
 | 
					    ports:
 | 
				
			||||||
      - "3000:3000"
 | 
					      - "${PREVIEW_PORT:-3000}:3000"
 | 
				
			||||||
      - "5000:5000"
 | 
					      - "${WEBUI_PORT:-5000}:5000"
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								illustration/lumeex-webui.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								illustration/lumeex-webui.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 620 KiB  | 
@@ -9,7 +9,6 @@ window.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
    entries.forEach(entry => {
 | 
					    entries.forEach(entry => {
 | 
				
			||||||
      if (entry.isIntersecting) {
 | 
					      if (entry.isIntersecting) {
 | 
				
			||||||
        const img = entry.target;
 | 
					        const img = entry.target;
 | 
				
			||||||
        console.log("Lazy-loading image:", img.dataset.src);
 | 
					 | 
				
			||||||
        img.src = img.dataset.src;
 | 
					        img.src = img.dataset.src;
 | 
				
			||||||
        img.onload = () => {
 | 
					        img.onload = () => {
 | 
				
			||||||
          img.classList.add("loaded");
 | 
					          img.classList.add("loaded");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Fade in effect for elements with class 'appear'
 | 
					// Fade in effect for elements with class 'appear'
 | 
				
			||||||
const setupIntersectionObserver = () => {
 | 
					const setupIntersectionObserver = () => {
 | 
				
			||||||
 | 
					  document.querySelectorAll('.appear').forEach(parent => {
 | 
				
			||||||
 | 
					    const children = parent.querySelectorAll('.appear');
 | 
				
			||||||
 | 
					    children.forEach((child, i) => {
 | 
				
			||||||
 | 
					      child.style.transitionDelay = `${i * 0.2}s`;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const items = document.querySelectorAll('.appear');
 | 
					  const items = document.querySelectorAll('.appear');
 | 
				
			||||||
  const io = new IntersectionObserver((entries) => {
 | 
					  const io = new IntersectionObserver((entries) => {
 | 
				
			||||||
    entries.forEach((entry) => {
 | 
					    entries.forEach((entry) => {
 | 
				
			||||||
@@ -32,6 +39,7 @@ const randomizeHeroBackground = () => {
 | 
				
			|||||||
      if (images.length === 0) return;
 | 
					      if (images.length === 0) return;
 | 
				
			||||||
      let currentIndex = Math.floor(Math.random() * images.length);
 | 
					      let currentIndex = Math.floor(Math.random() * images.length);
 | 
				
			||||||
      heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`;
 | 
					      heroBg.style.backgroundImage = `url(/img/${images[currentIndex]})`;
 | 
				
			||||||
 | 
					      if (images.length < 2) return; // <-- Prevent interval if only one image
 | 
				
			||||||
      setInterval(() => {
 | 
					      setInterval(() => {
 | 
				
			||||||
        let nextIndex;
 | 
					        let nextIndex;
 | 
				
			||||||
        do {
 | 
					        do {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -78,6 +78,8 @@ html,body {
 | 
				
			|||||||
	font-weight: 400;
 | 
						font-weight: 400;
 | 
				
			||||||
	line-height:1.5;
 | 
						line-height:1.5;
 | 
				
			||||||
	color:var(--color-primary-dark);
 | 
						color:var(--color-primary-dark);
 | 
				
			||||||
 | 
						display: flex;
 | 
				
			||||||
 | 
						flex-direction: column;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html {
 | 
					html {
 | 
				
			||||||
@@ -179,8 +181,8 @@ h2 {
 | 
				
			|||||||
/* animation */
 | 
					/* animation */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.appear {
 | 
					.appear {
 | 
				
			||||||
  -webkit-transition: all 0.3s;
 | 
					  -webkit-transition: all 1s;
 | 
				
			||||||
  transition: all 0.3s;
 | 
					  transition: all 1s;
 | 
				
			||||||
  opacity: 0;
 | 
					  opacity: 0;
 | 
				
			||||||
  -webkit-transform: translateY(20px);
 | 
					  -webkit-transform: translateY(20px);
 | 
				
			||||||
          transform: translateY(20px);
 | 
					          transform: translateY(20px);
 | 
				
			||||||
@@ -192,36 +194,6 @@ h2 {
 | 
				
			|||||||
          transform: none;
 | 
					          transform: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.appear.inview:nth-child(1) {
 | 
					 | 
				
			||||||
  -webkit-transition-delay: 0s;
 | 
					 | 
				
			||||||
          transition-delay: 0s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.appear.inview:nth-child(2) {
 | 
					 | 
				
			||||||
  -webkit-transition-delay: 0.2s;
 | 
					 | 
				
			||||||
          transition-delay: 0.2s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.appear.inview:nth-child(3) {
 | 
					 | 
				
			||||||
  -webkit-transition-delay: 0.4s;
 | 
					 | 
				
			||||||
          transition-delay: 0.4s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.appear.inview:nth-child(4) {
 | 
					 | 
				
			||||||
  -webkit-transition-delay: 0.6s;
 | 
					 | 
				
			||||||
          transition-delay: 0.6s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.appear.inview:nth-child(5) {
 | 
					 | 
				
			||||||
  -webkit-transition-delay: 0.8s;
 | 
					 | 
				
			||||||
          transition-delay: 0.8s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.appear.inview:nth-child(6) {
 | 
					 | 
				
			||||||
  -webkit-transition-delay: 1s;
 | 
					 | 
				
			||||||
          transition-delay: 1s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* img fade in */
 | 
					/* img fade in */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.fade-in-img {
 | 
					.fade-in-img {
 | 
				
			||||||
@@ -267,12 +239,23 @@ h2 {
 | 
				
			|||||||
/* Hero */
 | 
					/* Hero */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#hero {
 | 
					#hero {
 | 
				
			||||||
 | 
					  min-height: 100vh;
 | 
				
			||||||
 | 
					  flex: 1 0 auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#hero .section {
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#hero .content-wrapper, #hero .section {
 | 
					#hero .content-wrapper, #hero .section {
 | 
				
			||||||
	height:100%;
 | 
						height:100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.hero-background {
 | 
					.hero-background {
 | 
				
			||||||
	height: 66%;
 | 
						height: 66%;
 | 
				
			||||||
	width: 100%;
 | 
						width: 100%;
 | 
				
			||||||
@@ -333,7 +316,6 @@ h2 {
 | 
				
			|||||||
    font-size: 22px;
 | 
					    font-size: 22px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
.gallery {
 | 
					.gallery {
 | 
				
			||||||
		padding-top: 15px;
 | 
							padding-top: 15px;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -371,6 +353,11 @@ h2 {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/* Footer */
 | 
					/* Footer */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#footer {
 | 
				
			||||||
 | 
						margin: auto 0 0 0;
 | 
				
			||||||
 | 
						width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.navigation {
 | 
					.navigation {
 | 
				
			||||||
	text-align: center;
 | 
						text-align: center;
 | 
				
			||||||
	padding-top: 40px;
 | 
						padding-top: 40px;
 | 
				
			||||||
@@ -491,7 +478,7 @@ h2 {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.section img {
 | 
						.section img {
 | 
				
			||||||
		margin: 0px 0 60px 0;
 | 
							margin: 0px 0 40px 0;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.tag {
 | 
						.tag {
 | 
				
			||||||
@@ -500,8 +487,7 @@ h2 {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	#legals.content-wrapper {
 | 
						#legals.content-wrapper {
 | 
				
			||||||
		max-width: 90%;
 | 
							max-width: 90%;
 | 
				
			||||||
		margin: auto;
 | 
							margin: 50px auto;
 | 
				
			||||||
		margin-top: 50px;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.legals-content {
 | 
						.legals-content {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,8 @@ app = Flask(
 | 
				
			|||||||
    static_url_path=""
 | 
					    static_url_path=""
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WEBUI_PORT = int(os.getenv("WEBUI_PORT", 5000))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Config paths ---
 | 
					# --- Config paths ---
 | 
				
			||||||
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
 | 
					SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
 | 
				
			||||||
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
					PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
				
			||||||
@@ -72,9 +74,14 @@ def get_local_fonts(theme_name):
 | 
				
			|||||||
def index():
 | 
					def index():
 | 
				
			||||||
    return render_template("index.html")
 | 
					    return render_template("index.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PREVIEW_PORT = int(os.getenv("PREVIEW_PORT", 3000))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.context_processor
 | 
					@app.context_processor
 | 
				
			||||||
def inject_version():
 | 
					def inject_version():
 | 
				
			||||||
    return dict(lumeex_version=lumeex_version)
 | 
					    return dict(
 | 
				
			||||||
 | 
					        lumeex_version=lumeex_version,
 | 
				
			||||||
 | 
					        preview_port=PREVIEW_PORT
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Gallery & Hero API ---
 | 
					# --- Gallery & Hero API ---
 | 
				
			||||||
@app.route("/gallery-editor")
 | 
					@app.route("/gallery-editor")
 | 
				
			||||||
@@ -203,9 +210,21 @@ def get_site_info():
 | 
				
			|||||||
@app.route("/api/site-info", methods=["POST"])
 | 
					@app.route("/api/site-info", methods=["POST"])
 | 
				
			||||||
def update_site_info():
 | 
					def update_site_info():
 | 
				
			||||||
    """Update site info YAML."""
 | 
					    """Update site info YAML."""
 | 
				
			||||||
    data = request.json
 | 
					    new_data = request.json
 | 
				
			||||||
 | 
					    with open(SITE_YAML, "r") as f:
 | 
				
			||||||
 | 
					        old_data = yaml.safe_load(f) or {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def deep_merge(old, new):
 | 
				
			||||||
 | 
					        for k, v in new.items():
 | 
				
			||||||
 | 
					            if isinstance(v, dict) and isinstance(old.get(k), dict):
 | 
				
			||||||
 | 
					                old[k] = deep_merge(old[k], v)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                old[k] = v
 | 
				
			||||||
 | 
					        return old
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    merged = deep_merge(old_data, new_data)
 | 
				
			||||||
    with open(SITE_YAML, "w") as f:
 | 
					    with open(SITE_YAML, "w") as f:
 | 
				
			||||||
        yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
 | 
					        yaml.safe_dump(merged, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Theme management ---
 | 
					# --- Theme management ---
 | 
				
			||||||
@@ -396,7 +415,8 @@ def upload_font():
 | 
				
			|||||||
    fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
 | 
					    fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
 | 
				
			||||||
    fonts_dir.mkdir(parents=True, exist_ok=True)
 | 
					    fonts_dir.mkdir(parents=True, exist_ok=True)
 | 
				
			||||||
    file.save(fonts_dir / file.filename)
 | 
					    file.save(fonts_dir / file.filename)
 | 
				
			||||||
    return jsonify({"status": "ok", "filename": file.filename})
 | 
					    font_basename = Path(file.filename).stem
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok", "filename": font_basename})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/api/font/remove", methods=["POST"])
 | 
					@app.route("/api/font/remove", methods=["POST"])
 | 
				
			||||||
def remove_font():
 | 
					def remove_font():
 | 
				
			||||||
@@ -487,5 +507,6 @@ def download_output_zip():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# --- Run server ---
 | 
					# --- Run server ---
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    logging.info("Starting WebUI at http://0.0.0.0:5000")
 | 
					    logging.info("[~] Starting WebUI at http://0.0.0.0:5000")
 | 
				
			||||||
 | 
					    logging.info(f"[i] WebUI host port is set to {WEBUI_PORT}")
 | 
				
			||||||
    app.run(host="0.0.0.0", port=5000, debug=True)
 | 
					    app.run(host="0.0.0.0", port=5000, debug=True)
 | 
				
			||||||
@@ -12,7 +12,7 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="inner bottom-link appear">
 | 
					        <div class="inner bottom-link appear">
 | 
				
			||||||
          <p><span class="navigation-subtitle appear">{{ copyright }}</span><span class="nav-separator"> • </span><span class="navigation-bottom-link appear"><a href="{{ legal_link }}">{{ legal_label }}</a></span></p>
 | 
					          <p><span class="navigation-subtitle appear">{{ copyright }}</span><span class="nav-separator"> • </span><span class="navigation-bottom-link appear"><a href="{{ legal_link }}">{{ legal_label }}</a></span></p>
 | 
				
			||||||
          <p class="navigation-subtitle appear"> Built with <a href="https://git.djeex.fr/Djeex/lumeex">Lumeex</a></p>
 | 
					          <p class="navigation-subtitle appear"> Built with <a href="https://lumeex.djeex.fr">Lumeex</a></p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,16 @@
 | 
				
			|||||||
        <button id="remove-all-hero" class="up-btn">🗑 Delete all</button>
 | 
					        <button id="remove-all-hero" class="up-btn">🗑 Delete all</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
					      <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
				
			||||||
 | 
					      <span id="hero-count" class="photo-count"></span>
 | 
				
			||||||
      <div id="hero"></div>
 | 
					      <div id="hero"></div>
 | 
				
			||||||
 | 
					        <input type="file" id="upload-hero-bottom" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
				
			||||||
 | 
					        <div id="bottom-hero-upload" class="upload-actions-row bottom-action-row">
 | 
				
			||||||
 | 
					          <span id="hero-count-bottom" class="photo-count"></span>
 | 
				
			||||||
 | 
					          <label for="upload-hero-bottom" class="up-btn">
 | 
				
			||||||
 | 
					            📸 Upload photos
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					          <button id="remove-all-hero-bottom" class="up-btn bottom-remove-btn">🗑 Delete all</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- Gallery Upload Section -->
 | 
					    <!-- Gallery Upload Section -->
 | 
				
			||||||
@@ -31,8 +40,27 @@
 | 
				
			|||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
        <button id="remove-all-gallery" class="up-btn">🗑 Delete all</button>
 | 
					        <button id="remove-all-gallery" class="up-btn">🗑 Delete all</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					        <div class="filter-row">
 | 
				
			||||||
 | 
					          <label>
 | 
				
			||||||
 | 
					            <input type="radio" name="gallery-filter" id="show-all-radio" checked>
 | 
				
			||||||
 | 
					            All
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					          <label>
 | 
				
			||||||
 | 
					            <input type="radio" name="gallery-filter" id="show-untagged-radio">
 | 
				
			||||||
 | 
					            Untagged
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      <span id="gallery-count" class="photo-count"></span>
 | 
				
			||||||
      <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
					      <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
				
			||||||
      <div id="gallery"></div>
 | 
					      <div id="gallery"></div>
 | 
				
			||||||
 | 
					        <input type="file" id="upload-gallery-bottom" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
				
			||||||
 | 
					        <div id="bottom-gallery-upload" class="upload-actions-row bottom-action-row">
 | 
				
			||||||
 | 
					          <span id="gallery-count-bottom" class="photo-count"></span>
 | 
				
			||||||
 | 
					          <label for="upload-gallery-bottom" class="up-btn">
 | 
				
			||||||
 | 
					            📸 Upload photos
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					          <button id="remove-all-gallery-bottom" class="up-btn bottom-remove-btn">🗑 Delete all</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="section">
 | 
					  <div class="section">
 | 
				
			||||||
@@ -40,18 +68,14 @@
 | 
				
			|||||||
      <p> Follow the steps to generate your static gallery</p>
 | 
					      <p> Follow the steps to generate your static gallery</p>
 | 
				
			||||||
      <ul id="stepper">
 | 
					      <ul id="stepper">
 | 
				
			||||||
        <li><a class="step-active" href="/gallery-editor">Upload your photos</a></li>
 | 
					        <li><a class="step-active" href="/gallery-editor">Upload your photos</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					        <div></div>
 | 
				
			||||||
        <li><a href="/site-info">Configure site info</a></li>
 | 
					        <li><a href="/site-info">Configure site info</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					        <div></div>
 | 
				
			||||||
        <li><a href="/theme-editor">Customize your theme</a></li>
 | 
					        <li><a href="/theme-editor">Customize your theme</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					        <div></div>
 | 
				
			||||||
        <li><button id="stepper-build">Generate your static site!</button></li>
 | 
					        <li><button id="stepper-build">Generate your static site!</button></li>
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
 | 
					 | 
				
			||||||
  <script src="{{ url_for('static', filename='js/upload.js') }}" defer></script>
 | 
					 | 
				
			||||||
  <script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
 | 
					 | 
				
			||||||
    <!-- Delete confirmation modal -->
 | 
					    <!-- Delete confirmation modal -->
 | 
				
			||||||
    <div id="delete-modal" class="modal" style="display:none;">
 | 
					    <div id="delete-modal" class="modal" style="display:none;">
 | 
				
			||||||
      <div class="modal-content">
 | 
					      <div class="modal-content">
 | 
				
			||||||
@@ -65,9 +89,10 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block scripts %}
 | 
					{% block scripts %}
 | 
				
			||||||
  <script src="{{ url_for('static', filename='js/main.js') }}"></script>
 | 
					  <script src="{{ url_for('static', filename='js/gallery-editor.js') }}"></script>
 | 
				
			||||||
  <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
 | 
					  <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
@@ -11,13 +11,14 @@
 | 
				
			|||||||
    <div class="section">
 | 
					    <div class="section">
 | 
				
			||||||
      <h2>Steps</h2>
 | 
					      <h2>Steps</h2>
 | 
				
			||||||
      <p> Follow the steps to generate your static gallery</p>
 | 
					      <p> Follow the steps to generate your static gallery</p>
 | 
				
			||||||
 | 
					      <div class="stepper">
 | 
				
			||||||
        <ul id="stepper">
 | 
					        <ul id="stepper">
 | 
				
			||||||
          <li><a href="/gallery-editor">Upload your photos</a></li>
 | 
					          <li><a href="/gallery-editor">Upload your photos</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					          <div></div>
 | 
				
			||||||
          <li><a href="/site-info">Configure site info</a></li>
 | 
					          <li><a href="/site-info">Configure site info</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					          <div></div>
 | 
				
			||||||
          <li><a href="/theme-editor">Customize your theme</a></li>
 | 
					          <li><a href="/theme-editor">Customize your theme</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					          <div></div>
 | 
				
			||||||
          <li><button id="stepper-build">Generate your static site!</button></li>
 | 
					          <li><button id="stepper-build">Generate your static site!</button></li>
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,21 @@
 | 
				
			|||||||
 * @param {string} type - "success" or "error".
 | 
					 * @param {string} type - "success" or "error".
 | 
				
			||||||
 * @param {number} duration - Duration in ms.
 | 
					 * @param {number} duration - Duration in ms.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Loader helpers ---
 | 
				
			||||||
 | 
					function showLoader(text = "Uploading...") {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) {
 | 
				
			||||||
 | 
					    loader.classList.add("active");
 | 
				
			||||||
 | 
					    document.getElementById("loader-text").textContent = text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function hideLoader() {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) loader.classList.remove("active");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Toast helpers ---
 | 
				
			||||||
function showToast(message, type = "success", duration = 3000) {
 | 
					function showToast(message, type = "success", duration = 3000) {
 | 
				
			||||||
  const container = document.getElementById("toast-container");
 | 
					  const container = document.getElementById("toast-container");
 | 
				
			||||||
  if (!container) return;
 | 
					  if (!container) return;
 | 
				
			||||||
@@ -29,9 +44,11 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Build action handler
 | 
					  // Build action handler
 | 
				
			||||||
  async function handleBuildClick() {
 | 
					  async function handleBuildClick() {
 | 
				
			||||||
 | 
					    showLoader("Building static site...");
 | 
				
			||||||
    // Trigger build on backend
 | 
					    // Trigger build on backend
 | 
				
			||||||
    const res = await fetch("/api/build", { method: "POST" });
 | 
					    const res = await fetch("/api/build", { method: "POST" });
 | 
				
			||||||
    const result = await res.json();
 | 
					    const result = await res.json();
 | 
				
			||||||
 | 
					    hideLoader();
 | 
				
			||||||
    if (result.status === "ok") {
 | 
					    if (result.status === "ok") {
 | 
				
			||||||
      // Show build success modal
 | 
					      // Show build success modal
 | 
				
			||||||
      if (buildModal) buildModal.style.display = "flex";
 | 
					      if (buildModal) buildModal.style.display = "flex";
 | 
				
			||||||
@@ -75,6 +92,18 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Preview Site button
 | 
				
			||||||
 | 
					  const previewBtn = document.getElementById("preview-site-btn");
 | 
				
			||||||
 | 
					  if (previewBtn) {
 | 
				
			||||||
 | 
					    const previewPort = previewBtn.getAttribute("data-preview-port") || "3000";
 | 
				
			||||||
 | 
					    previewBtn.onclick = () => {
 | 
				
			||||||
 | 
					      const host = window.location.hostname;
 | 
				
			||||||
 | 
					      const protocol = window.location.protocol;
 | 
				
			||||||
 | 
					      const url = `${protocol}//${host}:${previewPort}/`;
 | 
				
			||||||
 | 
					      window.open(url, "_blank");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  // Modal close logic
 | 
					  // Modal close logic
 | 
				
			||||||
  if (buildModal && buildModalClose) {
 | 
					  if (buildModal && buildModalClose) {
 | 
				
			||||||
    buildModalClose.onclick = () => {
 | 
					    buildModalClose.onclick = () => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,29 @@
 | 
				
			|||||||
// --- Arrays to store gallery and hero images ---
 | 
					 | 
				
			||||||
let galleryImages = [];
 | 
					let galleryImages = [];
 | 
				
			||||||
let heroImages = [];
 | 
					let heroImages = [];
 | 
				
			||||||
let allTags = []; // global tag list
 | 
					let allTags = [];
 | 
				
			||||||
 | 
					let showOnlyUntagged = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Fade-in helper ---
 | 
				
			||||||
 | 
					function applyFadeInImages(container) {
 | 
				
			||||||
 | 
					  container.querySelectorAll("img.fade-in-img").forEach(img => {
 | 
				
			||||||
 | 
					    const onLoad = () => img.classList.add("loaded");
 | 
				
			||||||
 | 
					    if (img.complete && img.naturalHeight !== 0) onLoad();
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					      img.addEventListener("load", onLoad, { once: true });
 | 
				
			||||||
 | 
					      img.addEventListener("error", () => {
 | 
				
			||||||
 | 
					        console.warn("Image failed to load:", img.dataset?.src || img.src);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Load images from server on page load ---
 | 
					// --- Load images from server on page load ---
 | 
				
			||||||
async function loadData() {
 | 
					async function loadData() {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const galleryRes = await fetch('/api/gallery');
 | 
					    galleryImages = await (await fetch('/api/gallery')).json();
 | 
				
			||||||
    galleryImages = await galleryRes.json();
 | 
					 | 
				
			||||||
    updateAllTags();
 | 
					    updateAllTags();
 | 
				
			||||||
    renderGallery();
 | 
					    renderGallery();
 | 
				
			||||||
 | 
					    heroImages = await (await fetch('/api/hero')).json();
 | 
				
			||||||
    const heroRes = await fetch('/api/hero');
 | 
					 | 
				
			||||||
    heroImages = await heroRes.json();
 | 
					 | 
				
			||||||
    renderHero();
 | 
					    renderHero();
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err);
 | 
					    console.error(err);
 | 
				
			||||||
@@ -24,22 +35,43 @@ async function loadData() {
 | 
				
			|||||||
function updateAllTags() {
 | 
					function updateAllTags() {
 | 
				
			||||||
  allTags = [];
 | 
					  allTags = [];
 | 
				
			||||||
  galleryImages.forEach(img => {
 | 
					  galleryImages.forEach(img => {
 | 
				
			||||||
    if (img.tags) img.tags.forEach(t => {
 | 
					    (img.tags || []).forEach(t => {
 | 
				
			||||||
      if (!allTags.includes(t)) allTags.push(t);
 | 
					      if (!allTags.includes(t)) allTags.push(t);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Helper: update count and button visibility ---
 | 
				
			||||||
 | 
					function updateCountAndButtons(prefix, count) {
 | 
				
			||||||
 | 
					  const countTop = document.getElementById(`${prefix}-count`);
 | 
				
			||||||
 | 
					  const countBottom = document.getElementById(`${prefix}-count-bottom`);
 | 
				
			||||||
 | 
					  if (countTop) countTop.innerHTML = `<p>${count} photos</p>`;
 | 
				
			||||||
 | 
					  if (countBottom) countBottom.innerHTML = `<p>${count} photos</p>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const removeAllBtn = document.getElementById(`remove-all-${prefix}`);
 | 
				
			||||||
 | 
					  const removeAllBtnBottom = document.getElementById(`remove-all-${prefix}-bottom`);
 | 
				
			||||||
 | 
					  if (removeAllBtn) removeAllBtn.style.display = count > 0 ? 'inline-block' : 'none';
 | 
				
			||||||
 | 
					  if (removeAllBtnBottom) removeAllBtnBottom.style.display = count > 0 ? 'inline-block' : 'none';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bottomUpload = document.getElementById(`bottom-${prefix}-upload`);
 | 
				
			||||||
 | 
					  if (bottomUpload) bottomUpload.style.display = count > 0 ? 'flex' : 'none';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Render gallery images with tags and delete buttons ---
 | 
					// --- Render gallery images with tags and delete buttons ---
 | 
				
			||||||
function renderGallery() {
 | 
					function renderGallery() {
 | 
				
			||||||
  const container = document.getElementById('gallery');
 | 
					  const container = document.getElementById('gallery');
 | 
				
			||||||
  container.innerHTML = '';
 | 
					  container.innerHTML = '';
 | 
				
			||||||
  galleryImages.forEach((img, i) => {
 | 
					  let imagesToShow = showOnlyUntagged
 | 
				
			||||||
 | 
					    ? galleryImages.filter(img => !img.tags || img.tags.length === 0)
 | 
				
			||||||
 | 
					    : galleryImages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  imagesToShow.forEach((img) => {
 | 
				
			||||||
 | 
					    const i = galleryImages.indexOf(img);
 | 
				
			||||||
    const div = document.createElement('div');
 | 
					    const div = document.createElement('div');
 | 
				
			||||||
    div.className = 'photo flex-item flex-column';
 | 
					    div.className = 'photo flex-item flex-column';
 | 
				
			||||||
    div.innerHTML = `
 | 
					    div.innerHTML = `
 | 
				
			||||||
      <div class="flex-item">
 | 
					      <div class="flex-item">
 | 
				
			||||||
        <img src="/photos/${img.src}">
 | 
					        <img class="fade-in-img" src="/photos/${img.src}">
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="tags-display" data-index="${i}"></div>
 | 
					      <div class="tags-display" data-index="${i}"></div>
 | 
				
			||||||
      <div class="flex-item flex-full">
 | 
					      <div class="flex-item flex-full">
 | 
				
			||||||
@@ -50,22 +82,17 @@ function renderGallery() {
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
    container.appendChild(div);
 | 
					    container.appendChild(div);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    renderTags(i, img.tags || []);
 | 
					    renderTags(i, img.tags || []);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Show/hide Remove All button
 | 
					  updateCountAndButtons('gallery', imagesToShow.length);
 | 
				
			||||||
  const removeAllBtn = document.getElementById('remove-all-gallery');
 | 
					  applyFadeInImages(container);
 | 
				
			||||||
  if (removeAllBtn) {
 | 
					 | 
				
			||||||
    removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Render tags for a single image ---
 | 
					// --- Render tags for a single image ---
 | 
				
			||||||
function renderTags(imgIndex, tags) {
 | 
					function renderTags(imgIndex, tags) {
 | 
				
			||||||
  const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
 | 
					  const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
 | 
				
			||||||
  const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
 | 
					  const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  tagsDisplay.innerHTML = '';
 | 
					  tagsDisplay.innerHTML = '';
 | 
				
			||||||
  inputContainer.innerHTML = '';
 | 
					  inputContainer.innerHTML = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -73,7 +100,6 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
    const span = document.createElement('span');
 | 
					    const span = document.createElement('span');
 | 
				
			||||||
    span.className = 'tag';
 | 
					    span.className = 'tag';
 | 
				
			||||||
    span.textContent = tag;
 | 
					    span.textContent = tag;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const remove = document.createElement('span');
 | 
					    const remove = document.createElement('span');
 | 
				
			||||||
    remove.className = 'remove-tag';
 | 
					    remove.className = 'remove-tag';
 | 
				
			||||||
    remove.textContent = '×';
 | 
					    remove.textContent = '×';
 | 
				
			||||||
@@ -82,7 +108,6 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
      updateTags(imgIndex, tags);
 | 
					      updateTags(imgIndex, tags);
 | 
				
			||||||
      renderTags(imgIndex, tags);
 | 
					      renderTags(imgIndex, tags);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					 | 
				
			||||||
    span.appendChild(remove);
 | 
					    span.appendChild(remove);
 | 
				
			||||||
    tagsDisplay.appendChild(span);
 | 
					    tagsDisplay.appendChild(span);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -92,6 +117,13 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
  input.placeholder = 'Add tag...';
 | 
					  input.placeholder = 'Add tag...';
 | 
				
			||||||
  inputContainer.appendChild(input);
 | 
					  inputContainer.appendChild(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const validateBtn = document.createElement('button');
 | 
				
			||||||
 | 
					  validateBtn.textContent = '✔️';
 | 
				
			||||||
 | 
					  validateBtn.className = 'validate-tag-btn';
 | 
				
			||||||
 | 
					  validateBtn.style.display = 'none';
 | 
				
			||||||
 | 
					  validateBtn.style.marginLeft = '4px';
 | 
				
			||||||
 | 
					  inputContainer.appendChild(validateBtn);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const suggestionBox = document.createElement('ul');
 | 
					  const suggestionBox = document.createElement('ul');
 | 
				
			||||||
  suggestionBox.className = 'suggestions';
 | 
					  suggestionBox.className = 'suggestions';
 | 
				
			||||||
  inputContainer.appendChild(suggestionBox);
 | 
					  inputContainer.appendChild(suggestionBox);
 | 
				
			||||||
@@ -108,30 +140,20 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const updateSuggestions = () => {
 | 
					  const updateSuggestions = () => {
 | 
				
			||||||
    const value = input.value.toLowerCase();
 | 
					    const value = input.value.toLowerCase();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const allTagsFlat = galleryImages.flatMap(img => img.tags || []);
 | 
					    const allTagsFlat = galleryImages.flatMap(img => img.tags || []);
 | 
				
			||||||
    const tagCount = {};
 | 
					    const tagCount = {};
 | 
				
			||||||
    allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1);
 | 
					    allTagsFlat.forEach(t => tagCount[t] = (tagCount[t] || 0) + 1);
 | 
				
			||||||
 | 
					    const allTagsSorted = Object.keys(tagCount).sort((a, b) => tagCount[b] - tagCount[a]);
 | 
				
			||||||
    const allTagsSorted = Object.keys(tagCount)
 | 
					 | 
				
			||||||
      .sort((a, b) => tagCount[b] - tagCount[a]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t));
 | 
					    const suggestions = allTagsSorted.filter(t => t.toLowerCase().startsWith(value) && !tags.includes(t));
 | 
				
			||||||
 | 
					 | 
				
			||||||
    suggestionBox.innerHTML = '';
 | 
					    suggestionBox.innerHTML = '';
 | 
				
			||||||
    selectedIndex = -1;
 | 
					    selectedIndex = -1;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (suggestions.length) {
 | 
					    if (suggestions.length) {
 | 
				
			||||||
      suggestionBox.style.display = 'block';
 | 
					      suggestionBox.style.display = 'block';
 | 
				
			||||||
      suggestions.forEach((s, idx) => {
 | 
					      suggestions.forEach((s, idx) => {
 | 
				
			||||||
        const li = document.createElement('li');
 | 
					        const li = document.createElement('li');
 | 
				
			||||||
        li.style.fontStyle = 'italic';
 | 
					        li.style.fontStyle = 'italic';
 | 
				
			||||||
        li.style.textAlign = 'left';
 | 
					        li.style.textAlign = 'left';
 | 
				
			||||||
 | 
					        li.innerHTML = `<b>${s.substring(0, input.value.length)}</b>${s.substring(input.value.length)}`;
 | 
				
			||||||
        const boldPart = `<b>${s.substring(0, input.value.length)}</b>`;
 | 
					 | 
				
			||||||
        const rest = s.substring(input.value.length);
 | 
					 | 
				
			||||||
        li.innerHTML = boldPart + rest;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        li.addEventListener('mousedown', (e) => {
 | 
					        li.addEventListener('mousedown', (e) => {
 | 
				
			||||||
          e.preventDefault();
 | 
					          e.preventDefault();
 | 
				
			||||||
          addTag(s);
 | 
					          addTag(s);
 | 
				
			||||||
@@ -139,7 +161,6 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
          input.focus();
 | 
					          input.focus();
 | 
				
			||||||
          updateSuggestions();
 | 
					          updateSuggestions();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					 | 
				
			||||||
        li.onmouseover = () => selectedIndex = idx;
 | 
					        li.onmouseover = () => selectedIndex = idx;
 | 
				
			||||||
        suggestionBox.appendChild(li);
 | 
					        suggestionBox.appendChild(li);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@@ -148,9 +169,14 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  input.addEventListener('input', updateSuggestions);
 | 
					  input.addEventListener('input', () => {
 | 
				
			||||||
  input.addEventListener('focus', updateSuggestions);
 | 
					    updateSuggestions();
 | 
				
			||||||
 | 
					    validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  input.addEventListener('focus', () => {
 | 
				
			||||||
 | 
					    updateSuggestions();
 | 
				
			||||||
 | 
					    validateBtn.style.display = input.value.trim() ? 'inline-block' : 'none';
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
  input.addEventListener('keydown', (e) => {
 | 
					  input.addEventListener('keydown', (e) => {
 | 
				
			||||||
    const items = suggestionBox.querySelectorAll('li');
 | 
					    const items = suggestionBox.querySelectorAll('li');
 | 
				
			||||||
    if (e.key === 'ArrowDown') {
 | 
					    if (e.key === 'ArrowDown') {
 | 
				
			||||||
@@ -172,23 +198,31 @@ function renderTags(imgIndex, tags) {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      input.value = '';
 | 
					      input.value = '';
 | 
				
			||||||
      updateSuggestions();
 | 
					      updateSuggestions();
 | 
				
			||||||
 | 
					      validateBtn.style.display = 'none';
 | 
				
			||||||
    } else if ([' ', ','].includes(e.key)) {
 | 
					    } else if ([' ', ','].includes(e.key)) {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      addTag(input.value);
 | 
					      addTag(input.value);
 | 
				
			||||||
      input.value = '';
 | 
					      input.value = '';
 | 
				
			||||||
      updateSuggestions();
 | 
					      updateSuggestions();
 | 
				
			||||||
 | 
					      validateBtn.style.display = 'none';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  input.addEventListener('blur', () => {
 | 
					  input.addEventListener('blur', () => {
 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
    suggestionBox.style.display = 'none';
 | 
					    suggestionBox.style.display = 'none';
 | 
				
			||||||
    input.value = '';
 | 
					    input.value = '';
 | 
				
			||||||
    }, 150);
 | 
					    validateBtn.style.display = 'none';
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					  validateBtn.addEventListener('mousedown', (e) => {
 | 
				
			||||||
  input.focus();
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    if (input.value.trim()) {
 | 
				
			||||||
 | 
					      addTag(input.value.trim());
 | 
				
			||||||
 | 
					      input.value = '';
 | 
				
			||||||
      updateSuggestions();
 | 
					      updateSuggestions();
 | 
				
			||||||
 | 
					      validateBtn.style.display = 'none';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  updateSuggestions();
 | 
				
			||||||
 | 
					  if (!input.value.trim()) suggestionBox.style.display = 'none';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Update tags in galleryImages array ---
 | 
					// --- Update tags in galleryImages array ---
 | 
				
			||||||
@@ -206,7 +240,7 @@ function renderHero() {
 | 
				
			|||||||
    div.className = 'photo flex-item flex-column';
 | 
					    div.className = 'photo flex-item flex-column';
 | 
				
			||||||
    div.innerHTML = `
 | 
					    div.innerHTML = `
 | 
				
			||||||
      <div class="flex-item">
 | 
					      <div class="flex-item">
 | 
				
			||||||
        <img src="/photos/${img.src}">
 | 
					        <img class="fade-in-img" src="/photos/${img.src}">
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="flex-item flex-full">
 | 
					      <div class="flex-item flex-full">
 | 
				
			||||||
        <div class="flex-item flex-end">
 | 
					        <div class="flex-item flex-end">
 | 
				
			||||||
@@ -216,12 +250,8 @@ function renderHero() {
 | 
				
			|||||||
    `;
 | 
					    `;
 | 
				
			||||||
    container.appendChild(div);
 | 
					    container.appendChild(div);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					  updateCountAndButtons('hero', heroImages.length);
 | 
				
			||||||
  // Show/hide Remove All button
 | 
					  applyFadeInImages(container);
 | 
				
			||||||
  const removeAllBtn = document.getElementById('remove-all-hero');
 | 
					 | 
				
			||||||
  if (removeAllBtn) {
 | 
					 | 
				
			||||||
    removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Save gallery to server ---
 | 
					// --- Save gallery to server ---
 | 
				
			||||||
@@ -267,33 +297,27 @@ async function refreshHero() {
 | 
				
			|||||||
function showToast(message, type = "success", duration = 3000) {
 | 
					function showToast(message, type = "success", duration = 3000) {
 | 
				
			||||||
  const container = document.getElementById("toast-container");
 | 
					  const container = document.getElementById("toast-container");
 | 
				
			||||||
  if (!container) return;
 | 
					  if (!container) return;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const toast = document.createElement("div");
 | 
					  const toast = document.createElement("div");
 | 
				
			||||||
  toast.className = `toast ${type}`;
 | 
					  toast.className = `toast ${type}`;
 | 
				
			||||||
  toast.textContent = message;
 | 
					  toast.textContent = message;
 | 
				
			||||||
  container.appendChild(toast);
 | 
					  container.appendChild(toast);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  requestAnimationFrame(() => toast.classList.add("show"));
 | 
					  requestAnimationFrame(() => toast.classList.add("show"));
 | 
				
			||||||
 | 
					 | 
				
			||||||
  setTimeout(() => {
 | 
					  setTimeout(() => {
 | 
				
			||||||
    toast.classList.remove("show");
 | 
					    toast.classList.remove("show");
 | 
				
			||||||
    toast.addEventListener("transitionend", () => toast.remove());
 | 
					    toast.addEventListener("transitionend", () => toast.remove());
 | 
				
			||||||
  }, duration);
 | 
					  }, duration);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
 | 
					let pendingDelete = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Show delete confirmation modal ---
 | 
					// --- Show delete confirmation modal ---
 | 
				
			||||||
function showDeleteModal(type, index = null) {
 | 
					function showDeleteModal(type, index = null) {
 | 
				
			||||||
  pendingDelete = { type, index };
 | 
					  pendingDelete = { type, index };
 | 
				
			||||||
  const modalText = document.getElementById('delete-modal-text');
 | 
					  const modalText = document.getElementById('delete-modal-text');
 | 
				
			||||||
  if (type === 'gallery-all') {
 | 
					  modalText.textContent =
 | 
				
			||||||
    modalText.textContent = "Are you sure you want to delete ALL gallery images?";
 | 
					    type === 'gallery-all' ? "Are you sure you want to delete ALL gallery images?"
 | 
				
			||||||
  } else if (type === 'hero-all') {
 | 
					    : type === 'hero-all' ? "Are you sure you want to delete ALL hero images?"
 | 
				
			||||||
    modalText.textContent = "Are you sure you want to delete ALL hero images?";
 | 
					    : "Are you sure you want to delete this image?";
 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    modalText.textContent = "Are you sure you want to delete this image?";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  document.getElementById('delete-modal').style.display = 'flex';
 | 
					  document.getElementById('delete-modal').style.display = 'flex';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -306,15 +330,10 @@ function hideDeleteModal() {
 | 
				
			|||||||
// --- Confirm deletion ---
 | 
					// --- Confirm deletion ---
 | 
				
			||||||
async function confirmDelete() {
 | 
					async function confirmDelete() {
 | 
				
			||||||
  if (!pendingDelete) return;
 | 
					  if (!pendingDelete) return;
 | 
				
			||||||
  if (pendingDelete.type === 'gallery') {
 | 
					  if (pendingDelete.type === 'gallery') await actuallyDeleteGalleryImage(pendingDelete.index);
 | 
				
			||||||
    await actuallyDeleteGalleryImage(pendingDelete.index);
 | 
					  else if (pendingDelete.type === 'hero') await actuallyDeleteHeroImage(pendingDelete.index);
 | 
				
			||||||
  } else if (pendingDelete.type === 'hero') {
 | 
					  else if (pendingDelete.type === 'gallery-all') await actuallyDeleteAllGalleryImages();
 | 
				
			||||||
    await actuallyDeleteHeroImage(pendingDelete.index);
 | 
					  else if (pendingDelete.type === 'hero-all') await actuallyDeleteAllHeroImages();
 | 
				
			||||||
  } else if (pendingDelete.type === 'gallery-all') {
 | 
					 | 
				
			||||||
    await actuallyDeleteAllGalleryImages();
 | 
					 | 
				
			||||||
  } else if (pendingDelete.type === 'hero-all') {
 | 
					 | 
				
			||||||
    await actuallyDeleteAllHeroImages();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  hideDeleteModal();
 | 
					  hideDeleteModal();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -396,15 +415,35 @@ async function actuallyDeleteAllHeroImages() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// --- Modal event listeners and bulk delete buttons ---
 | 
					// --- Modal event listeners and bulk delete buttons ---
 | 
				
			||||||
document.addEventListener('DOMContentLoaded', () => {
 | 
					document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
  document.getElementById('delete-modal-close').onclick = hideDeleteModal;
 | 
					  ['delete-modal-close', 'delete-modal-cancel'].forEach(id => {
 | 
				
			||||||
  document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
 | 
					    const el = document.getElementById(id);
 | 
				
			||||||
  document.getElementById('delete-modal-confirm').onclick = confirmDelete;
 | 
					    if (el) el.onclick = hideDeleteModal;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const confirmBtn = document.getElementById('delete-modal-confirm');
 | 
				
			||||||
 | 
					  if (confirmBtn) confirmBtn.onclick = confirmDelete;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Gallery filter radios
 | 
				
			||||||
 | 
					  const showAllRadio = document.getElementById('show-all-radio');
 | 
				
			||||||
 | 
					  const showUntaggedRadio = document.getElementById('show-untagged-radio');
 | 
				
			||||||
 | 
					  if (showAllRadio) showAllRadio.addEventListener('change', () => {
 | 
				
			||||||
 | 
					    showOnlyUntagged = false;
 | 
				
			||||||
 | 
					    renderGallery();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (showUntaggedRadio) showUntaggedRadio.addEventListener('change', () => {
 | 
				
			||||||
 | 
					    showOnlyUntagged = true;
 | 
				
			||||||
 | 
					    renderGallery();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Bulk delete buttons
 | 
					  // Bulk delete buttons
 | 
				
			||||||
  const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
 | 
					  [
 | 
				
			||||||
  const removeAllHeroBtn = document.getElementById('remove-all-hero');
 | 
					    ['remove-all-gallery', 'gallery-all'],
 | 
				
			||||||
  if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
 | 
					    ['remove-all-gallery-bottom', 'gallery-all'],
 | 
				
			||||||
  if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
 | 
					    ['remove-all-hero', 'hero-all'],
 | 
				
			||||||
 | 
					    ['remove-all-hero-bottom', 'hero-all']
 | 
				
			||||||
 | 
					  ].forEach(([btnId, type]) => {
 | 
				
			||||||
 | 
					    const btn = document.getElementById(btnId);
 | 
				
			||||||
 | 
					    if (btn) btn.onclick = () => showDeleteModal(type);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Initialize ---
 | 
					// --- Initialize ---
 | 
				
			||||||
@@ -12,128 +12,122 @@ function showToast(message, type = "success", duration = 3000) {
 | 
				
			|||||||
  }, duration);
 | 
					  }, duration);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function showLoader(text = "Uploading...") {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) {
 | 
				
			||||||
 | 
					    loader.classList.add("active");
 | 
				
			||||||
 | 
					    document.getElementById("loader-text").textContent = text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function hideLoader() {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) loader.classList.remove("active");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
document.addEventListener("DOMContentLoaded", () => {
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
  // Form and menu logic
 | 
					  // --- Section Forms ---
 | 
				
			||||||
  const form = document.getElementById("site-info-form");
 | 
					  const forms = {
 | 
				
			||||||
 | 
					    info: document.getElementById("info-form"),
 | 
				
			||||||
 | 
					    social: document.getElementById("social-form"),
 | 
				
			||||||
 | 
					    menu: document.getElementById("menu-form"),
 | 
				
			||||||
 | 
					    footer: document.getElementById("footer-form"),
 | 
				
			||||||
 | 
					    legals: document.getElementById("legals-form"),
 | 
				
			||||||
 | 
					    build: document.getElementById("build-form")
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --- Menu logic ---
 | 
				
			||||||
  const menuList = document.getElementById("menu-items-list");
 | 
					  const menuList = document.getElementById("menu-items-list");
 | 
				
			||||||
  const addMenuBtn = document.getElementById("add-menu-item");
 | 
					  const addMenuBtn = document.getElementById("add-menu-item");
 | 
				
			||||||
 | 
					 | 
				
			||||||
  let menuItems = [];
 | 
					  let menuItems = [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Render menu items
 | 
					 | 
				
			||||||
  function renderMenuItems() {
 | 
					  function renderMenuItems() {
 | 
				
			||||||
    menuList.innerHTML = "";
 | 
					    menuList.innerHTML = "";
 | 
				
			||||||
    menuItems.forEach((item, idx) => {
 | 
					    menuItems.forEach((item, idx) => {
 | 
				
			||||||
      const div = document.createElement("div");
 | 
					      menuList.innerHTML += `
 | 
				
			||||||
      div.style.display = "flex";
 | 
					        <div style="display:flex;gap:8px;margin-bottom:6px;">
 | 
				
			||||||
      div.style.gap = "8px";
 | 
					 | 
				
			||||||
      div.style.marginBottom = "6px";
 | 
					 | 
				
			||||||
      div.innerHTML = `
 | 
					 | 
				
			||||||
          <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
 | 
					          <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
 | 
				
			||||||
          <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
 | 
					          <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
 | 
				
			||||||
          <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
					          <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      menuList.appendChild(div);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Update menu items from inputs
 | 
					 | 
				
			||||||
  function updateMenuItemsFromInputs() {
 | 
					  function updateMenuItemsFromInputs() {
 | 
				
			||||||
    const inputs = menuList.querySelectorAll("input");
 | 
					    const inputs = menuList.querySelectorAll("input");
 | 
				
			||||||
    const items = [];
 | 
					    menuItems = [];
 | 
				
			||||||
    for (let i = 0; i < inputs.length; i += 2) {
 | 
					    for (let i = 0; i < inputs.length; i += 2) {
 | 
				
			||||||
      const label = inputs[i].value.trim();
 | 
					      const label = inputs[i].value.trim();
 | 
				
			||||||
      const href = inputs[i + 1].value.trim();
 | 
					      const href = inputs[i + 1].value.trim();
 | 
				
			||||||
      if (label || href) items.push({ label, href });
 | 
					      if (label || href) menuItems.push({ label, href });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    menuItems = items;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Intellectual property paragraphs logic
 | 
					  // --- Intellectual property paragraphs logic ---
 | 
				
			||||||
  const ipList = document.getElementById("ip-list");
 | 
					  const ipList = document.getElementById("ip-list");
 | 
				
			||||||
  const addIpBtn = document.getElementById("add-ip-paragraph");
 | 
					  const addIpBtn = document.getElementById("add-ip-paragraph");
 | 
				
			||||||
  let ipParagraphs = [];
 | 
					  let ipParagraphs = [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Render IP paragraphs
 | 
					 | 
				
			||||||
  function renderIpParagraphs() {
 | 
					  function renderIpParagraphs() {
 | 
				
			||||||
    ipList.innerHTML = "";
 | 
					    ipList.innerHTML = "";
 | 
				
			||||||
    ipParagraphs.forEach((item, idx) => {
 | 
					    ipParagraphs.forEach((item, idx) => {
 | 
				
			||||||
      const div = document.createElement("div");
 | 
					      ipList.innerHTML += `
 | 
				
			||||||
      div.style.display = "flex";
 | 
					        <div style="display:flex;gap:8px;margin-bottom:6px;">
 | 
				
			||||||
      div.style.gap = "8px";
 | 
					 | 
				
			||||||
      div.style.marginBottom = "6px";
 | 
					 | 
				
			||||||
      div.innerHTML = `
 | 
					 | 
				
			||||||
          <textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
 | 
					          <textarea placeholder="Paragraph" required style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
 | 
				
			||||||
          <button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
 | 
					          <button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      ipList.appendChild(div);
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Update IP paragraphs from textareas
 | 
					 | 
				
			||||||
  function updateIpParagraphsFromInputs() {
 | 
					  function updateIpParagraphsFromInputs() {
 | 
				
			||||||
    const textareas = ipList.querySelectorAll("textarea");
 | 
					    ipParagraphs = Array.from(ipList.querySelectorAll("textarea"))
 | 
				
			||||||
    ipParagraphs = Array.from(textareas).map(textarea => ({
 | 
					      .map(textarea => ({ paragraph: textarea.value.trim() }))
 | 
				
			||||||
      paragraph: textarea.value.trim()
 | 
					      .filter(item => item.paragraph !== "");
 | 
				
			||||||
    })).filter(item => item.paragraph !== "");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Build options
 | 
					  // --- Build options & Theme select ---
 | 
				
			||||||
  const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
 | 
					  const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
 | 
				
			||||||
  const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
 | 
					  const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Theme select
 | 
					 | 
				
			||||||
  const themeSelect = document.getElementById("theme-select");
 | 
					  const themeSelect = document.getElementById("theme-select");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Thumbnail upload and modal logic
 | 
					  // --- Thumbnail upload and modal logic ---
 | 
				
			||||||
  const thumbnailInput = form?.elements["social.thumbnail"];
 | 
					  const thumbnailInput = document.getElementById("social-thumbnail");
 | 
				
			||||||
  const thumbnailUpload = document.getElementById("thumbnail-upload");
 | 
					  const thumbnailUpload = document.getElementById("thumbnail-upload");
 | 
				
			||||||
  const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
 | 
					  const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
 | 
				
			||||||
  const thumbnailPreview = document.getElementById("thumbnail-preview");
 | 
					  const thumbnailPreview = document.getElementById("thumbnail-preview");
 | 
				
			||||||
  const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
 | 
					  const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Modal elements for delete confirmation
 | 
					  // --- Modal helpers ---
 | 
				
			||||||
  const deleteModal = document.getElementById("delete-modal");
 | 
					  function setupModal(modal, closeBtn, confirmBtn, cancelBtn, onConfirm) {
 | 
				
			||||||
  const deleteModalClose = document.getElementById("delete-modal-close");
 | 
					    if (!modal) return;
 | 
				
			||||||
  const deleteModalConfirm = document.getElementById("delete-modal-confirm");
 | 
					    if (closeBtn) closeBtn.onclick = () => modal.style.display = "none";
 | 
				
			||||||
  const deleteModalCancel = document.getElementById("delete-modal-cancel");
 | 
					    if (cancelBtn) cancelBtn.onclick = () => modal.style.display = "none";
 | 
				
			||||||
 | 
					    window.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					      if (e.target === modal) modal.style.display = "none";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (confirmBtn && onConfirm) confirmBtn.onclick = onConfirm;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Modal elements for theme deletion
 | 
					  // --- Thumbnail preview logic ---
 | 
				
			||||||
  const deleteThemeModal = document.getElementById("delete-theme-modal");
 | 
					 | 
				
			||||||
  const deleteThemeModalClose = document.getElementById("delete-theme-modal-close");
 | 
					 | 
				
			||||||
  const deleteThemeModalConfirm = document.getElementById("delete-theme-modal-confirm");
 | 
					 | 
				
			||||||
  const deleteThemeModalCancel = document.getElementById("delete-theme-modal-cancel");
 | 
					 | 
				
			||||||
  const deleteThemeModalText = document.getElementById("delete-theme-modal-text");
 | 
					 | 
				
			||||||
  let themeToDelete = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Show/hide thumbnail preview, remove button, and choose button
 | 
					 | 
				
			||||||
  function updateThumbnailPreview(src) {
 | 
					  function updateThumbnailPreview(src) {
 | 
				
			||||||
    if (thumbnailPreview) {
 | 
					    if (thumbnailPreview) {
 | 
				
			||||||
      thumbnailPreview.src = src || "";
 | 
					      thumbnailPreview.src = src || "";
 | 
				
			||||||
      thumbnailPreview.style.display = src ? "block" : "none";
 | 
					      thumbnailPreview.style.display = src ? "block" : "none";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (removeThumbnailBtn) {
 | 
					    if (removeThumbnailBtn) removeThumbnailBtn.style.display = src ? "inline-block" : "none";
 | 
				
			||||||
      removeThumbnailBtn.style.display = src ? "inline-block" : "none";
 | 
					    if (chooseThumbnailBtn) chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (chooseThumbnailBtn) {
 | 
					 | 
				
			||||||
      chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Choose thumbnail button triggers file input
 | 
					 | 
				
			||||||
  if (chooseThumbnailBtn && thumbnailUpload) {
 | 
					  if (chooseThumbnailBtn && thumbnailUpload) {
 | 
				
			||||||
    chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click());
 | 
					    chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Handle thumbnail upload and refresh preview (with cache busting)
 | 
					 | 
				
			||||||
  if (thumbnailUpload) {
 | 
					  if (thumbnailUpload) {
 | 
				
			||||||
    thumbnailUpload.addEventListener("change", async (e) => {
 | 
					    thumbnailUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
      const file = e.target.files[0];
 | 
					      const file = e.target.files[0];
 | 
				
			||||||
      if (!file) return;
 | 
					      if (!file) return;
 | 
				
			||||||
 | 
					      showLoader("Uploading thumbnail...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      formData.append("file", file);
 | 
					      formData.append("file", file);
 | 
				
			||||||
      const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        if (thumbnailInput) thumbnailInput.value = result.filename;
 | 
					        if (thumbnailInput) thumbnailInput.value = result.filename;
 | 
				
			||||||
        updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
 | 
					        updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
 | 
				
			||||||
@@ -141,27 +135,20 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("❌ Error uploading thumbnail", "error");
 | 
					        showToast("❌ Error uploading thumbnail", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      updateSectionStatus("social");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Remove thumbnail button triggers modal
 | 
					 | 
				
			||||||
  if (removeThumbnailBtn) {
 | 
					  if (removeThumbnailBtn) {
 | 
				
			||||||
    removeThumbnailBtn.addEventListener("click", () => {
 | 
					    removeThumbnailBtn.addEventListener("click", () => {
 | 
				
			||||||
      deleteModal.style.display = "flex";
 | 
					      document.getElementById("delete-modal").style.display = "flex";
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  setupModal(
 | 
				
			||||||
  // Modal logic for thumbnail deletion
 | 
					    document.getElementById("delete-modal"),
 | 
				
			||||||
  if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
 | 
					    document.getElementById("delete-modal-close"),
 | 
				
			||||||
    deleteModalClose.onclick = deleteModalCancel.onclick = () => {
 | 
					    document.getElementById("delete-modal-confirm"),
 | 
				
			||||||
      deleteModal.style.display = "none";
 | 
					    document.getElementById("delete-modal-cancel"),
 | 
				
			||||||
    };
 | 
					    async () => {
 | 
				
			||||||
    window.onclick = function(event) {
 | 
					 | 
				
			||||||
      if (event.target === deleteModal) {
 | 
					 | 
				
			||||||
        deleteModal.style.display = "none";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    deleteModalConfirm.onclick = async () => {
 | 
					 | 
				
			||||||
      const res = await fetch("/api/thumbnail/remove", { method: "POST" });
 | 
					      const res = await fetch("/api/thumbnail/remove", { method: "POST" });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
@@ -171,46 +158,38 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("❌ Error removing thumbnail", "error");
 | 
					        showToast("❌ Error removing thumbnail", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      deleteModal.style.display = "none";
 | 
					      document.getElementById("delete-modal").style.display = "none";
 | 
				
			||||||
    };
 | 
					      updateSectionStatus("social");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Theme upload logic (custom theme folder)
 | 
					  // --- Theme upload logic ---
 | 
				
			||||||
  const themeUpload = document.getElementById("theme-upload");
 | 
					  const themeUpload = document.getElementById("theme-upload");
 | 
				
			||||||
  const chooseThemeBtn = document.getElementById("choose-theme-btn");
 | 
					  const chooseThemeBtn = document.getElementById("choose-theme-btn");
 | 
				
			||||||
  if (chooseThemeBtn && themeUpload) {
 | 
					  if (chooseThemeBtn && themeUpload) {
 | 
				
			||||||
    chooseThemeBtn.addEventListener("click", () => themeUpload.click());
 | 
					    chooseThemeBtn.addEventListener("click", () => themeUpload.click());
 | 
				
			||||||
    themeUpload.addEventListener("change", async (e) => {
 | 
					    themeUpload.addEventListener("change", async (e) => {
 | 
				
			||||||
      const files = Array.from(e.target.files);
 | 
					      const files = Array.from(e.target.files);
 | 
				
			||||||
      if (files.length === 0) return;
 | 
					      if (!files.length) return;
 | 
				
			||||||
 | 
					      showLoader("Uploading theme...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      files.forEach(file => {
 | 
					      files.forEach(file => formData.append("files", file, file.webkitRelativePath || file.name));
 | 
				
			||||||
        formData.append("files", file, file.webkitRelativePath || file.name);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Theme uploaded!", "success");
 | 
					        showToast("✅ Theme uploaded!", "success");
 | 
				
			||||||
        // Refresh theme select after upload
 | 
					        refreshThemes();
 | 
				
			||||||
        fetch("/api/themes")
 | 
					 | 
				
			||||||
          .then(res => res.json())
 | 
					 | 
				
			||||||
          .then(themes => {
 | 
					 | 
				
			||||||
            themeSelect.innerHTML = "";
 | 
					 | 
				
			||||||
            themes.forEach(theme => {
 | 
					 | 
				
			||||||
              const option = document.createElement("option");
 | 
					 | 
				
			||||||
              option.value = theme;
 | 
					 | 
				
			||||||
              option.textContent = theme;
 | 
					 | 
				
			||||||
              themeSelect.appendChild(option);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("❌ Error uploading theme", "error");
 | 
					        showToast("❌ Error uploading theme", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      updateSectionStatus("build");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Remove theme button triggers modal
 | 
					  // --- Remove theme logic ---
 | 
				
			||||||
  const removeThemeBtn = document.getElementById("remove-theme-btn");
 | 
					  const removeThemeBtn = document.getElementById("remove-theme-btn");
 | 
				
			||||||
 | 
					  let themeToDelete = null;
 | 
				
			||||||
  if (removeThemeBtn && themeSelect) {
 | 
					  if (removeThemeBtn && themeSelect) {
 | 
				
			||||||
    removeThemeBtn.addEventListener("click", () => {
 | 
					    removeThemeBtn.addEventListener("click", () => {
 | 
				
			||||||
      const theme = themeSelect.value;
 | 
					      const theme = themeSelect.value;
 | 
				
			||||||
@@ -220,55 +199,39 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      themeToDelete = theme;
 | 
					      themeToDelete = theme;
 | 
				
			||||||
      deleteThemeModalText.textContent = `Are you sure you want to remove theme "${theme}"?`;
 | 
					      document.getElementById("delete-theme-modal-text").textContent = `Are you sure you want to remove theme "${theme}"?`;
 | 
				
			||||||
      deleteThemeModal.style.display = "flex";
 | 
					      document.getElementById("delete-theme-modal").style.display = "flex";
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  setupModal(
 | 
				
			||||||
  // Modal logic for theme deletion
 | 
					    document.getElementById("delete-theme-modal"),
 | 
				
			||||||
  if (deleteThemeModal && deleteThemeModalClose && deleteThemeModalConfirm && deleteThemeModalCancel) {
 | 
					    document.getElementById("delete-theme-modal-close"),
 | 
				
			||||||
    deleteThemeModalClose.onclick = deleteThemeModalCancel.onclick = () => {
 | 
					    document.getElementById("delete-theme-modal-confirm"),
 | 
				
			||||||
      deleteThemeModal.style.display = "none";
 | 
					    document.getElementById("delete-theme-modal-cancel"),
 | 
				
			||||||
      themeToDelete = null;
 | 
					    async () => {
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    window.onclick = function(event) {
 | 
					 | 
				
			||||||
      if (event.target === deleteThemeModal) {
 | 
					 | 
				
			||||||
        deleteThemeModal.style.display = "none";
 | 
					 | 
				
			||||||
        themeToDelete = null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    deleteThemeModalConfirm.onclick = async () => {
 | 
					 | 
				
			||||||
      if (!themeToDelete) return;
 | 
					      if (!themeToDelete) return;
 | 
				
			||||||
 | 
					      showLoader("Removing theme...");
 | 
				
			||||||
      const res = await fetch("/api/theme/remove", {
 | 
					      const res = await fetch("/api/theme/remove", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
        body: JSON.stringify({ theme: themeToDelete })
 | 
					        body: JSON.stringify({ theme: themeToDelete })
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Theme removed!", "success");
 | 
					        showToast("✅ Theme removed!", "success");
 | 
				
			||||||
        // Refresh theme select
 | 
					        refreshThemes();
 | 
				
			||||||
        fetch("/api/themes")
 | 
					 | 
				
			||||||
          .then(res => res.json())
 | 
					 | 
				
			||||||
          .then(themes => {
 | 
					 | 
				
			||||||
            themeSelect.innerHTML = "";
 | 
					 | 
				
			||||||
            themes.forEach(theme => {
 | 
					 | 
				
			||||||
              const option = document.createElement("option");
 | 
					 | 
				
			||||||
              option.value = theme;
 | 
					 | 
				
			||||||
              option.textContent = theme;
 | 
					 | 
				
			||||||
              themeSelect.appendChild(option);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast(result.error || "❌ Error removing theme", "error");
 | 
					        showToast(result.error || "❌ Error removing theme", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      deleteThemeModal.style.display = "none";
 | 
					      document.getElementById("delete-theme-modal").style.display = "none";
 | 
				
			||||||
      themeToDelete = null;
 | 
					      themeToDelete = null;
 | 
				
			||||||
    };
 | 
					      updateSectionStatus("build");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Fetch theme list and populate select
 | 
					  // --- Theme select refresh ---
 | 
				
			||||||
  if (themeSelect) {
 | 
					  function refreshThemes() {
 | 
				
			||||||
    fetch("/api/themes")
 | 
					    fetch("/api/themes")
 | 
				
			||||||
      .then(res => res.json())
 | 
					      .then(res => res.json())
 | 
				
			||||||
      .then(themes => {
 | 
					      .then(themes => {
 | 
				
			||||||
@@ -279,144 +242,276 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
          option.textContent = theme;
 | 
					          option.textContent = theme;
 | 
				
			||||||
          themeSelect.appendChild(option);
 | 
					          themeSelect.appendChild(option);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Set selected value after loading config
 | 
					        loadConfigAndUpdateBuildStatus();
 | 
				
			||||||
        fetch("/api/site-info")
 | 
					 | 
				
			||||||
          .then(res => res.json())
 | 
					 | 
				
			||||||
          .then(data => {
 | 
					 | 
				
			||||||
            themeSelect.value = data.build?.theme || "";
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Load config from server and populate form
 | 
					  // --- Config loading ---
 | 
				
			||||||
  if (form) {
 | 
					  let loadedConfig = {};
 | 
				
			||||||
 | 
					  function loadConfigAndUpdateBuildStatus() {
 | 
				
			||||||
    fetch("/api/site-info")
 | 
					    fetch("/api/site-info")
 | 
				
			||||||
      .then(res => res.json())
 | 
					      .then(res => res.json())
 | 
				
			||||||
      .then(data => {
 | 
					      .then(data => {
 | 
				
			||||||
 | 
					        loadedConfig = data;
 | 
				
			||||||
 | 
					        // Info
 | 
				
			||||||
 | 
					        if (forms.info) {
 | 
				
			||||||
 | 
					          forms.info.elements["info.title"].value = data.info?.title || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.subtitle"].value = data.info?.subtitle || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.description"].value = data.info?.description || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.canonical"].value = data.info?.canonical || "";
 | 
				
			||||||
 | 
					          forms.info.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
 | 
				
			||||||
 | 
					          forms.info.elements["info.author"].value = data.info?.author || "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Social
 | 
				
			||||||
 | 
					        if (forms.social) {
 | 
				
			||||||
 | 
					          forms.social.elements["social.instagram_url"].value = data.social?.instagram_url || "";
 | 
				
			||||||
 | 
					          if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
 | 
				
			||||||
 | 
					          updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Menu
 | 
				
			||||||
 | 
					        menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
 | 
				
			||||||
 | 
					        renderMenuItems();
 | 
				
			||||||
 | 
					        // Footer
 | 
				
			||||||
 | 
					        if (forms.footer) {
 | 
				
			||||||
 | 
					          forms.footer.elements["footer.copyright"].value = data.footer?.copyright || "";
 | 
				
			||||||
 | 
					          forms.footer.elements["footer.legal_label"].value = data.footer?.legal_label || "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Legals
 | 
				
			||||||
        ipParagraphs = Array.isArray(data.legals?.intellectual_property)
 | 
					        ipParagraphs = Array.isArray(data.legals?.intellectual_property)
 | 
				
			||||||
          ? data.legals.intellectual_property
 | 
					          ? data.legals.intellectual_property
 | 
				
			||||||
          : [];
 | 
					          : [];
 | 
				
			||||||
        renderIpParagraphs();
 | 
					        renderIpParagraphs();
 | 
				
			||||||
        menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
 | 
					        if (forms.legals) {
 | 
				
			||||||
        renderMenuItems();
 | 
					          forms.legals.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
 | 
				
			||||||
        form.elements["info.title"].value = data.info?.title || "";
 | 
					          forms.legals.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
 | 
				
			||||||
        form.elements["info.subtitle"].value = data.info?.subtitle || "";
 | 
					          forms.legals.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
 | 
				
			||||||
        form.elements["info.description"].value = data.info?.description || "";
 | 
					 | 
				
			||||||
        form.elements["info.canonical"].value = data.info?.canonical || "";
 | 
					 | 
				
			||||||
        form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
 | 
					 | 
				
			||||||
        form.elements["info.author"].value = data.info?.author || "";
 | 
					 | 
				
			||||||
        form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
 | 
					 | 
				
			||||||
        if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
 | 
					 | 
				
			||||||
        updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}?t=${Date.now()}` : "");
 | 
					 | 
				
			||||||
        form.elements["footer.copyright"].value = data.footer?.copyright || "";
 | 
					 | 
				
			||||||
        form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
 | 
					 | 
				
			||||||
        if (themeSelect) {
 | 
					 | 
				
			||||||
          themeSelect.value = data.build?.theme || "";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
 | 
					 | 
				
			||||||
        form.elements["legals.hoster_address"].value = data.legals?.hoster_address || "";
 | 
					 | 
				
			||||||
        form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
 | 
					 | 
				
			||||||
        // Build checkboxes
 | 
					 | 
				
			||||||
        if (convertImagesCheckbox) {
 | 
					 | 
				
			||||||
          convertImagesCheckbox.checked = !!data.build?.convert_images;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (resizeImagesCheckbox) {
 | 
					 | 
				
			||||||
          resizeImagesCheckbox.checked = !!data.build?.resize_images;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        // Build
 | 
				
			||||||
 | 
					        if (themeSelect) themeSelect.value = data.build?.theme || "";
 | 
				
			||||||
 | 
					        if (convertImagesCheckbox) convertImagesCheckbox.checked = !!data.build?.convert_images;
 | 
				
			||||||
 | 
					        if (resizeImagesCheckbox) resizeImagesCheckbox.checked = !!data.build?.resize_images;
 | 
				
			||||||
 | 
					        ["info", "social", "menu", "footer", "legals"].forEach(updateSectionStatus);
 | 
				
			||||||
 | 
					        updateSectionStatus("build");
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (themeSelect) refreshThemes();
 | 
				
			||||||
 | 
					  else loadConfigAndUpdateBuildStatus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Add menu item
 | 
					  // --- Add/remove menu items ---
 | 
				
			||||||
  if (addMenuBtn) {
 | 
					  if (addMenuBtn) addMenuBtn.addEventListener("click", () => {
 | 
				
			||||||
    addMenuBtn.addEventListener("click", () => {
 | 
					 | 
				
			||||||
    menuItems.push({ label: "", href: "" });
 | 
					    menuItems.push({ label: "", href: "" });
 | 
				
			||||||
    renderMenuItems();
 | 
					    renderMenuItems();
 | 
				
			||||||
 | 
					    updateSectionStatus("menu");
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Remove menu item
 | 
					 | 
				
			||||||
  menuList.addEventListener("click", (e) => {
 | 
					  menuList.addEventListener("click", (e) => {
 | 
				
			||||||
    if (e.target.classList.contains("remove-menu-item")) {
 | 
					    if (e.target.classList.contains("remove-menu-item")) {
 | 
				
			||||||
      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
					      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
				
			||||||
      menuItems.splice(idx, 1);
 | 
					      menuItems.splice(idx, 1);
 | 
				
			||||||
      renderMenuItems();
 | 
					      renderMenuItems();
 | 
				
			||||||
 | 
					      updateSectionStatus("menu");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Update menuItems on input change
 | 
					 | 
				
			||||||
  menuList.addEventListener("input", () => {
 | 
					  menuList.addEventListener("input", () => {
 | 
				
			||||||
    updateMenuItemsFromInputs();
 | 
					    updateMenuItemsFromInputs();
 | 
				
			||||||
 | 
					    updateSectionStatus("menu");
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Add paragraph
 | 
					  // --- Add/remove IP paragraphs ---
 | 
				
			||||||
  if (addIpBtn) {
 | 
					  if (addIpBtn) addIpBtn.addEventListener("click", () => {
 | 
				
			||||||
    addIpBtn.addEventListener("click", () => {
 | 
					 | 
				
			||||||
    ipParagraphs.push({ paragraph: "" });
 | 
					    ipParagraphs.push({ paragraph: "" });
 | 
				
			||||||
    renderIpParagraphs();
 | 
					    renderIpParagraphs();
 | 
				
			||||||
 | 
					    updateSectionStatus("legals");
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Remove paragraph
 | 
					 | 
				
			||||||
  ipList.addEventListener("click", (e) => {
 | 
					  ipList.addEventListener("click", (e) => {
 | 
				
			||||||
    if (e.target.classList.contains("remove-ip-paragraph")) {
 | 
					    if (e.target.classList.contains("remove-ip-paragraph")) {
 | 
				
			||||||
      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
					      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
				
			||||||
      ipParagraphs.splice(idx, 1);
 | 
					      ipParagraphs.splice(idx, 1);
 | 
				
			||||||
      renderIpParagraphs();
 | 
					      renderIpParagraphs();
 | 
				
			||||||
 | 
					      updateSectionStatus("legals");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Update ipParagraphs on input change
 | 
					 | 
				
			||||||
  ipList.addEventListener("input", () => {
 | 
					  ipList.addEventListener("input", () => {
 | 
				
			||||||
    updateIpParagraphsFromInputs();
 | 
					    updateIpParagraphsFromInputs();
 | 
				
			||||||
 | 
					    updateSectionStatus("legals");
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Save config to server
 | 
					  // --- Section value helpers ---
 | 
				
			||||||
  if (form) {
 | 
					  function getSectionValues(section) {
 | 
				
			||||||
    form.addEventListener("submit", async (e) => {
 | 
					    switch (section) {
 | 
				
			||||||
      e.preventDefault();
 | 
					      case "info":
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          title: forms.info.elements["info.title"].value,
 | 
				
			||||||
 | 
					          subtitle: forms.info.elements["info.subtitle"].value,
 | 
				
			||||||
 | 
					          description: forms.info.elements["info.description"].value,
 | 
				
			||||||
 | 
					          canonical: forms.info.elements["info.canonical"].value,
 | 
				
			||||||
 | 
					          keywords: forms.info.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
 | 
				
			||||||
 | 
					          author: forms.info.elements["info.author"].value
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      case "social":
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          instagram_url: forms.social.elements["social.instagram_url"].value,
 | 
				
			||||||
 | 
					          thumbnail: thumbnailInput ? thumbnailInput.value : ""
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      case "menu":
 | 
				
			||||||
        updateMenuItemsFromInputs();
 | 
					        updateMenuItemsFromInputs();
 | 
				
			||||||
 | 
					        return { items: menuItems };
 | 
				
			||||||
 | 
					      case "footer":
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          copyright: forms.footer.elements["footer.copyright"].value,
 | 
				
			||||||
 | 
					          legal_label: forms.footer.elements["footer.legal_label"].value
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      case "legals":
 | 
				
			||||||
        updateIpParagraphsFromInputs();
 | 
					        updateIpParagraphsFromInputs();
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
      // Check if thumbnail is set before saving (uploaded or present in input)
 | 
					          hoster_name: forms.legals.elements["legals.hoster_name"].value,
 | 
				
			||||||
      if (!thumbnailInput || !thumbnailInput.value) {
 | 
					          hoster_address: forms.legals.elements["legals.hoster_address"].value,
 | 
				
			||||||
        showToast("❌ Thumbnail is required.", "error");
 | 
					          hoster_contact: forms.legals.elements["legals.hoster_contact"].value,
 | 
				
			||||||
        return;
 | 
					          intellectual_property: ipParagraphs
 | 
				
			||||||
      }
 | 
					        };
 | 
				
			||||||
 | 
					      case "build":
 | 
				
			||||||
      const build = {
 | 
					        return {
 | 
				
			||||||
          theme: themeSelect ? themeSelect.value : "",
 | 
					          theme: themeSelect ? themeSelect.value : "",
 | 
				
			||||||
          convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
 | 
					          convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
 | 
				
			||||||
          resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
 | 
					          resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
      const payload = {
 | 
					        return {};
 | 
				
			||||||
        info: {
 | 
					 | 
				
			||||||
          title: form.elements["info.title"].value,
 | 
					 | 
				
			||||||
          subtitle: form.elements["info.subtitle"].value,
 | 
					 | 
				
			||||||
          description: form.elements["info.description"].value,
 | 
					 | 
				
			||||||
          canonical: form.elements["info.canonical"].value,
 | 
					 | 
				
			||||||
          keywords: form.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
 | 
					 | 
				
			||||||
          author: form.elements["info.author"].value
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        social: {
 | 
					 | 
				
			||||||
          instagram_url: form.elements["social.instagram_url"].value,
 | 
					 | 
				
			||||||
          thumbnail: thumbnailInput ? thumbnailInput.value : ""
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        menu: {
 | 
					 | 
				
			||||||
          items: menuItems
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        footer: {
 | 
					 | 
				
			||||||
          copyright: form.elements["footer.copyright"].value,
 | 
					 | 
				
			||||||
          legal_label: form.elements["footer.legal_label"].value
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        build,
 | 
					 | 
				
			||||||
        legals: {
 | 
					 | 
				
			||||||
          hoster_name: form.elements["legals.hoster_name"].value,
 | 
					 | 
				
			||||||
          hoster_address: form.elements["legals.hoster_address"].value,
 | 
					 | 
				
			||||||
          hoster_contact: form.elements["legals.hoster_contact"].value,
 | 
					 | 
				
			||||||
          intellectual_property: ipParagraphs
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      };
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function isSectionSaved(section) {
 | 
				
			||||||
 | 
					    const values = getSectionValues(section);
 | 
				
			||||||
 | 
					    const config = loadedConfig[section] || {};
 | 
				
			||||||
 | 
					    function normalizeMenuItems(items) {
 | 
				
			||||||
 | 
					      return (items || []).map(item => ({
 | 
				
			||||||
 | 
					        label: item.label || "",
 | 
				
			||||||
 | 
					        href: item.href || ""
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    switch (section) {
 | 
				
			||||||
 | 
					      case "info":
 | 
				
			||||||
 | 
					        return Object.keys(values).every(
 | 
				
			||||||
 | 
					          key => values[key] && (
 | 
				
			||||||
 | 
					            key === "keywords"
 | 
				
			||||||
 | 
					              ? Array.isArray(config.keywords) && values.keywords.join(",") === config.keywords.join(",")
 | 
				
			||||||
 | 
					              : values[key] === (config[key] || "")
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case "social":
 | 
				
			||||||
 | 
					        return values.instagram_url && values.thumbnail &&
 | 
				
			||||||
 | 
					          values.instagram_url === (config.instagram_url || "") &&
 | 
				
			||||||
 | 
					          values.thumbnail === (config.thumbnail || "");
 | 
				
			||||||
 | 
					      case "menu":
 | 
				
			||||||
 | 
					        return JSON.stringify(normalizeMenuItems(values.items)) === JSON.stringify(normalizeMenuItems(config.items));
 | 
				
			||||||
 | 
					      case "footer":
 | 
				
			||||||
 | 
					        return values.copyright && values.legal_label &&
 | 
				
			||||||
 | 
					          values.copyright === (config.copyright || "") &&
 | 
				
			||||||
 | 
					          values.legal_label === (config.legal_label || "");
 | 
				
			||||||
 | 
					      case "legals":
 | 
				
			||||||
 | 
					        return values.hoster_name && values.hoster_address && values.hoster_contact &&
 | 
				
			||||||
 | 
					          values.hoster_name === (config.hoster_name || "") &&
 | 
				
			||||||
 | 
					          values.hoster_address === (config.hoster_address || "") &&
 | 
				
			||||||
 | 
					          values.hoster_contact === (config.hoster_contact || "") &&
 | 
				
			||||||
 | 
					          JSON.stringify(values.intellectual_property) === JSON.stringify(config.intellectual_property || []);
 | 
				
			||||||
 | 
					      case "build":
 | 
				
			||||||
 | 
					        return values.theme === (config.theme || "") &&
 | 
				
			||||||
 | 
					          !!values.convert_images === !!config.convert_images &&
 | 
				
			||||||
 | 
					          !!values.resize_images === !!config.resize_images;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function isSectionComplete(section) {
 | 
				
			||||||
 | 
					    const values = getSectionValues(section);
 | 
				
			||||||
 | 
					    switch (section) {
 | 
				
			||||||
 | 
					      case "info":
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          values.title &&
 | 
				
			||||||
 | 
					          values.subtitle &&
 | 
				
			||||||
 | 
					          values.description &&
 | 
				
			||||||
 | 
					          values.canonical &&
 | 
				
			||||||
 | 
					          values.keywords.length > 0 &&
 | 
				
			||||||
 | 
					          values.author
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case "social":
 | 
				
			||||||
 | 
					        return values.instagram_url && values.thumbnail;
 | 
				
			||||||
 | 
					      case "menu":
 | 
				
			||||||
 | 
					        return Array.isArray(values.items) && values.items.every(item => item.label && item.href);
 | 
				
			||||||
 | 
					      case "footer":
 | 
				
			||||||
 | 
					        return values.copyright && values.legal_label;
 | 
				
			||||||
 | 
					      case "legals":
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          values.hoster_name &&
 | 
				
			||||||
 | 
					          values.hoster_address &&
 | 
				
			||||||
 | 
					          values.hoster_contact &&
 | 
				
			||||||
 | 
					          Array.isArray(values.intellectual_property) &&
 | 
				
			||||||
 | 
					          values.intellectual_property.length > 0 &&
 | 
				
			||||||
 | 
					          values.intellectual_property.every(ip => ip.paragraph)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case "build":
 | 
				
			||||||
 | 
					        return !!values.theme;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function updateSectionStatus(section) {
 | 
				
			||||||
 | 
					    const statusEl = document.querySelector(`#${section}-section .section-status`);
 | 
				
			||||||
 | 
					    if (!statusEl) return;
 | 
				
			||||||
 | 
					    if (!isSectionComplete(section)) {
 | 
				
			||||||
 | 
					      statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields";
 | 
				
			||||||
 | 
					      statusEl.style.color = "#ffc700";
 | 
				
			||||||
 | 
					      statusEl.style.display = "";
 | 
				
			||||||
 | 
					      statusEl.style.fontStyle = "normal";
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (isSectionSaved(section)) {
 | 
				
			||||||
 | 
					      statusEl.innerHTML = "";
 | 
				
			||||||
 | 
					      statusEl.style.display = "none";
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      statusEl.innerHTML = "⚠️ Section not yet saved";
 | 
				
			||||||
 | 
					      statusEl.style.color = "#ffc700";
 | 
				
			||||||
 | 
					      statusEl.style.display = "";
 | 
				
			||||||
 | 
					      statusEl.style.fontStyle = "normal";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --- Listen for changes in each section ---
 | 
				
			||||||
 | 
					  Object.entries(forms).forEach(([section, form]) => {
 | 
				
			||||||
 | 
					    if (!form) return;
 | 
				
			||||||
 | 
					    form.addEventListener("input", () => updateSectionStatus(section));
 | 
				
			||||||
 | 
					    form.addEventListener("change", () => updateSectionStatus(section));
 | 
				
			||||||
 | 
					    form.addEventListener("submit", async (e) => {
 | 
				
			||||||
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					      if (!form.reportValidity()) {
 | 
				
			||||||
 | 
					        showToast("❌ Please fill all required fields before saving.", "error");
 | 
				
			||||||
 | 
					        updateSectionStatus(section);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (section === "social" && (!thumbnailInput || !thumbnailInput.value)) {
 | 
				
			||||||
 | 
					        showToast("❌ Thumbnail is required.", "error");
 | 
				
			||||||
 | 
					        updateSectionStatus(section);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (section === "menu") {
 | 
				
			||||||
 | 
					        updateMenuItemsFromInputs();
 | 
				
			||||||
 | 
					        if (!menuItems.length || !menuItems.every(item => item.label && item.href)) {
 | 
				
			||||||
 | 
					          showToast("❌ Please fill all menu item fields.", "error");
 | 
				
			||||||
 | 
					          updateSectionStatus(section);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (section === "legals") {
 | 
				
			||||||
 | 
					        updateIpParagraphsFromInputs();
 | 
				
			||||||
 | 
					        if (!ipParagraphs.length || !ipParagraphs.every(ip => ip.paragraph)) {
 | 
				
			||||||
 | 
					          showToast("❌ Please fill all intellectual property paragraphs.", "error");
 | 
				
			||||||
 | 
					          updateSectionStatus(section);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      let payload = {};
 | 
				
			||||||
 | 
					      payload[section] = getSectionValues(section);
 | 
				
			||||||
      const res = await fetch("/api/site-info", {
 | 
					      const res = await fetch("/api/site-info", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
@@ -424,10 +519,16 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Site info saved!", "success");
 | 
					        showToast("✅ Section saved!", "success");
 | 
				
			||||||
 | 
					        fetch("/api/site-info")
 | 
				
			||||||
 | 
					          .then(res => res.json())
 | 
				
			||||||
 | 
					          .then(data => {
 | 
				
			||||||
 | 
					            loadedConfig = data;
 | 
				
			||||||
 | 
					            updateSectionStatus(section);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        showToast("❌ Error saving site info", "error");
 | 
					        showToast("❌ Error saving section", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -31,6 +31,20 @@ function showToast(message, type = "success", duration = 3000) {
 | 
				
			|||||||
  }, duration);
 | 
					  }, duration);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Loader helpers ---
 | 
				
			||||||
 | 
					function showLoader(text = "Uploading...") {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) {
 | 
				
			||||||
 | 
					    loader.classList.add("active");
 | 
				
			||||||
 | 
					    document.getElementById("loader-text").textContent = text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function hideLoader() {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) loader.classList.remove("active");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Color Picker
 | 
				
			||||||
function setupColorPicker(colorId, btnId, textId, initial) {
 | 
					function setupColorPicker(colorId, btnId, textId, initial) {
 | 
				
			||||||
  const colorInput = document.getElementById(colorId);
 | 
					  const colorInput = document.getElementById(colorId);
 | 
				
			||||||
  const colorBtn = document.getElementById(btnId);
 | 
					  const colorBtn = document.getElementById(btnId);
 | 
				
			||||||
@@ -40,7 +54,6 @@ function setupColorPicker(colorId, btnId, textId, initial) {
 | 
				
			|||||||
  colorBtn.style.background = initial;
 | 
					  colorBtn.style.background = initial;
 | 
				
			||||||
  textInput.value = initial.toUpperCase();
 | 
					  textInput.value = initial.toUpperCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Color input is positioned over the button and is clickable
 | 
					 | 
				
			||||||
  colorInput.addEventListener("input", () => {
 | 
					  colorInput.addEventListener("input", () => {
 | 
				
			||||||
    colorBtn.style.background = colorInput.value;
 | 
					    colorBtn.style.background = colorInput.value;
 | 
				
			||||||
    textInput.value = colorInput.value.toUpperCase();
 | 
					    textInput.value = colorInput.value.toUpperCase();
 | 
				
			||||||
@@ -57,9 +70,11 @@ function setupColorPicker(colorId, btnId, textId, initial) {
 | 
				
			|||||||
function setFontDropdown(selectId, value, options) {
 | 
					function setFontDropdown(selectId, value, options) {
 | 
				
			||||||
  const select = document.getElementById(selectId);
 | 
					  const select = document.getElementById(selectId);
 | 
				
			||||||
  if (!select) return;
 | 
					  if (!select) return;
 | 
				
			||||||
  select.innerHTML = options.map(opt =>
 | 
					  select.innerHTML = options.map(opt => {
 | 
				
			||||||
    `<option value="${opt}"${opt === value ? " selected" : ""}>${opt}</option>`
 | 
					    // Remove extension if present
 | 
				
			||||||
  ).join("");
 | 
					    const base = opt.replace(/\.(woff2?|ttf|otf)$/, "");
 | 
				
			||||||
 | 
					    return `<option value="${base}"${base === value ? " selected" : ""}>${base}</option>`;
 | 
				
			||||||
 | 
					  }).join("");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setFallbackDropdown(selectId, value) {
 | 
					function setFallbackDropdown(selectId, value) {
 | 
				
			||||||
@@ -103,54 +118,173 @@ function renderLocalFonts(fonts) {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Section helpers ---
 | 
				
			||||||
 | 
					function getSectionValues(section) {
 | 
				
			||||||
 | 
					  switch (section) {
 | 
				
			||||||
 | 
					    case "colors":
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        primary: document.getElementById("color-primary-text").value,
 | 
				
			||||||
 | 
					        primary_dark: document.getElementById("color-primary-dark-text").value,
 | 
				
			||||||
 | 
					        secondary: document.getElementById("color-secondary-text").value,
 | 
				
			||||||
 | 
					        accent: document.getElementById("color-accent-text").value,
 | 
				
			||||||
 | 
					        text_dark: document.getElementById("color-text-dark-text").value,
 | 
				
			||||||
 | 
					        background: document.getElementById("color-background-text").value,
 | 
				
			||||||
 | 
					        browser_color: document.getElementById("color-browser-color-text").value
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    case "google-fonts":
 | 
				
			||||||
 | 
					      const googleFontsFields = document.getElementById("google-fonts-fields");
 | 
				
			||||||
 | 
					      const fonts = [];
 | 
				
			||||||
 | 
					      if (googleFontsFields) {
 | 
				
			||||||
 | 
					        googleFontsFields.querySelectorAll(".input-field").forEach(field => {
 | 
				
			||||||
 | 
					          const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value.trim();
 | 
				
			||||||
 | 
					          const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
 | 
				
			||||||
 | 
					            .split(",").map(w => w.trim()).filter(Boolean);
 | 
				
			||||||
 | 
					          if (family) fonts.push({ family, weights });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return fonts;
 | 
				
			||||||
 | 
					    case "fonts":
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        primary: {
 | 
				
			||||||
 | 
					          name: document.getElementById("font-primary").value,
 | 
				
			||||||
 | 
					          fallback: document.getElementById("font-primary-fallback").value
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        secondary: {
 | 
				
			||||||
 | 
					          name: document.getElementById("font-secondary").value,
 | 
				
			||||||
 | 
					          fallback: document.getElementById("font-secondary-fallback").value
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    case "favicon":
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        path: document.getElementById("favicon-path").value
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isSectionComplete(section) {
 | 
				
			||||||
 | 
					  switch (section) {
 | 
				
			||||||
 | 
					    case "colors":
 | 
				
			||||||
 | 
					      const v = getSectionValues("colors");
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        v.primary &&
 | 
				
			||||||
 | 
					        v.primary_dark &&
 | 
				
			||||||
 | 
					        v.secondary &&
 | 
				
			||||||
 | 
					        v.accent &&
 | 
				
			||||||
 | 
					        v.text_dark &&
 | 
				
			||||||
 | 
					        v.background &&
 | 
				
			||||||
 | 
					        v.browser_color
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    case "google-fonts":
 | 
				
			||||||
 | 
					      const fonts = getSectionValues("google-fonts");
 | 
				
			||||||
 | 
					      return fonts.every(f => f.family);
 | 
				
			||||||
 | 
					    case "fonts":
 | 
				
			||||||
 | 
					      const f = getSectionValues("fonts");
 | 
				
			||||||
 | 
					      return f.primary.name && f.primary.fallback && f.secondary.name && f.secondary.fallback;
 | 
				
			||||||
 | 
					    case "favicon":
 | 
				
			||||||
 | 
					      const fav = getSectionValues("favicon");
 | 
				
			||||||
 | 
					      return !!fav.path;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isSectionSaved(section, loadedConfig) {
 | 
				
			||||||
 | 
					  switch (section) {
 | 
				
			||||||
 | 
					    case "colors":
 | 
				
			||||||
 | 
					      const v = getSectionValues("colors");
 | 
				
			||||||
 | 
					      const c = loadedConfig.colors || {};
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        v.primary === c.primary &&
 | 
				
			||||||
 | 
					        v.primary_dark === c.primary_dark &&
 | 
				
			||||||
 | 
					        v.secondary === c.secondary &&
 | 
				
			||||||
 | 
					        v.accent === c.accent &&
 | 
				
			||||||
 | 
					        v.text_dark === c.text_dark &&
 | 
				
			||||||
 | 
					        v.background === c.background &&
 | 
				
			||||||
 | 
					        v.browser_color === c.browser_color
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    case "google-fonts":
 | 
				
			||||||
 | 
					      const fonts = getSectionValues("google-fonts");
 | 
				
			||||||
 | 
					      const cf = loadedConfig.google_fonts || [];
 | 
				
			||||||
 | 
					      return JSON.stringify(fonts) === JSON.stringify(cf);
 | 
				
			||||||
 | 
					    case "fonts":
 | 
				
			||||||
 | 
					      const f = getSectionValues("fonts");
 | 
				
			||||||
 | 
					      const cfnt = loadedConfig.fonts || {};
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        f.primary.name === (cfnt.primary?.name || "") &&
 | 
				
			||||||
 | 
					        f.primary.fallback === (cfnt.primary?.fallback || "") &&
 | 
				
			||||||
 | 
					        f.secondary.name === (cfnt.secondary?.name || "") &&
 | 
				
			||||||
 | 
					        f.secondary.fallback === (cfnt.secondary?.fallback || "")
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    case "favicon":
 | 
				
			||||||
 | 
					      const fav = getSectionValues("favicon");
 | 
				
			||||||
 | 
					      const cfav = loadedConfig.favicon || {};
 | 
				
			||||||
 | 
					      return fav.path === (cfav.path || "");
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function updateSectionStatus(section, loadedConfig) {
 | 
				
			||||||
 | 
					  const statusEl = document.querySelector(`#${section}-form .section-status`);
 | 
				
			||||||
 | 
					  if (!statusEl) return;
 | 
				
			||||||
 | 
					  if (!isSectionComplete(section)) {
 | 
				
			||||||
 | 
					    statusEl.innerHTML = "⚠️ Section not yet saved. Please fill required fields";
 | 
				
			||||||
 | 
					    statusEl.style.color = "#ffc700";
 | 
				
			||||||
 | 
					    statusEl.style.display = "";
 | 
				
			||||||
 | 
					    statusEl.style.fontStyle = "normal";
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (isSectionSaved(section, loadedConfig)) {
 | 
				
			||||||
 | 
					    statusEl.innerHTML = "";
 | 
				
			||||||
 | 
					    statusEl.style.display = "none";
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    statusEl.innerHTML = "⚠️ Section not yet saved";
 | 
				
			||||||
 | 
					    statusEl.style.color = "#ffc700";
 | 
				
			||||||
 | 
					    statusEl.style.display = "";
 | 
				
			||||||
 | 
					    statusEl.style.fontStyle = "normal";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
document.addEventListener("DOMContentLoaded", async () => {
 | 
					document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			||||||
  const themeInfo = await fetchThemeInfo();
 | 
					  const themeInfo = await fetchThemeInfo();
 | 
				
			||||||
  const themeNameSpan = document.getElementById("current-theme");
 | 
					  const themeNameSpan = document.getElementById("current-theme");
 | 
				
			||||||
  if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
 | 
					  if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const themeYaml = themeInfo.theme_yaml;
 | 
					  let loadedConfig = themeInfo.theme_yaml;
 | 
				
			||||||
  const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : [];
 | 
					  let googleFonts = loadedConfig.google_fonts ? JSON.parse(JSON.stringify(loadedConfig.google_fonts)) : [];
 | 
				
			||||||
  let localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
					  let localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Colors
 | 
					  // Colors
 | 
				
			||||||
  if (themeYaml.colors) {
 | 
					  if (loadedConfig.colors) {
 | 
				
			||||||
    setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", themeYaml.colors.primary || "#0065a1");
 | 
					    setupColorPicker("color-primary", "color-primary-btn", "color-primary-text", loadedConfig.colors.primary || "#0065a1");
 | 
				
			||||||
    setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384");
 | 
					    setupColorPicker("color-primary-dark", "color-primary-dark-btn", "color-primary-dark-text", loadedConfig.colors.primary_dark || "#005384");
 | 
				
			||||||
    setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0");
 | 
					    setupColorPicker("color-secondary", "color-secondary-btn", "color-secondary-text", loadedConfig.colors.secondary || "#00b0f0");
 | 
				
			||||||
    setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", themeYaml.colors.accent || "#ffc700");
 | 
					    setupColorPicker("color-accent", "color-accent-btn", "color-accent-text", loadedConfig.colors.accent || "#ffc700");
 | 
				
			||||||
    setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", themeYaml.colors.text_dark || "#616161");
 | 
					    setupColorPicker("color-text-dark", "color-text-dark-btn", "color-text-dark-text", loadedConfig.colors.text_dark || "#616161");
 | 
				
			||||||
    setupColorPicker("color-background", "color-background-btn", "color-background-text", themeYaml.colors.background || "#fff");
 | 
					    setupColorPicker("color-background", "color-background-btn", "color-background-text", loadedConfig.colors.background || "#fff");
 | 
				
			||||||
    setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", themeYaml.colors.browser_color || "#fff");
 | 
					    setupColorPicker("color-browser-color", "color-browser-color-btn", "color-browser-color-text", loadedConfig.colors.browser_color || "#fff");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Fonts
 | 
					  // Fonts
 | 
				
			||||||
  function refreshFontDropdowns() {
 | 
					  function refreshFontDropdowns() {
 | 
				
			||||||
    setFontDropdown("font-primary", document.getElementById("font-primary").value, [
 | 
					    setFontDropdown("font-primary", loadedConfig.fonts?.primary?.name || "Lato", [
 | 
				
			||||||
      ...googleFonts.map(f => f.family),
 | 
					      ...googleFonts.map(f => f.family),
 | 
				
			||||||
      ...localFonts
 | 
					      ...localFonts
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
    setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [
 | 
					    setFontDropdown("font-secondary", loadedConfig.fonts?.secondary?.name || "Montserrat", [
 | 
				
			||||||
      ...googleFonts.map(f => f.family),
 | 
					      ...googleFonts.map(f => f.family),
 | 
				
			||||||
      ...localFonts
 | 
					      ...localFonts
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					    setFallbackDropdown("font-primary-fallback", loadedConfig.fonts?.primary?.fallback || "sans-serif");
 | 
				
			||||||
 | 
					    setFallbackDropdown("font-secondary-fallback", loadedConfig.fonts?.secondary?.fallback || "serif");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (themeYaml.fonts) {
 | 
					  refreshFontDropdowns();
 | 
				
			||||||
    setFontDropdown("font-primary", themeYaml.fonts.primary?.name || "Lato", [
 | 
					 | 
				
			||||||
      ...googleFonts.map(f => f.family),
 | 
					 | 
				
			||||||
      ...localFonts
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
    setFallbackDropdown("font-primary-fallback", themeYaml.fonts.primary?.fallback || "sans-serif");
 | 
					 | 
				
			||||||
    setFontDropdown("font-secondary", themeYaml.fonts.secondary?.name || "Montserrat", [
 | 
					 | 
				
			||||||
      ...googleFonts.map(f => f.family),
 | 
					 | 
				
			||||||
      ...localFonts
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
    setFallbackDropdown("font-secondary-fallback", themeYaml.fonts.secondary?.fallback || "serif");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Font upload logic
 | 
					  // Font upload logic
 | 
				
			||||||
  const fontUploadInput = document.getElementById("font-upload");
 | 
					  const fontUploadInput = document.getElementById("font-upload");
 | 
				
			||||||
  const chooseFontBtn = document.getElementById("choose-font-btn");
 | 
					  const chooseFontBtn = document.getElementById("choose-font-btn");
 | 
				
			||||||
  const fontUploadStatus = document.getElementById("font-upload-status");
 | 
					 | 
				
			||||||
  const localFontsList = document.getElementById("local-fonts-list");
 | 
					  const localFontsList = document.getElementById("local-fonts-list");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Modal logic for font deletion
 | 
					  // Modal logic for font deletion
 | 
				
			||||||
@@ -178,11 +312,13 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
        showToast("Only .woff and .woff2 fonts are allowed.", "error");
 | 
					        showToast("Only .woff and .woff2 fonts are allowed.", "error");
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      showLoader("Uploading font...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      formData.append("file", file);
 | 
					      formData.append("file", file);
 | 
				
			||||||
      formData.append("theme", themeInfo.theme_name);
 | 
					      formData.append("theme", themeInfo.theme_name);
 | 
				
			||||||
      const res = await fetch("/api/font/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/font/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("✅ Font uploaded!", "success");
 | 
					        showToast("✅ Font uploaded!", "success");
 | 
				
			||||||
        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
					        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
@@ -219,9 +355,11 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
    deleteFontModalConfirm.onclick = async () => {
 | 
					    deleteFontModalConfirm.onclick = async () => {
 | 
				
			||||||
      if (!fontToDelete) return;
 | 
					      if (!fontToDelete) return;
 | 
				
			||||||
 | 
					      showLoader("Removing font...");
 | 
				
			||||||
      const result = await removeFont(themeInfo.theme_name, fontToDelete);
 | 
					      const result = await removeFont(themeInfo.theme_name, fontToDelete);
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        showToast("Font removed!", "✅ success");
 | 
					        showToast("Font removed!", "success");
 | 
				
			||||||
        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
					        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
				
			||||||
        refreshLocalFonts();
 | 
					        refreshLocalFonts();
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
@@ -272,11 +410,13 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
        showToast("Invalid file type for favicon.", "error");
 | 
					        showToast("Invalid file type for favicon.", "error");
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      showLoader("Uploading favicon...");
 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      formData.append("file", file);
 | 
					      formData.append("file", file);
 | 
				
			||||||
      formData.append("theme", themeInfo.theme_name);
 | 
					      formData.append("theme", themeInfo.theme_name);
 | 
				
			||||||
      const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
 | 
					      const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        faviconInput.value = result.filename;
 | 
					        faviconInput.value = result.filename;
 | 
				
			||||||
        updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
 | 
					        updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
 | 
				
			||||||
@@ -303,12 +443,14 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    deleteFaviconModalConfirm.onclick = async () => {
 | 
					    deleteFaviconModalConfirm.onclick = async () => {
 | 
				
			||||||
 | 
					      showLoader("Removing favicon...");
 | 
				
			||||||
      const res = await fetch("/api/favicon/remove", {
 | 
					      const res = await fetch("/api/favicon/remove", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
        body: JSON.stringify({ theme: themeInfo.theme_name })
 | 
					        body: JSON.stringify({ theme: themeInfo.theme_name })
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const result = await res.json();
 | 
					      const result = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (result.status === "ok") {
 | 
					      if (result.status === "ok") {
 | 
				
			||||||
        faviconInput.value = "";
 | 
					        faviconInput.value = "";
 | 
				
			||||||
        updateFaviconPreview("");
 | 
					        updateFaviconPreview("");
 | 
				
			||||||
@@ -320,9 +462,9 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (themeYaml.favicon && themeYaml.favicon.path) {
 | 
					  if (loadedConfig.favicon && loadedConfig.favicon.path) {
 | 
				
			||||||
    faviconInput.value = themeYaml.favicon.path;
 | 
					    faviconInput.value = loadedConfig.favicon.path;
 | 
				
			||||||
    updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`);
 | 
					    updateFaviconPreview(`/themes/${themeInfo.theme_name}/${loadedConfig.favicon.path}?t=${Date.now()}`);
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    updateFaviconPreview("");
 | 
					    updateFaviconPreview("");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -335,31 +477,28 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
  if (addGoogleFontBtn) {
 | 
					  if (addGoogleFontBtn) {
 | 
				
			||||||
    addGoogleFontBtn.addEventListener("click", async () => {
 | 
					    addGoogleFontBtn.addEventListener("click", async () => {
 | 
				
			||||||
      googleFonts.push({ family: "", weights: [] });
 | 
					      googleFonts.push({ family: "", weights: [] });
 | 
				
			||||||
      // Save immediately to backend
 | 
					 | 
				
			||||||
      await fetch("/api/theme-google-fonts", {
 | 
					      await fetch("/api/theme-google-fonts", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
        body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
					        body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      // Fetch updated theme info and refresh dropdowns
 | 
					 | 
				
			||||||
      const updatedThemeInfo = await fetchThemeInfo();
 | 
					      const updatedThemeInfo = await fetchThemeInfo();
 | 
				
			||||||
      const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
					      const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
				
			||||||
      googleFonts.length = 0;
 | 
					      googleFonts.length = 0;
 | 
				
			||||||
      googleFonts.push(...updatedGoogleFonts);
 | 
					      googleFonts.push(...updatedGoogleFonts);
 | 
				
			||||||
      renderGoogleFonts(googleFonts);
 | 
					      renderGoogleFonts(googleFonts);
 | 
				
			||||||
      refreshFontDropdowns();
 | 
					      refreshFontDropdowns();
 | 
				
			||||||
 | 
					      updateSectionStatus("google-fonts", loadedConfig);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const googleFontsFields = document.getElementById("google-fonts-fields");
 | 
					  const googleFontsFields = document.getElementById("google-fonts-fields");
 | 
				
			||||||
  if (googleFontsFields) {
 | 
					  if (googleFontsFields) {
 | 
				
			||||||
    // Save on blur for family/weights fields
 | 
					 | 
				
			||||||
    googleFontsFields.addEventListener("blur", async (e) => {
 | 
					    googleFontsFields.addEventListener("blur", async (e) => {
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        e.target.name &&
 | 
					        e.target.name &&
 | 
				
			||||||
        (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
 | 
					        (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        // Update googleFonts array from the form fields
 | 
					 | 
				
			||||||
        const fontFields = googleFontsFields.querySelectorAll(".input-field");
 | 
					        const fontFields = googleFontsFields.querySelectorAll(".input-field");
 | 
				
			||||||
        googleFonts.length = 0;
 | 
					        googleFonts.length = 0;
 | 
				
			||||||
        fontFields.forEach(field => {
 | 
					        fontFields.forEach(field => {
 | 
				
			||||||
@@ -368,87 +507,102 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
            .split(",").map(w => w.trim()).filter(Boolean);
 | 
					            .split(",").map(w => w.trim()).filter(Boolean);
 | 
				
			||||||
          googleFonts.push({ family, weights });
 | 
					          googleFonts.push({ family, weights });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Save immediately to backend
 | 
					 | 
				
			||||||
        await fetch("/api/theme-google-fonts", {
 | 
					        await fetch("/api/theme-google-fonts", {
 | 
				
			||||||
          method: "POST",
 | 
					          method: "POST",
 | 
				
			||||||
          headers: { "Content-Type": "application/json" },
 | 
					          headers: { "Content-Type": "application/json" },
 | 
				
			||||||
          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
					          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Fetch updated theme info and refresh dropdowns
 | 
					 | 
				
			||||||
        const updatedThemeInfo = await fetchThemeInfo();
 | 
					        const updatedThemeInfo = await fetchThemeInfo();
 | 
				
			||||||
        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
					        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
				
			||||||
        googleFonts.length = 0;
 | 
					        googleFonts.length = 0;
 | 
				
			||||||
        googleFonts.push(...updatedGoogleFonts);
 | 
					        googleFonts.push(...updatedGoogleFonts);
 | 
				
			||||||
        renderGoogleFonts(googleFonts);
 | 
					        renderGoogleFonts(googleFonts);
 | 
				
			||||||
        refreshFontDropdowns();
 | 
					        refreshFontDropdowns();
 | 
				
			||||||
 | 
					        updateSectionStatus("google-fonts", loadedConfig);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }, true); // Use capture phase to catch blur from children
 | 
					    }, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Delegate remove button click for Google Fonts
 | 
					 | 
				
			||||||
    googleFontsFields.addEventListener("click", async (e) => {
 | 
					    googleFontsFields.addEventListener("click", async (e) => {
 | 
				
			||||||
      if (e.target.classList.contains("remove-google-font")) {
 | 
					      if (e.target.classList.contains("remove-google-font")) {
 | 
				
			||||||
        const idx = Number(e.target.dataset.idx);
 | 
					        const idx = Number(e.target.dataset.idx);
 | 
				
			||||||
        googleFonts.splice(idx, 1);
 | 
					        googleFonts.splice(idx, 1);
 | 
				
			||||||
        // Save immediately to backend
 | 
					 | 
				
			||||||
        await fetch("/api/theme-google-fonts", {
 | 
					        await fetch("/api/theme-google-fonts", {
 | 
				
			||||||
          method: "POST",
 | 
					          method: "POST",
 | 
				
			||||||
          headers: { "Content-Type": "application/json" },
 | 
					          headers: { "Content-Type": "application/json" },
 | 
				
			||||||
          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
					          body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        // Fetch updated theme info and refresh dropdowns
 | 
					 | 
				
			||||||
        const updatedThemeInfo = await fetchThemeInfo();
 | 
					        const updatedThemeInfo = await fetchThemeInfo();
 | 
				
			||||||
        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
					        const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
 | 
				
			||||||
        googleFonts.length = 0;
 | 
					        googleFonts.length = 0;
 | 
				
			||||||
        googleFonts.push(...updatedGoogleFonts);
 | 
					        googleFonts.push(...updatedGoogleFonts);
 | 
				
			||||||
        renderGoogleFonts(googleFonts);
 | 
					        renderGoogleFonts(googleFonts);
 | 
				
			||||||
        refreshFontDropdowns();
 | 
					        refreshFontDropdowns();
 | 
				
			||||||
 | 
					        updateSectionStatus("google-fonts", loadedConfig);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Form submit
 | 
					  // --- Section status listeners ---
 | 
				
			||||||
  document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
 | 
					  [
 | 
				
			||||||
 | 
					    { form: document.getElementById("colors-form"), section: "colors" },
 | 
				
			||||||
 | 
					    { form: document.getElementById("google-fonts-form"), section: "google-fonts" },
 | 
				
			||||||
 | 
					    { form: document.getElementById("fonts-form"), section: "fonts" },
 | 
				
			||||||
 | 
					    { form: document.getElementById("favicon-form"), section: "favicon" }
 | 
				
			||||||
 | 
					  ].forEach(({ form, section }) => {
 | 
				
			||||||
 | 
					    if (!form) return;
 | 
				
			||||||
 | 
					    form.addEventListener("input", () => updateSectionStatus(section, loadedConfig));
 | 
				
			||||||
 | 
					    form.addEventListener("change", () => updateSectionStatus(section, loadedConfig));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // --- Section save handlers ---
 | 
				
			||||||
 | 
					  [
 | 
				
			||||||
 | 
					    { form: document.getElementById("colors-form"), section: "colors" },
 | 
				
			||||||
 | 
					    { form: document.getElementById("google-fonts-form"), section: "google-fonts" },
 | 
				
			||||||
 | 
					    { form: document.getElementById("fonts-form"), section: "fonts" },
 | 
				
			||||||
 | 
					    { form: document.getElementById("favicon-form"), section: "favicon" }
 | 
				
			||||||
 | 
					  ].forEach(({ form, section }) => {
 | 
				
			||||||
 | 
					    if (!form) return;
 | 
				
			||||||
 | 
					    form.addEventListener("submit", async (e) => {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
    const data = {};
 | 
					      if (!form.reportValidity() || !isSectionComplete(section)) {
 | 
				
			||||||
    data.colors = {
 | 
					        showToast("❌ Please fill all required fields before saving.", "error");
 | 
				
			||||||
      primary: document.getElementById("color-primary-text").value,
 | 
					        updateSectionStatus(section, loadedConfig);
 | 
				
			||||||
      primary_dark: document.getElementById("color-primary-dark-text").value,
 | 
					        return;
 | 
				
			||||||
      secondary: document.getElementById("color-secondary-text").value,
 | 
					 | 
				
			||||||
      accent: document.getElementById("color-accent-text").value,
 | 
					 | 
				
			||||||
      text_dark: document.getElementById("color-text-dark-text").value,
 | 
					 | 
				
			||||||
      background: document.getElementById("color-background-text").value,
 | 
					 | 
				
			||||||
      browser_color: document.getElementById("color-browser-color-text").value
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    data.fonts = {
 | 
					 | 
				
			||||||
      primary: {
 | 
					 | 
				
			||||||
        name: document.getElementById("font-primary").value,
 | 
					 | 
				
			||||||
        fallback: document.getElementById("font-primary-fallback").value
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      secondary: {
 | 
					 | 
				
			||||||
        name: document.getElementById("font-secondary").value,
 | 
					 | 
				
			||||||
        fallback: document.getElementById("font-secondary-fallback").value
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					      // Merge with loadedConfig to avoid overwriting other sections
 | 
				
			||||||
    data.favicon = {
 | 
					      let payload = { ...loadedConfig };
 | 
				
			||||||
      path: faviconInput.value
 | 
					      switch (section) {
 | 
				
			||||||
    };
 | 
					        case "colors":
 | 
				
			||||||
    data.google_fonts = [];
 | 
					          payload.colors = getSectionValues("colors");
 | 
				
			||||||
    document.querySelectorAll("#google-fonts-fields .input-field").forEach(field => {
 | 
					          break;
 | 
				
			||||||
      const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value;
 | 
					        case "google-fonts":
 | 
				
			||||||
      const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
 | 
					          payload.google_fonts = getSectionValues("google-fonts");
 | 
				
			||||||
        .split(",").map(w => w.trim()).filter(w => w);
 | 
					          break;
 | 
				
			||||||
      if (family) data.google_fonts.push({ family, weights });
 | 
					        case "fonts":
 | 
				
			||||||
    });
 | 
					          payload.fonts = getSectionValues("fonts");
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case "favicon":
 | 
				
			||||||
 | 
					          payload.favicon = getSectionValues("favicon");
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      showLoader("Saving...");
 | 
				
			||||||
      const res = await fetch("/api/theme-info", {
 | 
					      const res = await fetch("/api/theme-info", {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        headers: { "Content-Type": "application/json" },
 | 
					        headers: { "Content-Type": "application/json" },
 | 
				
			||||||
      body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
 | 
					        body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: payload })
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (res.ok) {
 | 
					      if (res.ok) {
 | 
				
			||||||
      showToast("✅ Theme saved!", "success");
 | 
					        showToast("✅ Section saved!", "success");
 | 
				
			||||||
 | 
					        const updatedThemeInfo = await fetchThemeInfo();
 | 
				
			||||||
 | 
					        loadedConfig = updatedThemeInfo.theme_yaml;
 | 
				
			||||||
 | 
					        updateSectionStatus(section, loadedConfig);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
      showToast("Error saving theme.", "error");
 | 
					        showToast("Error saving section.", "error");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Initial status update
 | 
				
			||||||
 | 
					  ["colors", "google-fonts", "fonts", "favicon"].forEach(section => updateSectionStatus(section, loadedConfig));
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -1,41 +1,47 @@
 | 
				
			|||||||
// --- Upload gallery images ---
 | 
					// --- Loader helpers ---
 | 
				
			||||||
document.getElementById('upload-gallery').addEventListener('change', async (e) => {
 | 
					function showLoader(text = "Uploading...") {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) {
 | 
				
			||||||
 | 
					    loader.classList.add("active");
 | 
				
			||||||
 | 
					    document.getElementById("loader-text").textContent = text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function hideLoader() {
 | 
				
			||||||
 | 
					  const loader = document.getElementById("global-loader");
 | 
				
			||||||
 | 
					  if (loader) loader.classList.remove("active");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Generic upload handler ---
 | 
				
			||||||
 | 
					function setupUpload(inputId, apiUrl, loaderText, successMsg, refreshFn) {
 | 
				
			||||||
 | 
					  const input = document.getElementById(inputId);
 | 
				
			||||||
 | 
					  if (!input) return;
 | 
				
			||||||
 | 
					  input.addEventListener('change', async (e) => {
 | 
				
			||||||
    const files = e.target.files;
 | 
					    const files = e.target.files;
 | 
				
			||||||
    if (!files.length) return;
 | 
					    if (!files.length) return;
 | 
				
			||||||
 | 
					    showLoader(loaderText);
 | 
				
			||||||
    const formData = new FormData();
 | 
					    const formData = new FormData();
 | 
				
			||||||
    for (const file of files) formData.append('files', file);
 | 
					    for (const file of files) formData.append('files', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
    const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
 | 
					      const res = await fetch(apiUrl, { method: 'POST', body: formData });
 | 
				
			||||||
      const data = await res.json();
 | 
					      const data = await res.json();
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      if (res.ok) {
 | 
					      if (res.ok) {
 | 
				
			||||||
      showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
 | 
					        showToast(`✅ ${data.uploaded.length} ${successMsg}`, "success");
 | 
				
			||||||
      refreshGallery();
 | 
					        if (typeof refreshFn === "function") refreshFn();
 | 
				
			||||||
      } else showToast('Error: ' + data.error, "error");
 | 
					      } else showToast('Error: ' + data.error, "error");
 | 
				
			||||||
    } catch(err) {
 | 
					    } catch(err) {
 | 
				
			||||||
 | 
					      hideLoader();
 | 
				
			||||||
      console.error(err);
 | 
					      console.error(err);
 | 
				
			||||||
      showToast('Server error!', "error");
 | 
					      showToast('Server error!', "error");
 | 
				
			||||||
  } finally { e.target.value = ''; }
 | 
					    } finally {
 | 
				
			||||||
});
 | 
					      e.target.value = '';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Upload hero images ---
 | 
					// --- Setup all upload inputs ---
 | 
				
			||||||
document.getElementById('upload-hero').addEventListener('change', async (e) => {
 | 
					setupUpload('upload-gallery', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
 | 
				
			||||||
  const files = e.target.files;
 | 
					setupUpload('upload-hero', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
 | 
				
			||||||
  if (!files.length) return;
 | 
					setupUpload('upload-gallery-bottom', '/api/gallery/upload', "Uploading photos...", "gallery image(s) uploaded!", refreshGallery);
 | 
				
			||||||
 | 
					setupUpload('upload-hero-bottom', '/api/hero/upload', "Uploading hero photos...", "hero image(s) uploaded!", refreshHero);
 | 
				
			||||||
  const formData = new FormData();
 | 
					 | 
				
			||||||
  for (const file of files) formData.append('files', file);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
 | 
					 | 
				
			||||||
    const data = await res.json();
 | 
					 | 
				
			||||||
    if (res.ok) {
 | 
					 | 
				
			||||||
      showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
 | 
					 | 
				
			||||||
      refreshHero();
 | 
					 | 
				
			||||||
    } else showToast('Error: ' + data.error, "error");
 | 
					 | 
				
			||||||
  } catch(err) {
 | 
					 | 
				
			||||||
    console.error(err);
 | 
					 | 
				
			||||||
    showToast('Server error!', "error");
 | 
					 | 
				
			||||||
  } finally { e.target.value = ''; }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@@ -4,11 +4,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h1>Edit Site Info</h1>
 | 
					<h1>Edit Site Info</h1>
 | 
				
			||||||
    <form id="site-info-form">
 | 
					
 | 
				
			||||||
      <!-- Info Section -->
 | 
					<!-- Info Section -->
 | 
				
			||||||
      <fieldset>
 | 
					<form id="info-form" autocomplete="off">
 | 
				
			||||||
 | 
					  <fieldset id="info-section">
 | 
				
			||||||
    <h2>Info</h2>
 | 
					    <h2>Info</h2>
 | 
				
			||||||
 | 
					    <p class="section-status"></p>
 | 
				
			||||||
    <p>Set the basic information for your site and SEO</p>
 | 
					    <p>Set the basic information for your site and SEO</p>
 | 
				
			||||||
    <div class="fields">
 | 
					    <div class="fields">
 | 
				
			||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
@@ -36,10 +38,15 @@
 | 
				
			|||||||
        <input type="text" name="info.author" placeholder="Your Name" required>
 | 
					        <input type="text" name="info.author" placeholder="Your Name" required>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <button type="submit" class="section-save-btn" data-section="info">Save</button>
 | 
				
			||||||
  </fieldset>
 | 
					  </fieldset>
 | 
				
			||||||
      <!-- Social Section -->
 | 
					</form>
 | 
				
			||||||
      <fieldset>
 | 
					
 | 
				
			||||||
 | 
					<!-- Social Section -->
 | 
				
			||||||
 | 
					<form id="social-form" autocomplete="off">
 | 
				
			||||||
 | 
					  <fieldset id="social-section">
 | 
				
			||||||
    <h2>Social</h2>
 | 
					    <h2>Social</h2>
 | 
				
			||||||
 | 
					    <p class="section-status"></p>
 | 
				
			||||||
    <p>Set your social media links and thumbnail for link sharing</p>
 | 
					    <p>Set your social media links and thumbnail for link sharing</p>
 | 
				
			||||||
    <div class="fields">
 | 
					    <div class="fields">
 | 
				
			||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
@@ -49,16 +56,21 @@
 | 
				
			|||||||
        <input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
 | 
					        <input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
 | 
				
			||||||
        <button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
 | 
					        <button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
 | 
				
			||||||
        <div class="thumbnail-form">
 | 
					        <div class="thumbnail-form">
 | 
				
			||||||
            <input type="hidden" name="social.thumbnail" id="social-thumbnail">
 | 
					          <input type="hidden" name="social.thumbnail" id="social-thumbnail" required>
 | 
				
			||||||
          <img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
 | 
					          <img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
 | 
				
			||||||
          <button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
 | 
					          <button type="button" id="remove-thumbnail-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <button type="submit" class="section-save-btn" data-section="social">Save</button>
 | 
				
			||||||
  </fieldset>
 | 
					  </fieldset>
 | 
				
			||||||
      <!-- Menu Section -->
 | 
					</form>
 | 
				
			||||||
      <fieldset>
 | 
					
 | 
				
			||||||
 | 
					<!-- Menu Section -->
 | 
				
			||||||
 | 
					<form id="menu-form" autocomplete="off">
 | 
				
			||||||
 | 
					  <fieldset id="menu-section">
 | 
				
			||||||
    <h2>Menu</h2>
 | 
					    <h2>Menu</h2>
 | 
				
			||||||
 | 
					    <p class="section-status"></p>
 | 
				
			||||||
    <p>Manage your site menu items. You can use tag combination to propose custom filters</p>
 | 
					    <p>Manage your site menu items. You can use tag combination to propose custom filters</p>
 | 
				
			||||||
    <div class="fields">
 | 
					    <div class="fields">
 | 
				
			||||||
      <div class="input-field" style="flex: 1 1 100%;">
 | 
					      <div class="input-field" style="flex: 1 1 100%;">
 | 
				
			||||||
@@ -66,10 +78,15 @@
 | 
				
			|||||||
        <button type="button" id="add-menu-item">+ Add menu item</button>
 | 
					        <button type="button" id="add-menu-item">+ Add menu item</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <button type="submit" class="section-save-btn" data-section="menu">Save</button>
 | 
				
			||||||
  </fieldset>
 | 
					  </fieldset>
 | 
				
			||||||
      <!-- Footer Section -->
 | 
					</form>
 | 
				
			||||||
      <fieldset>
 | 
					
 | 
				
			||||||
 | 
					<!-- Footer Section -->
 | 
				
			||||||
 | 
					<form id="footer-form" autocomplete="off">
 | 
				
			||||||
 | 
					  <fieldset id="footer-section">
 | 
				
			||||||
    <h2>Footer</h2>
 | 
					    <h2>Footer</h2>
 | 
				
			||||||
 | 
					    <p class="section-status"></p>
 | 
				
			||||||
    <p>Set your copyright informations and legal link name</p>
 | 
					    <p>Set your copyright informations and legal link name</p>
 | 
				
			||||||
    <div class="fields">
 | 
					    <div class="fields">
 | 
				
			||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
@@ -78,13 +95,18 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
        <label>Legal Label</label>
 | 
					        <label>Legal Label</label>
 | 
				
			||||||
            <input type="text" name="footer.legal_label" re>
 | 
					        <input type="text" name="footer.legal_label" required>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <button type="submit" class="section-save-btn" data-section="footer">Save</button>
 | 
				
			||||||
  </fieldset>
 | 
					  </fieldset>
 | 
				
			||||||
      <!-- Legals Section -->
 | 
					</form>
 | 
				
			||||||
      <fieldset>
 | 
					
 | 
				
			||||||
 | 
					<!-- Legals Section -->
 | 
				
			||||||
 | 
					<form id="legals-form" autocomplete="off">
 | 
				
			||||||
 | 
					  <fieldset id="legals-section">
 | 
				
			||||||
    <h2>Legals</h2>
 | 
					    <h2>Legals</h2>
 | 
				
			||||||
 | 
					    <p class="section-status"></p>
 | 
				
			||||||
    <p>Set your legal informations</p>
 | 
					    <p>Set your legal informations</p>
 | 
				
			||||||
    <div class="fields">
 | 
					    <div class="fields">
 | 
				
			||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
@@ -105,15 +127,20 @@
 | 
				
			|||||||
        <button type="button" id="add-ip-paragraph">+ Add paragraph</button>
 | 
					        <button type="button" id="add-ip-paragraph">+ Add paragraph</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <button type="submit" class="section-save-btn" data-section="legals">Save</button>
 | 
				
			||||||
  </fieldset>
 | 
					  </fieldset>
 | 
				
			||||||
      <!-- Build Section -->
 | 
					</form>
 | 
				
			||||||
      <fieldset>
 | 
					
 | 
				
			||||||
 | 
					<!-- Build Section -->
 | 
				
			||||||
 | 
					<form id="build-form" autocomplete="off">
 | 
				
			||||||
 | 
					  <fieldset id="build-section">
 | 
				
			||||||
    <h2>Build</h2>
 | 
					    <h2>Build</h2>
 | 
				
			||||||
 | 
					    <p class="section-status"></p>
 | 
				
			||||||
    <p>Select a theme from the dropdown menu or add your custom theme folder</p>
 | 
					    <p>Select a theme from the dropdown menu or add your custom theme folder</p>
 | 
				
			||||||
    <div class="fields">
 | 
					    <div class="fields">
 | 
				
			||||||
      <div class="input-field">
 | 
					      <div class="input-field">
 | 
				
			||||||
        <label>Theme</label>
 | 
					        <label>Theme</label>
 | 
				
			||||||
            <select name="build.theme" id="theme-select" required></select>
 | 
					        <select name="build.theme" id="theme-select"></select>
 | 
				
			||||||
        <button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
 | 
					        <button type="button" id="remove-theme-btn" class="remove-btn up-btn danger">🗑 Remove selected theme</button>
 | 
				
			||||||
        <input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
 | 
					        <input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
 | 
				
			||||||
        <button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
 | 
					        <button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
 | 
				
			||||||
@@ -129,24 +156,26 @@
 | 
				
			|||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <button type="submit" class="section-save-btn" data-section="build">Save</button>
 | 
				
			||||||
  </fieldset>
 | 
					  </fieldset>
 | 
				
			||||||
      <button type="submit">Save</button>
 | 
					</form>
 | 
				
			||||||
    </form>
 | 
					
 | 
				
			||||||
    <div class="section">
 | 
					<!-- Stepper -->
 | 
				
			||||||
 | 
					<div class="section">
 | 
				
			||||||
  <h2>Steps</h2>
 | 
					  <h2>Steps</h2>
 | 
				
			||||||
        <p> Follow the steps to generate your static gallery</p>
 | 
					  <p>Follow the steps to generate your static gallery</p>
 | 
				
			||||||
  <ul id="stepper">
 | 
					  <ul id="stepper">
 | 
				
			||||||
    <li><a href="/gallery-editor">Upload your photos</a></li>
 | 
					    <li><a href="/gallery-editor">Upload your photos</a></li>
 | 
				
			||||||
          <div>→</div>
 | 
					    <div></div>
 | 
				
			||||||
    <li><a class="step-active" href="/site-info">Configure site info</a></li>
 | 
					    <li><a class="step-active" href="/site-info">Configure site info</a></li>
 | 
				
			||||||
          <div>→</div>
 | 
					    <div></div>
 | 
				
			||||||
    <li><a href="/theme-editor">Customize your theme</a></li>
 | 
					    <li><a href="/theme-editor">Customize your theme</a></li>
 | 
				
			||||||
          <div>→</div>
 | 
					    <div></div>
 | 
				
			||||||
    <li><button id="stepper-build">Generate your static site!</button></li>
 | 
					    <li><button id="stepper-build">Generate your static site!</button></li>
 | 
				
			||||||
  </ul>
 | 
					  </ul>
 | 
				
			||||||
      </div>
 | 
					</div>
 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
<!-- Delete thumbnail confirmation modal-->
 | 
					<!-- Delete thumbnail confirmation modal-->
 | 
				
			||||||
 | 
					<div class="content-inner">
 | 
				
			||||||
  <div id="delete-modal" class="modal" style="display:none;">
 | 
					  <div id="delete-modal" class="modal" style="display:none;">
 | 
				
			||||||
    <div class="modal-content">
 | 
					    <div class="modal-content">
 | 
				
			||||||
      <span id="delete-modal-close" class="modal-close">×</span>
 | 
					      <span id="delete-modal-close" class="modal-close">×</span>
 | 
				
			||||||
@@ -158,7 +187,9 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <!-- Delete theme confirmation modal -->
 | 
					</div>
 | 
				
			||||||
 | 
					<!-- Delete theme confirmation modal -->
 | 
				
			||||||
 | 
					<div class="content-inner">
 | 
				
			||||||
  <div id="delete-theme-modal" class="modal" style="display:none;">
 | 
					  <div id="delete-theme-modal" class="modal" style="display:none;">
 | 
				
			||||||
    <div class="modal-content">
 | 
					    <div class="modal-content">
 | 
				
			||||||
      <span id="delete-theme-modal-close" class="modal-close">×</span>
 | 
					      <span id="delete-theme-modal-close" class="modal-close">×</span>
 | 
				
			||||||
@@ -170,6 +201,7 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,13 @@
 | 
				
			|||||||
/* --- Base Styles --- */
 | 
					/* --- Base Styles --- */
 | 
				
			||||||
body {
 | 
					body {
 | 
				
			||||||
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
 | 
					  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
 | 
				
			||||||
  margin: 20px;
 | 
					 | 
				
			||||||
  background: #111010;
 | 
					  background: #111010;
 | 
				
			||||||
  /* background:radial-gradient(ellipse at bottom center, #002a30, #000000bd), radial-gradient(ellipse at top center, #0558a8, #000000fa); */
 | 
					 | 
				
			||||||
  color: #FBFBFB;
 | 
					  color: #FBFBFB;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
  min-height: 100vh;
 | 
					  min-height: 100vh;
 | 
				
			||||||
  margin:0px;
 | 
					  margin:0px;
 | 
				
			||||||
 | 
					  min-width: 320px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a {
 | 
					a {
 | 
				
			||||||
@@ -16,15 +15,6 @@ a {
 | 
				
			|||||||
  color: #d3d3d3;
 | 
					  color: #d3d3d3;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.content-inner {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  margin: 0 auto;
 | 
					 | 
				
			||||||
  max-width: 1140px;
 | 
					 | 
				
			||||||
  padding: 0 40px;
 | 
					 | 
				
			||||||
  padding-top: 70px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
h1, h2 {
 | 
					h1, h2 {
 | 
				
			||||||
  color: #FBFBFB;
 | 
					  color: #FBFBFB;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -32,26 +22,204 @@ h2 {
 | 
				
			|||||||
  color: #55c3ec;
 | 
					  color: #55c3ec;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Toolbar --- */
 | 
					.content-inner {
 | 
				
			||||||
.toolbar {
 | 
					  margin: 0 auto;
 | 
				
			||||||
  margin-bottom: 20px;
 | 
					  max-width: 1220px;
 | 
				
			||||||
 | 
					  padding-top: 70px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.inner {
 | 
				
			||||||
 | 
					  padding: 0 40px;
 | 
				
			||||||
 | 
					  margin: auto;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.toolbar button {
 | 
					
 | 
				
			||||||
  margin-right: 10px;
 | 
					/* --- Navbar & Burger Menu --- */
 | 
				
			||||||
  padding: 8px 12px;
 | 
					
 | 
				
			||||||
 | 
					.nav-bar {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 70px;
 | 
				
			||||||
 | 
					  background: #0c0d0c29;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					  backdrop-filter: blur(20px);
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #21212157;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  max-width: 1140px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 0 40px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					  height: 70px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-title {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  font-size: 22px;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav img {
 | 
				
			||||||
 | 
					  height: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-links {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-list {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 0;
 | 
				
			||||||
 | 
					  list-style: none;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-item a {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color:#fff;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-item a:hover {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color: #55c3ec;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-list > li + li::before {
 | 
				
			||||||
 | 
					  content: " → ";
 | 
				
			||||||
 | 
					  color: #ffc700;
 | 
				
			||||||
 | 
					  margin: 0 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button {
 | 
				
			||||||
 | 
					  padding: 10px 25px;
 | 
				
			||||||
 | 
					  border-radius: 40px;
 | 
				
			||||||
 | 
					  margin-left: 10px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  background: linear-gradient(135deg, #26c4ff, #016074);
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  background-color: #4CAF50;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  transition: background 0.2s;
 | 
				
			||||||
  transition: background-color 0.2s;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.toolbar button:hover {
 | 
					.button:hover {
 | 
				
			||||||
  background-color: #45a049;
 | 
					  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- Burger Menu --- */
 | 
				
			||||||
 | 
					.nav-burger {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  width: 40px;
 | 
				
			||||||
 | 
					  height: 40px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  margin-left: auto;
 | 
				
			||||||
 | 
					  z-index: 1100;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav-burger span {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  width: 28px;
 | 
				
			||||||
 | 
					  height: 4px;
 | 
				
			||||||
 | 
					  margin: 4px;
 | 
				
			||||||
 | 
					  background: #fff;
 | 
				
			||||||
 | 
					  border-radius: 2px;
 | 
				
			||||||
 | 
					  transition: 0.3s;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- Responsive Navbar & Burger --- */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .nav-burger {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .nav-list > li::before {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .nav-links {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 70px;
 | 
				
			||||||
 | 
					    left: 0;
 | 
				
			||||||
 | 
					    width: 100vw;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    align-items: flex-start;
 | 
				
			||||||
 | 
					    padding: 24px 0 12px 0;
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    z-index: 1099;
 | 
				
			||||||
 | 
					    backdrop-filter: blur(20px);
 | 
				
			||||||
 | 
					    background-color: #000000d4
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .nav-links .nav-list {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .nav-links .nav-item {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .nav-links .nav-item a,
 | 
				
			||||||
 | 
					  .nav-links .nav-item button {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 16px 24px;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    font-size: 18px;
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    margin: 0 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Show menu when burger is checked */
 | 
				
			||||||
 | 
					  .nav-toggle:checked ~ .nav-burger + .nav-links {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  /* Animate burger to X */
 | 
				
			||||||
 | 
					  .nav-toggle:checked ~ .nav-burger span:nth-child(1) {
 | 
				
			||||||
 | 
					    transform: translateY(12px) rotate(45deg);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .nav-toggle:checked ~ .nav-burger span:nth-child(2) {
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .nav-toggle:checked ~ .nav-burger span:nth-child(3) {
 | 
				
			||||||
 | 
					    transform: translateY(-12px) rotate(-45deg);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
/* --- Upload Section --- */
 | 
					/* --- Upload Section --- */
 | 
				
			||||||
.section {
 | 
					.section {
 | 
				
			||||||
  margin-bottom: 30px;
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
@@ -60,17 +228,40 @@ h2 {
 | 
				
			|||||||
  border: 1px solid #2f2e2e80;
 | 
					  border: 1px solid #2f2e2e80;
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
  padding: 0px 20px 20px 20px;
 | 
					  padding: 0px 20px 20px 20px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.section label {
 | 
					.section label {
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- Filter Row --- */
 | 
				
			||||||
 | 
					.filter-row label {
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  margin-right: 16px;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-row input[type="radio"] {
 | 
				
			||||||
 | 
					  accent-color: #55c3ec;
 | 
				
			||||||
 | 
					  margin-right: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Gallery & Hero Grid --- */
 | 
					/* --- Gallery & Hero Grid --- */
 | 
				
			||||||
#gallery, #hero {
 | 
					#gallery, #hero {
 | 
				
			||||||
  display: grid;
 | 
					  display: grid;
 | 
				
			||||||
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
 | 
					  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
 | 
				
			||||||
  gap: 15px;
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					  margin: 30px 0 0 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#hero-count-bottom, #gallery-count-bottom {
 | 
				
			||||||
 | 
					  flex-basis: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bottom-action-row {
 | 
				
			||||||
  margin-top: 30px;
 | 
					  margin-top: 30px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -113,22 +304,6 @@ h2 {
 | 
				
			|||||||
  background-color: #d32f2f;
 | 
					  background-color: #d32f2f;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Responsive Adjustments --- */
 | 
					 | 
				
			||||||
@media (max-width: 500px) {
 | 
					 | 
				
			||||||
  body {
 | 
					 | 
				
			||||||
    margin: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .toolbar button {
 | 
					 | 
				
			||||||
    margin-bottom: 8px;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .section label {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Toast Notifications --- */
 | 
					/* --- Toast Notifications --- */
 | 
				
			||||||
#toast-container {
 | 
					#toast-container {
 | 
				
			||||||
@@ -162,10 +337,23 @@ h2 {
 | 
				
			|||||||
.toast.success { background-color: #28a7468c; }
 | 
					.toast.success { background-color: #28a7468c; }
 | 
				
			||||||
.toast.error { background-color: #dc3545; }
 | 
					.toast.error { background-color: #dc3545; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* img fade in */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fade-in-img {
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  transform: scale(1.02);
 | 
				
			||||||
 | 
					  transition: opacity 1.2s ease-out, transform 1.2s ease-out;
 | 
				
			||||||
 | 
					  will-change: opacity, transform;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fade-in-img.loaded {
 | 
				
			||||||
 | 
					  opacity: 1;
 | 
				
			||||||
 | 
					  transform: scale(1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Tags --- */
 | 
					/* --- Tags --- */
 | 
				
			||||||
.tag-input {
 | 
					.tag-input {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-wrap: wrap-reverse;
 | 
					 | 
				
			||||||
  align-content: flex-start;
 | 
					  align-content: flex-start;
 | 
				
			||||||
  gap: 4px;
 | 
					  gap: 4px;
 | 
				
			||||||
  padding: 4px;
 | 
					  padding: 4px;
 | 
				
			||||||
@@ -213,15 +401,18 @@ h2 {
 | 
				
			|||||||
  z-index: 999;
 | 
					  z-index: 999;
 | 
				
			||||||
  display: none;
 | 
					  display: none;
 | 
				
			||||||
  box-shadow: 0 4px 8px rgba(0,0,0,0.15);
 | 
					  box-shadow: 0 4px 8px rgba(0,0,0,0.15);
 | 
				
			||||||
 | 
					  padding: 8px;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tag-input ul.suggestions li {
 | 
					.tag-input ul.suggestions li {
 | 
				
			||||||
  padding: 6px 8px;
 | 
					  padding: 6px 10px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tag-input ul.suggestions li:hover {
 | 
					.tag-input ul.suggestions li:hover {
 | 
				
			||||||
  background-color: #007782;
 | 
					  background-color: #007782;
 | 
				
			||||||
 | 
					  border-radius: 7px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tags-display {
 | 
					.tags-display {
 | 
				
			||||||
@@ -231,11 +422,31 @@ h2 {
 | 
				
			|||||||
  margin-top: 8px;
 | 
					  margin-top: 8px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photo button.validate-tag-btn {
 | 
				
			||||||
 | 
					    border-radius: 30px;
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background: #049b3d;
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    font-size: 10px;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    margin-left: 4px;
 | 
				
			||||||
 | 
					    transition: all ease 0.2s;
 | 
				
			||||||
 | 
					    width: 35px;
 | 
				
			||||||
 | 
					    border: 1px solid #585858;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photo button.validate-tag-btn:hover {
 | 
				
			||||||
 | 
					  background: #02cb4e;
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.suggestions li.selected {
 | 
					.suggestions li.selected {
 | 
				
			||||||
  background-color: #007782;
 | 
					  background-color: #007782;
 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  border-radius: 7px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.suggestions li {
 | 
					.suggestions li {
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -245,6 +456,14 @@ h2 {
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.modal-actions-row {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 16px;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  justify-content: center; /* Center horizontally */
 | 
				
			||||||
 | 
					  align-items: center;     /* Center vertically */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.flex-column {
 | 
					.flex-column {
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -259,149 +478,7 @@ h2 {
 | 
				
			|||||||
  width: 100%
 | 
					  width: 100%
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Top Bar & Navigation --- */
 | 
					/* --- Upload Buttons --- */
 | 
				
			||||||
.nav {
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  max-width: 1140px;
 | 
					 | 
				
			||||||
  padding: 0 40px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-bar {
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					 | 
				
			||||||
  top: 0;
 | 
					 | 
				
			||||||
  left: 0;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: 70px;
 | 
					 | 
				
			||||||
  background-color: #0c0d0c29;
 | 
					 | 
				
			||||||
  z-index: 1000;
 | 
					 | 
				
			||||||
  backdrop-filter: blur(20px);
 | 
					 | 
				
			||||||
  border-bottom: 1px solid #21212157;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav img {
 | 
					 | 
				
			||||||
  height: 30px;
 | 
					 | 
				
			||||||
  padding: 0px;
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
} 
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > .nav-header {
 | 
					 | 
				
			||||||
  display: inline;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > .nav-header > .nav-title {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  font-size: 22px;
 | 
					 | 
				
			||||||
  color: #fff;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > .nav-btn {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-btn label {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  justify-content: center;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  width: 32px;
 | 
					 | 
				
			||||||
  height: 32px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > .nav-links {
 | 
					 | 
				
			||||||
  display: inline;
 | 
					 | 
				
			||||||
  float: right;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  line-height: 70px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-item {
 | 
					 | 
				
			||||||
  display: inline;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-list {
 | 
					 | 
				
			||||||
  list-style-type: disc;
 | 
					 | 
				
			||||||
  margin: 0px;
 | 
					 | 
				
			||||||
  padding: 0px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > .nav-links > .nav-list > .nav-item > a {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  padding: 0px 15px 0px 15px;
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  font-weight: 700;
 | 
					 | 
				
			||||||
  color:#fff
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > .nav-links > .nav-list > .nav-item > a:hover {
 | 
					 | 
				
			||||||
  color: #55c3ec;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > .nav-links > .nav-list > .nav-item > a:active {
 | 
					 | 
				
			||||||
  color: #55c3ec;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav > #nav-check {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-list > li + li::before{
 | 
					 | 
				
			||||||
  content: " → ";
 | 
					 | 
				
			||||||
  color: #ffc700;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-cta {
 | 
					 | 
				
			||||||
  display: inline;
 | 
					 | 
				
			||||||
  float: right;
 | 
					 | 
				
			||||||
  height: 70px;
 | 
					 | 
				
			||||||
  line-height: 70px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-cta > .arrow {
 | 
					 | 
				
			||||||
  font-size: 12px;
 | 
					 | 
				
			||||||
  display: inline;
 | 
					 | 
				
			||||||
  color: #ffc700;
 | 
					 | 
				
			||||||
  font-weight: 700;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-bar .button {
 | 
					 | 
				
			||||||
  padding: 10px 25px;
 | 
					 | 
				
			||||||
  border-radius: 40px;
 | 
					 | 
				
			||||||
  margin: 15px 20px 15px 10px;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  display: inline;
 | 
					 | 
				
			||||||
  background: linear-gradient(135deg, #26c4ff, #016074);
 | 
					 | 
				
			||||||
  transition: all 0.2s ease;
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
  color: #fff;
 | 
					 | 
				
			||||||
  font-weight: 700;
 | 
					 | 
				
			||||||
  border: none;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-bar .button:hover {
 | 
					 | 
				
			||||||
  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
					 | 
				
			||||||
  transition: all 0.2s ease;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-links > ul {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav-btn span {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  height: 4px;
 | 
					 | 
				
			||||||
  width: 28px;
 | 
					 | 
				
			||||||
  background: #fff;
 | 
					 | 
				
			||||||
  margin: 4px 0;
 | 
					 | 
				
			||||||
  border-radius: 2px;
 | 
					 | 
				
			||||||
  transition: all 0.3s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/* --- Custom Upload Buttons --- */
 | 
					 | 
				
			||||||
.up-btn, .footer-links a{
 | 
					.up-btn, .footer-links a{
 | 
				
			||||||
  display: inline-block;
 | 
					  display: inline-block;
 | 
				
			||||||
  background: #00000000;
 | 
					  background: #00000000;
 | 
				
			||||||
@@ -413,7 +490,6 @@ h2 {
 | 
				
			|||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  transition: all 0.1s ease;
 | 
					  transition: all 0.1s ease;
 | 
				
			||||||
  user-select: none;
 | 
					  user-select: none;
 | 
				
			||||||
  /* box-shadow: 0 4px 10px rgba(0,0,0,0.25);*/
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
  border: 1px solid #585858;
 | 
					  border: 1px solid #585858;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -430,18 +506,20 @@ h2 {
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  margin: 0 30px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-content {
 | 
					.modal-content {
 | 
				
			||||||
  background: #131313;
 | 
					  background: #000000a3;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  padding: 2rem 2.5rem;
 | 
					  padding: 2rem 2.5rem;
 | 
				
			||||||
  border-radius: 10px;
 | 
					  border-radius: 10px;
 | 
				
			||||||
  box-shadow: 0 4px 24px rgba(0,0,0,0.25);
 | 
					  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
 | 
				
			||||||
  min-width: 300px;
 | 
					  min-width: 200px;
 | 
				
			||||||
  max-width: 90vw;
 | 
					  max-width: 90vw;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  backdrop-filter: blur(20px);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-close {
 | 
					.modal-close {
 | 
				
			||||||
@@ -491,31 +569,36 @@ h2 {
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  gap: 16px;
 | 
					  gap: 16px;
 | 
				
			||||||
  margin-bottom: 10px;
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Remove All Buttons */
 | 
					/* --- Remove All Buttons --- */
 | 
				
			||||||
#remove-all-hero, #remove-all-gallery {
 | 
					#remove-all-hero, #remove-all-gallery, .bottom-remove-btn {
 | 
				
			||||||
  background: #2d2d2d;
 | 
					  background: #2d2d2d;
 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
  display: none;
 | 
					  display: none;
 | 
				
			||||||
 | 
					  margin-bottom: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bottom-remove-btn {
 | 
				
			||||||
 | 
					  display: inherit;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#remove-all-gallery:hover,
 | 
					#remove-all-gallery:hover,
 | 
				
			||||||
#remove-all-hero:hover {
 | 
					#remove-all-hero:hover, .bottom-remove-btn:hover {
 | 
				
			||||||
  background: rgb(121, 26, 19);
 | 
					  background: rgb(121, 26, 19);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Responsive: stack buttons vertically on small screens */
 | 
					 | 
				
			||||||
@media (max-width: 500px) {
 | 
					@media (max-width: 500px) {
 | 
				
			||||||
  .upload-actions-row {
 | 
					  .upload-actions-row {
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    align-items: stretch;
 | 
					    align-items: stretch;
 | 
				
			||||||
    gap: 8px;
 | 
					    gap: 8px;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Site Info --- */
 | 
					/* --- Forms --- */
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
fieldset {
 | 
					fieldset {
 | 
				
			||||||
  background-color: rgb(67 67 67 / 26%);
 | 
					  background-color: rgb(67 67 67 / 26%);
 | 
				
			||||||
@@ -543,7 +626,7 @@ legend {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.input-field {
 | 
					.input-field {
 | 
				
			||||||
  flex: 1 1 calc(33.333% - 18px);
 | 
					  flex: 1 1 calc(33.333% - 18px);
 | 
				
			||||||
  min-width: 220px;
 | 
					  min-width: 150px;
 | 
				
			||||||
  max-width: 100%;
 | 
					  max-width: 100%;
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -559,9 +642,9 @@ label {
 | 
				
			|||||||
  letter-spacing: 0.5px;
 | 
					  letter-spacing: 0.5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form input, #theme-editor-form input,
 | 
					form input,
 | 
				
			||||||
#site-info-form textarea, #theme-editor-form textarea,
 | 
					form textarea,
 | 
				
			||||||
#site-info-form select, #theme-editor-form select {
 | 
					form select {
 | 
				
			||||||
  background: #1f2223;
 | 
					  background: #1f2223;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  border: 1px solid #585858;
 | 
					  border: 1px solid #585858;
 | 
				
			||||||
@@ -575,25 +658,32 @@ label {
 | 
				
			|||||||
  box-shadow: 0 2px 8px rgba(0,0,0,0.07);
 | 
					  box-shadow: 0 2px 8px rgba(0,0,0,0.07);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form input::placeholder,
 | 
					#theme-editor-form input, #theme-editor-form textarea,#theme-editor-form select {
 | 
				
			||||||
#theme-editor-form input::placeholder,
 | 
					  margin-bottom: 18px;
 | 
				
			||||||
#site-info-form textarea::placeholder,
 | 
					}
 | 
				
			||||||
#theme-editor-form textarea::placeholder {
 | 
					
 | 
				
			||||||
 | 
					#theme-editor-form .fields {
 | 
				
			||||||
 | 
					  gap: 0 18px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.theme-info {
 | 
				
			||||||
 | 
					  margin-bottom: 25px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					form input::placeholder,
 | 
				
			||||||
 | 
					form textarea::placeholder {
 | 
				
			||||||
  color: #585858;
 | 
					  color: #585858;
 | 
				
			||||||
  font-style: italic;
 | 
					  font-style: italic;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form input:focus,
 | 
					form input:focus,
 | 
				
			||||||
#theme-editor-form input:focus,
 | 
					form textarea:focus,
 | 
				
			||||||
#site-info-form textarea:focus,
 | 
					form select:focus {
 | 
				
			||||||
#theme-editor-form textarea:focus,
 | 
					 | 
				
			||||||
#site-info-form select:focus,
 | 
					 | 
				
			||||||
#theme-editor-form select:focus {
 | 
					 | 
				
			||||||
  border-color: #585858;
 | 
					  border-color: #585858;
 | 
				
			||||||
  background: #161616;
 | 
					  background: #161616;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#site-info-form textarea,
 | 
					
 | 
				
			||||||
#theme-editor-form textarea {
 | 
					form textarea {
 | 
				
			||||||
  min-height: 60px;
 | 
					  min-height: 60px;
 | 
				
			||||||
  resize: vertical;
 | 
					  resize: vertical;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -612,7 +702,7 @@ img#thumbnail-preview {
 | 
				
			|||||||
  border: 1px solid #585858;
 | 
					  border: 1px solid #585858;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
 | 
					form button[type="submit"] {
 | 
				
			||||||
  background: linear-gradient(135deg, #26c4ff, #016074);
 | 
					  background: linear-gradient(135deg, #26c4ff, #016074);
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  font-weight: 700;
 | 
					  font-weight: 700;
 | 
				
			||||||
@@ -620,21 +710,22 @@ img#thumbnail-preview {
 | 
				
			|||||||
  border-radius: 30px;
 | 
					  border-radius: 30px;
 | 
				
			||||||
  padding: 12px 32px;
 | 
					  padding: 12px 32px;
 | 
				
			||||||
  font-size: 1.1em;
 | 
					  font-size: 1.1em;
 | 
				
			||||||
  margin: 0 0 45px 0;
 | 
					  margin: 16px 0 0 0;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  box-shadow: 0 4px 16px rgba(38,196,255,0.15);
 | 
					  box-shadow: 0 4px 16px rgba(38,196,255,0.15);
 | 
				
			||||||
  transition: background 0.2s;
 | 
					  transition: background 0.2s;
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="submit"]:hover,  #theme-editor-form button[type="submit"]:hover {
 | 
					form button[type="submit"]:hover {
 | 
				
			||||||
  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
					  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="button"], #theme-editor-form button[type="button"] {
 | 
					form button[type="button"] {
 | 
				
			||||||
  background: #00000000;
 | 
					  background: #00000000;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  border-radius: 18px;
 | 
					  border-radius: 30px;
 | 
				
			||||||
  padding: 7px 18px;
 | 
					  padding: 7px 18px;
 | 
				
			||||||
  font-size: 0.98em;
 | 
					  font-size: 0.98em;
 | 
				
			||||||
  margin-top: 8px;
 | 
					  margin-top: 8px;
 | 
				
			||||||
@@ -643,48 +734,36 @@ img#thumbnail-preview {
 | 
				
			|||||||
  border: 1px solid #585858;
 | 
					  border: 1px solid #585858;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button[type="button"]:hover, #theme-editor-form button[type="button"]:hover {
 | 
					form button[type="button"]:hover{
 | 
				
			||||||
  background: #2d2d2d;
 | 
					  background: #2d2d2d;
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (max-width: 900px) {
 | 
					 | 
				
			||||||
  #site-info-form, #theme-editor-form {
 | 
					 | 
				
			||||||
    padding: 18px 8px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  .fields,
 | 
					 | 
				
			||||||
  fieldset {
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  .input-field {
 | 
					 | 
				
			||||||
    min-width: 100%;
 | 
					 | 
				
			||||||
    margin-bottom: 12px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph {
 | 
					form button.remove-menu-item,
 | 
				
			||||||
 | 
					form button.remove-ip-paragraph {
 | 
				
			||||||
  margin-top: 0px;
 | 
					  margin-top: 0px;
 | 
				
			||||||
  margin-bottom: 4px;
 | 
					  margin-bottom: 4px;
 | 
				
			||||||
  border-radius: 30px;
 | 
					  border-radius: 30px;
 | 
				
			||||||
  background: #2d2d2d;
 | 
					  background: #2d2d2d;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover, #theme-editor-form button.remove-menu-item:hover, #theme-editor-form button.remove-ip-paragraph:hover {
 | 
					form button.remove-menu-item:hover,
 | 
				
			||||||
 | 
					form button.remove-ip-paragraph:hover {
 | 
				
			||||||
  background: rgb(121, 26, 19);
 | 
					  background: rgb(121, 26, 19);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-btn, #theme-editor-form button.remove-btn {
 | 
					form button.remove-btn {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  border-radius: 30px;
 | 
					  border-radius: 30px;
 | 
				
			||||||
  background: #2d2d2d;
 | 
					  background: #2d2d2d;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form button.remove-btn:hover, #theme-editor-form button.remove-btn:hover {
 | 
					form button.remove-btn:hover {
 | 
				
			||||||
  background: rgb(121, 26, 19);
 | 
					  background: rgb(121, 26, 19);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label {
 | 
					form .thumbnail-form-label {
 | 
				
			||||||
  margin-top: 10px;
 | 
					  margin-top: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -693,9 +772,10 @@ img#thumbnail-preview {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.color-fields {
 | 
					.fields.color-fields {
 | 
				
			||||||
  gap: 8px;
 | 
					  gap: 8px;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  flex-wrap: nowrap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -745,13 +825,17 @@ fieldset p, .section p {
 | 
				
			|||||||
  margin-top: 16px;
 | 
					  margin-top: 16px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* --- Home --- */
 | 
					.section-status {
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- Stepper --- */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#stepper {
 | 
					#stepper {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 18px;
 | 
					  gap: 18px;
 | 
				
			||||||
  flex-wrap: nowrap;
 | 
					  flex-wrap: nowrap;
 | 
				
			||||||
  align-items: stretch; /* Ensures all children match tallest height */
 | 
					  align-items: stretch;
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  margin-left: auto;
 | 
					  margin-left: auto;
 | 
				
			||||||
  margin-right: auto;
 | 
					  margin-right: auto;
 | 
				
			||||||
@@ -764,6 +848,11 @@ fieldset p, .section p {
 | 
				
			|||||||
  
 | 
					  
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #stepper > div::before {
 | 
				
			||||||
 | 
					    content: "→";
 | 
				
			||||||
 | 
					    color: #ffc700;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#stepper li a, #stepper li button, #stepper > div  {
 | 
					#stepper li a, #stepper li button, #stepper > div  {
 | 
				
			||||||
justify-content: center;
 | 
					justify-content: center;
 | 
				
			||||||
  min-width: 100px;
 | 
					  min-width: 100px;
 | 
				
			||||||
@@ -816,11 +905,11 @@ justify-content: center;
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#stepper li {
 | 
					#stepper li {
 | 
				
			||||||
  flex: 1 1 auto; /* li can grow/shrink, width is flexible */
 | 
					  flex: 1 1 auto; 
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#stepper > div {
 | 
					#stepper > div {
 | 
				
			||||||
  flex: 0 0 auto; /* Do not grow or shrink, width is auto */
 | 
					  flex: 0 0 auto;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
@@ -828,10 +917,16 @@ justify-content: center;
 | 
				
			|||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  background: none;
 | 
					  background: none;
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  min-width: 0; /* Prevent unwanted minimum width */
 | 
					  min-width: 0;
 | 
				
			||||||
  width: auto;   /* Let width fit content */
 | 
					  width: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stepper {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- Footer --- */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#footer {
 | 
					#footer {
 | 
				
			||||||
  background-color: #0c0d0c29;
 | 
					  background-color: #0c0d0c29;
 | 
				
			||||||
  z-index: 1000;
 | 
					  z-index: 1000;
 | 
				
			||||||
@@ -868,6 +963,7 @@ justify-content: center;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#footer a {
 | 
					#footer a {
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  margin: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.footer-credit .lum-first::before {
 | 
					.footer-credit .lum-first::before {
 | 
				
			||||||
@@ -883,9 +979,123 @@ justify-content: center;
 | 
				
			|||||||
    width: 16px;
 | 
					    width: 16px;
 | 
				
			||||||
    height: 16px;
 | 
					    height: 16px;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    margin: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.icon-text {
 | 
					.icon-text {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- Global Loader & Spinner --- */
 | 
				
			||||||
 | 
					#global-loader {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0; left: 0; width: 100vw; height: 100vh;
 | 
				
			||||||
 | 
					  z-index: 99999;
 | 
				
			||||||
 | 
					  background: rgba(0,0,0,0.4);
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#global-loader.active {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loader-inner {
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  transition: opacity 0.9s cubic-bezier(.4,0,.2,1);
 | 
				
			||||||
 | 
					  background: #0c0d0c29;
 | 
				
			||||||
 | 
					  padding: 32px 48px;
 | 
				
			||||||
 | 
					  border-radius: 16px;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 24px #000;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  backdrop-filter: blur(20px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#global-loader.active .loader-inner {
 | 
				
			||||||
 | 
					  opacity: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loader-spinner {
 | 
				
			||||||
 | 
					  width: 48px;
 | 
				
			||||||
 | 
					  height: 48px;
 | 
				
			||||||
 | 
					  border: 6px solid #55c3ec;
 | 
				
			||||||
 | 
					  border-top: 6px solid #222;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  animation: spin 1s linear infinite;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#loader-text {
 | 
				
			||||||
 | 
					  margin-top: 18px;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes spin { 100% { transform: rotate(360deg); } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* --- Responsive Adjustments --- */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .nav {
 | 
				
			||||||
 | 
					    padding: 0 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .inner {
 | 
				
			||||||
 | 
					    padding: 0 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .section label {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #menu-items-list > div {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  form button[type="button"], form button[type="submit"],   #stepper li {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #stepper {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .footer-container, .footer-links {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #ip-list > div {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  #stepper > div {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  #stepper > div::before {
 | 
				
			||||||
 | 
					    content: "↓";
 | 
				
			||||||
 | 
					    color: #ffc700;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  #stepper > div {
 | 
				
			||||||
 | 
					    /* Hide the default arrow */
 | 
				
			||||||
 | 
					    color: transparent;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .input-field {
 | 
				
			||||||
 | 
					    min-width: 100%;
 | 
				
			||||||
 | 
					    margin-bottom: 12px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #color-picker .input-field{
 | 
				
			||||||
 | 
					    min-width: 170px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  #theme-editor-form .fields.color-fields {
 | 
				
			||||||
 | 
					    gap: 0 8px;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    flex-wrap: nowrap;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,31 +5,31 @@
 | 
				
			|||||||
    <meta charset="UTF-8">
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
    <title>{% block title %}Lumeex{% endblock %}</title>
 | 
					    <title>{% block title %}Lumeex{% endblock %}</title>
 | 
				
			||||||
    <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
					    <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
				
			||||||
 | 
					    <meta name="darkreader-lock">
 | 
				
			||||||
    </head>
 | 
					    </head>
 | 
				
			||||||
    <body>
 | 
					    <body>
 | 
				
			||||||
    <!-- Top bar -->	
 | 
					    <!-- Top bar -->	
 | 
				
			||||||
    <div class="nav-bar">
 | 
					    <div class="nav-bar">
 | 
				
			||||||
        <div class="content-inner nav">
 | 
					        <div class="content-inner nav">
 | 
				
			||||||
        <input type="checkbox" id="nav-check">
 | 
					 | 
				
			||||||
            <div class="nav-header">
 | 
					            <div class="nav-header">
 | 
				
			||||||
            <div class="nav-title">
 | 
					                <a href="/" class="nav-title">
 | 
				
			||||||
            <a href="/"><img src="{{ url_for('static', filename='img/logo.svg') }}"></a>
 | 
					                    <img src="{{ url_for('static', filename='img/logo.svg') }}">
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					            <!-- Burger toggle input and label -->
 | 
				
			||||||
        <div class="nav-btn">
 | 
					            <input type="checkbox" id="nav-toggle" class="nav-toggle" hidden>
 | 
				
			||||||
            <label for="nav-check">
 | 
					            <label for="nav-toggle" class="nav-burger">
 | 
				
			||||||
                <span></span>
 | 
					                <span></span>
 | 
				
			||||||
                <span></span>
 | 
					                <span></span>
 | 
				
			||||||
                <span></span>
 | 
					                <span></span>
 | 
				
			||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
            <div class="nav-links">
 | 
					            <div class="nav-links">
 | 
				
			||||||
                <ul class="nav-list">
 | 
					                <ul class="nav-list">
 | 
				
			||||||
                    <li class="nav-item"><a href="/gallery-editor">Gallery</a></li>
 | 
					                    <li class="nav-item"><a href="/gallery-editor">Gallery</a></li>
 | 
				
			||||||
                    <li class="nav-item"><a href="/site-info">Site info</a></li>
 | 
					                    <li class="nav-item"><a href="/site-info">Site info</a></li>
 | 
				
			||||||
                    <li class="nav-item"><a href="/theme-editor">Theme info</a></li>
 | 
					                    <li class="nav-item"><a href="/theme-editor">Theme info</a></li>
 | 
				
			||||||
                    <li class="nav-item">
 | 
					                    <li class="nav-item">
 | 
				
			||||||
                <button id="build-btn" class="button">🚀 Build !</button>
 | 
					                        <button id="build-btn" class="button">🚀 Build!</button>
 | 
				
			||||||
                    </li>
 | 
					                    </li>
 | 
				
			||||||
                </ul>
 | 
					                </ul>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@@ -37,34 +37,51 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <!-- Toast container for notifications -->
 | 
					    <!-- Toast container for notifications -->
 | 
				
			||||||
    <div class="content-inner first-content">
 | 
					    <div class="content-inner first-content">
 | 
				
			||||||
 | 
					        <div class="inner">
 | 
				
			||||||
            <div id="toast-container"></div>
 | 
					            <div id="toast-container"></div>
 | 
				
			||||||
            <!-- Page content -->
 | 
					            <!-- Page content -->
 | 
				
			||||||
            {% block content %}{% endblock %}
 | 
					            {% block content %}{% endblock %}
 | 
				
			||||||
            <!-- Build success modal -->
 | 
					            <!-- Build success modal -->
 | 
				
			||||||
 | 
					            <div class="content-inner">
 | 
				
			||||||
                <div id="build-success-modal" class="modal" style="display:none;">
 | 
					                <div id="build-success-modal" class="modal" style="display:none;">
 | 
				
			||||||
                    <div class="modal-content">
 | 
					                    <div class="modal-content">
 | 
				
			||||||
                        <span id="build-success-modal-close" class="modal-close">×</span>
 | 
					                        <span id="build-success-modal-close" class="modal-close">×</span>
 | 
				
			||||||
                        <h3>✅ Build completed!</h3>
 | 
					                        <h3>✅ Build completed!</h3>
 | 
				
			||||||
                        <p>Your files are available in the output folder.</p>
 | 
					                        <p>Your files are available in the output folder.</p>
 | 
				
			||||||
                <button id="download-zip-btn" class="modal-btn">Download ZIP</button>
 | 
					                        <div class="modal-actions-row">
 | 
				
			||||||
 | 
					                            <button id="preview-site-btn" class="modal-btn" data-preview-port="{{ preview_port }}">🔎 Preview Site</button>
 | 
				
			||||||
 | 
					                            <button id="download-zip-btn" class="modal-btn">📦 Download ZIP</button>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
                        <div id="zip-loader" style="display:none;">Creating ZIP...</div>
 | 
					                        <div id="zip-loader" style="display:none;">Creating ZIP...</div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
        <!-- Footer -->
 | 
					        <!-- Footer -->
 | 
				
			||||||
        <div id="footer">
 | 
					        <div id="footer">
 | 
				
			||||||
            <div class="content-inner">
 | 
					            <div class="content-inner">
 | 
				
			||||||
 | 
					                <div class="inner">
 | 
				
			||||||
                    <div class="footer-container">
 | 
					                    <div class="footer-container">
 | 
				
			||||||
                        <div class="footer-credit">
 | 
					                        <div class="footer-credit">
 | 
				
			||||||
                            <p><a href="https//lumeex.djeex.fr"><span class="lum-first">Lum</span><span class="lum-second">eex</span> v{{ lumeex_version }}</a> — © 2025</p>
 | 
					                            <p><a href="https//lumeex.djeex.fr"><span class="lum-first">Lum</span><span class="lum-second">eex</span> v{{ lumeex_version }}</a> — © 2025</p>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                        <div class="footer-links">
 | 
					                        <div class="footer-links">
 | 
				
			||||||
                            <a class="footer-link documentation" href="https://lumeex.djeex.fr"><span class="icon"><img src="/img/favicon.svg"></span><span class="icon-text">Documentation</span></a>
 | 
					                            <a class="footer-link documentation" href="https://lumeex.djeex.fr"><span class="icon"><img src="/img/favicon.svg"></span><span class="icon-text">Documentation</span></a>
 | 
				
			||||||
                        <a class="footer-link gitea" href="https://gitea.com/Djeex/lumeex"><span class="icon"><img src="/img/gitea.svg"></span><span class="icon-text">Giteex</span></a>
 | 
					                            <a class="footer-link gitea" href="https://git.djeex.fr/Djeex/lumeex"><span class="icon"><img src="/img/gitea.svg"></span><span class="icon-text">Giteex</span></a>
 | 
				
			||||||
                            <a class="footer-link github" href="https://github.com/Djeex/lumeex"><span class="icon"><img src="/img/github.svg"></span><span class="icon-text">Github</span></a>
 | 
					                            <a class="footer-link github" href="https://github.com/Djeex/lumeex"><span class="icon"><img src="/img/github.svg"></span><span class="icon-text">Github</span></a>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <!-- Loader -->
 | 
				
			||||||
 | 
					        <div id="global-loader">
 | 
				
			||||||
 | 
					            <div class="loader-inner">
 | 
				
			||||||
 | 
					                <div class="loader-spinner"></div>
 | 
				
			||||||
 | 
					                <div id="loader-text">Uploading...</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
        <!-- Scripts -->
 | 
					        <!-- Scripts -->
 | 
				
			||||||
        <script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
 | 
					        <script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
 | 
				
			||||||
        {% block scripts %}{% endblock %}
 | 
					        {% block scripts %}{% endblock %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,121 +4,138 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 <h1>Edit Theme</h1>
 | 
					<h1>Edit Theme</h1>
 | 
				
			||||||
    <!-- Show current theme -->
 | 
					<!-- Show current theme -->
 | 
				
			||||||
    <div class="theme-info">
 | 
					<div class="theme-info">
 | 
				
			||||||
  <strong>Current theme:</strong> <span id="current-theme"></span>
 | 
					  <strong>Current theme:</strong> <span id="current-theme"></span>
 | 
				
			||||||
    </div>
 | 
					</div>
 | 
				
			||||||
    <form id="theme-editor-form">
 | 
					<div id="theme-editor-form">
 | 
				
			||||||
  <!-- Colors Section -->
 | 
					  <!-- Colors Section -->
 | 
				
			||||||
      <fieldset>
 | 
					  <form id="colors-form" autocomplete="off">
 | 
				
			||||||
 | 
					    <fieldset id="color-picker">
 | 
				
			||||||
      <h2>Colors</h2>
 | 
					      <h2>Colors</h2>
 | 
				
			||||||
 | 
					      <p class="section-status"></p>
 | 
				
			||||||
      <p>Set the color values for your theme</p>
 | 
					      <p>Set the color values for your theme</p>
 | 
				
			||||||
      <div class="fields">
 | 
					      <div class="fields">
 | 
				
			||||||
          <!-- Example for one color field, repeat for all -->
 | 
					 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Primary</label>
 | 
					          <label>Primary</label>
 | 
				
			||||||
          <div class="fields color-fields">
 | 
					          <div class="fields color-fields">
 | 
				
			||||||
            <button type="button" id="color-primary-btn" class="color-btn"></button>
 | 
					            <button type="button" id="color-primary-btn" class="color-btn"></button>
 | 
				
			||||||
              <input type="color" name="colors.primary" id="color-primary" class="color-input">
 | 
					            <input type="color" name="colors.primary" id="color-primary" class="color-input" required>
 | 
				
			||||||
              <input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;">
 | 
					            <input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Primary Dark</label>
 | 
					          <label>Primary Dark</label>
 | 
				
			||||||
          <div class="fields color-fields">
 | 
					          <div class="fields color-fields">
 | 
				
			||||||
            <button type="button" id="color-primary-dark-btn" class="color-btn"></button>
 | 
					            <button type="button" id="color-primary-dark-btn" class="color-btn"></button>
 | 
				
			||||||
              <input type="color" name="colors.primary_dark" id="color-primary-dark" class="color-input">
 | 
					            <input type="color" name="colors.primary_dark" id="color-primary-dark" class="color-input" required>
 | 
				
			||||||
              <input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;">
 | 
					            <input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Secondary</label>
 | 
					          <label>Secondary</label>
 | 
				
			||||||
          <div class="fields color-fields">
 | 
					          <div class="fields color-fields">
 | 
				
			||||||
            <button type="button" id="color-secondary-btn" class="color-btn"></button>
 | 
					            <button type="button" id="color-secondary-btn" class="color-btn"></button>
 | 
				
			||||||
              <input type="color" name="colors.secondary" id="color-secondary" class="color-input">
 | 
					            <input type="color" name="colors.secondary" id="color-secondary" class="color-input" required>
 | 
				
			||||||
              <input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;">
 | 
					            <input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Accent</label>
 | 
					          <label>Accent</label>
 | 
				
			||||||
          <div class="fields color-fields">
 | 
					          <div class="fields color-fields">
 | 
				
			||||||
            <button type="button" id="color-accent-btn" class="color-btn"></button>
 | 
					            <button type="button" id="color-accent-btn" class="color-btn"></button>
 | 
				
			||||||
              <input type="color" name="colors.accent" id="color-accent" class="color-input">
 | 
					            <input type="color" name="colors.accent" id="color-accent" class="color-input" required>
 | 
				
			||||||
              <input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;">
 | 
					            <input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Text Dark</label>
 | 
					          <label>Text Dark</label>
 | 
				
			||||||
          <div class="fields color-fields">
 | 
					          <div class="fields color-fields">
 | 
				
			||||||
            <button type="button" id="color-text-dark-btn" class="color-btn"></button>
 | 
					            <button type="button" id="color-text-dark-btn" class="color-btn"></button>
 | 
				
			||||||
              <input type="color" name="colors.text_dark" id="color-text-dark" class="color-input">
 | 
					            <input type="color" name="colors.text_dark" id="color-text-dark" class="color-input" required>
 | 
				
			||||||
              <input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;">
 | 
					            <input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Background</label>
 | 
					          <label>Background</label>
 | 
				
			||||||
          <div class="fields color-fields">
 | 
					          <div class="fields color-fields">
 | 
				
			||||||
            <button type="button" id="color-background-btn" class="color-btn"></button>
 | 
					            <button type="button" id="color-background-btn" class="color-btn"></button>
 | 
				
			||||||
              <input type="color" name="colors.background" id="color-background" class="color-input">
 | 
					            <input type="color" name="colors.background" id="color-background" class="color-input" required>
 | 
				
			||||||
              <input type="text" name="colors.background_text" id="color-background-text" style="width:100px;">
 | 
					            <input type="text" name="colors.background_text" id="color-background-text" style="width:100px;" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Browser Color</label>
 | 
					          <label>Browser Color</label>
 | 
				
			||||||
          <div class="fields color-fields">
 | 
					          <div class="fields color-fields">
 | 
				
			||||||
            <button type="button" id="color-browser-color-btn" class="color-btn"></button>
 | 
					            <button type="button" id="color-browser-color-btn" class="color-btn"></button>
 | 
				
			||||||
              <input type="color" name="colors.browser_color" id="color-browser-color" class="color-input">
 | 
					            <input type="color" name="colors.browser_color" id="color-browser-color" class="color-input" required>
 | 
				
			||||||
              <input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;">
 | 
					            <input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;" required>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <button type="submit" class="section-save-btn" data-section="colors">Save</button>
 | 
				
			||||||
    </fieldset>
 | 
					    </fieldset>
 | 
				
			||||||
 | 
					  </form>
 | 
				
			||||||
  <!-- Google Fonts Section -->
 | 
					  <!-- Google Fonts Section -->
 | 
				
			||||||
 | 
					  <form id="google-fonts-form" autocomplete="off">
 | 
				
			||||||
    <fieldset>
 | 
					    <fieldset>
 | 
				
			||||||
      <h2>Google Fonts</h2>
 | 
					      <h2>Google Fonts</h2>
 | 
				
			||||||
 | 
					      <p class="section-status"></p>
 | 
				
			||||||
      <p>Add Google Fonts to your theme</p>
 | 
					      <p>Add Google Fonts to your theme</p>
 | 
				
			||||||
      <div class="fields" id="google-fonts-fields">
 | 
					      <div class="fields" id="google-fonts-fields">
 | 
				
			||||||
        <!-- JS will render font family and weights inputs here -->
 | 
					        <!-- JS will render font family and weights inputs here -->
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <button type="button" id="add-google-font">Add Google Font</button>
 | 
					      <button type="button" id="add-google-font">Add Google Font</button>
 | 
				
			||||||
 | 
					      <button type="submit" class="section-save-btn" data-section="google-fonts">Save</button>
 | 
				
			||||||
    </fieldset>
 | 
					    </fieldset>
 | 
				
			||||||
 | 
					  </form>
 | 
				
			||||||
  <!-- Custom Font Upload Section -->
 | 
					  <!-- Custom Font Upload Section -->
 | 
				
			||||||
 | 
					  <form id="font-upload-form" autocomplete="off">
 | 
				
			||||||
    <fieldset>
 | 
					    <fieldset>
 | 
				
			||||||
      <h2>Upload Custom Font</h2>
 | 
					      <h2>Upload Custom Font</h2>
 | 
				
			||||||
 | 
					      <p class="section-status"></p>
 | 
				
			||||||
      <p>Supported formats: .woff, .woff2</p>
 | 
					      <p>Supported formats: .woff, .woff2</p>
 | 
				
			||||||
      <input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
 | 
					      <input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
 | 
				
			||||||
      <div id="local-fonts-list" class="font-list"></div>
 | 
					      <div id="local-fonts-list" class="font-list"></div>
 | 
				
			||||||
      <button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
 | 
					      <button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
 | 
				
			||||||
 | 
					      <!-- Save button not needed for upload-only section -->
 | 
				
			||||||
    </fieldset>
 | 
					    </fieldset>
 | 
				
			||||||
 | 
					  </form>
 | 
				
			||||||
  <!-- Fonts Section -->
 | 
					  <!-- Fonts Section -->
 | 
				
			||||||
 | 
					  <form id="fonts-form" autocomplete="off">
 | 
				
			||||||
    <fieldset>
 | 
					    <fieldset>
 | 
				
			||||||
      <h2>Fonts</h2>
 | 
					      <h2>Fonts</h2>
 | 
				
			||||||
 | 
					      <p class="section-status"></p>
 | 
				
			||||||
      <p>Select where to apply your fonts</p>
 | 
					      <p>Select where to apply your fonts</p>
 | 
				
			||||||
      <div class="fields">
 | 
					      <div class="fields">
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Primary Font</label>
 | 
					          <label>Primary Font</label>
 | 
				
			||||||
            <select name="fonts.primary.name" id="font-primary"></select>
 | 
					          <select name="fonts.primary.name" id="font-primary" required></select>
 | 
				
			||||||
          <label>Fallback</label>
 | 
					          <label>Fallback</label>
 | 
				
			||||||
            <select name="fonts.primary.fallback" id="font-primary-fallback">
 | 
					          <select name="fonts.primary.fallback" id="font-primary-fallback" required>
 | 
				
			||||||
            <option value="sans-serif">sans-serif</option>
 | 
					            <option value="sans-serif">sans-serif</option>
 | 
				
			||||||
            <option value="serif">serif</option>
 | 
					            <option value="serif">serif</option>
 | 
				
			||||||
          </select>
 | 
					          </select>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
          <label>Secondary Font</label>
 | 
					          <label>Secondary Font</label>
 | 
				
			||||||
            <select name="fonts.secondary.name" id="font-secondary"></select>
 | 
					          <select name="fonts.secondary.name" id="font-secondary" required></select>
 | 
				
			||||||
          <label>Fallback</label>
 | 
					          <label>Fallback</label>
 | 
				
			||||||
            <select name="fonts.secondary.fallback" id="font-secondary-fallback">
 | 
					          <select name="fonts.secondary.fallback" id="font-secondary-fallback" required>
 | 
				
			||||||
            <option value="sans-serif">sans-serif</option>
 | 
					            <option value="sans-serif">sans-serif</option>
 | 
				
			||||||
            <option value="serif">serif</option>
 | 
					            <option value="serif">serif</option>
 | 
				
			||||||
          </select>
 | 
					          </select>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <button type="submit" class="section-save-btn" data-section="fonts">Save</button>
 | 
				
			||||||
    </fieldset>
 | 
					    </fieldset>
 | 
				
			||||||
 | 
					  </form>
 | 
				
			||||||
  <!-- Favicon Section -->
 | 
					  <!-- Favicon Section -->
 | 
				
			||||||
 | 
					  <form id="favicon-form" autocomplete="off">
 | 
				
			||||||
    <fieldset>
 | 
					    <fieldset>
 | 
				
			||||||
      <h2>Favicon</h2>
 | 
					      <h2>Favicon</h2>
 | 
				
			||||||
 | 
					      <p class="section-status"></p>
 | 
				
			||||||
      <p>Supported formats: .png, .jpg, .jpeg</p>
 | 
					      <p>Supported formats: .png, .jpg, .jpeg</p>
 | 
				
			||||||
      <div class="fields">
 | 
					      <div class="fields">
 | 
				
			||||||
        <div class="input-field">
 | 
					        <div class="input-field">
 | 
				
			||||||
@@ -132,26 +149,26 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <button type="submit" class="section-save-btn" data-section="favicon">Save</button>
 | 
				
			||||||
    </fieldset>
 | 
					    </fieldset>
 | 
				
			||||||
      <button type="submit">Save Theme</button>
 | 
					 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
    <div class="section">
 | 
					</div>
 | 
				
			||||||
 | 
					<!-- Stepper -->
 | 
				
			||||||
 | 
					<div class="section">
 | 
				
			||||||
  <h2>Steps</h2>
 | 
					  <h2>Steps</h2>
 | 
				
			||||||
      <p> Follow the steps to generate your static gallery</p>
 | 
					  <p>Follow the steps to generate your static gallery</p>
 | 
				
			||||||
  <ul id="stepper">
 | 
					  <ul id="stepper">
 | 
				
			||||||
    <li><a href="/gallery-editor">Upload your photos</a></li>
 | 
					    <li><a href="/gallery-editor">Upload your photos</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					    <div></div>
 | 
				
			||||||
    <li><a href="/site-info">Configure site info</a></li>
 | 
					    <li><a href="/site-info">Configure site info</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					    <div></div>
 | 
				
			||||||
    <li><a class="step-active" href="/theme-editor">Customize your theme</a></li>
 | 
					    <li><a class="step-active" href="/theme-editor">Customize your theme</a></li>
 | 
				
			||||||
        <div>→</div>
 | 
					    <div></div>
 | 
				
			||||||
    <li><button id="stepper-build">Generate your static site!</button></li>
 | 
					    <li><button id="stepper-build">Generate your static site!</button></li>
 | 
				
			||||||
  </ul>
 | 
					  </ul>
 | 
				
			||||||
    </div>
 | 
					</div>
 | 
				
			||||||
  </div>
 | 
					<!-- Delete confirmation modal for favicon -->
 | 
				
			||||||
  </div>
 | 
					<div id="delete-favicon-modal" class="modal" style="display:none;">
 | 
				
			||||||
  <!-- Delete confirmation modal for favicon -->
 | 
					 | 
				
			||||||
  <div id="delete-favicon-modal" class="modal" style="display:none;">
 | 
					 | 
				
			||||||
  <div class="modal-content">
 | 
					  <div class="modal-content">
 | 
				
			||||||
    <span id="delete-favicon-modal-close" class="modal-close">×</span>
 | 
					    <span id="delete-favicon-modal-close" class="modal-close">×</span>
 | 
				
			||||||
    <h3>Confirm Deletion</h3>
 | 
					    <h3>Confirm Deletion</h3>
 | 
				
			||||||
@@ -161,9 +178,9 @@
 | 
				
			|||||||
      <button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
 | 
					      <button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  </div>
 | 
					</div>
 | 
				
			||||||
  <!-- Delete confirmation modal for font -->
 | 
					<!-- Delete confirmation modal for font -->
 | 
				
			||||||
  <div id="delete-font-modal" class="modal" style="display:none;">
 | 
					<div id="delete-font-modal" class="modal" style="display:none;">
 | 
				
			||||||
  <div class="modal-content">
 | 
					  <div class="modal-content">
 | 
				
			||||||
    <span id="delete-font-modal-close" class="modal-close">×</span>
 | 
					    <span id="delete-font-modal-close" class="modal-close">×</span>
 | 
				
			||||||
    <h3>Confirm Deletion</h3>
 | 
					    <h3>Confirm Deletion</h3>
 | 
				
			||||||
@@ -173,7 +190,7 @@
 | 
				
			|||||||
      <button id="delete-font-modal-cancel" class="modal-btn">Cancel</button>
 | 
					      <button id="delete-font-modal-cancel" class="modal-btn">Cancel</button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  </div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user