diff --git a/api/list_images.php b/api/list_images.php
new file mode 100644
index 0000000..d790a4e
--- /dev/null
+++ b/api/list_images.php
@@ -0,0 +1,33 @@
+ false, 'error' => 'img directory not found']);
+ exit;
+}
+
+$allowedExt = ['jpg','jpeg','png','gif','webp'];
+$images = [];
+
+$files = scandir($imgDir);
+if ($files === false) $files = [];
+
+foreach ($files as $f) {
+ if ($f === '.' || $f === '..') continue;
+ $path = $imgDir . DIRECTORY_SEPARATOR . $f;
+ if (!is_file($path)) continue;
+ $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
+ if (!in_array($ext, $allowedExt, true)) continue;
+ $images[] = $f;
+}
+
+// stable order
+natcasesort($images);
+$images = array_values($images);
+
+echo json_encode(['ok' => true, 'images' => $images], JSON_UNESCAPED_UNICODE);
+
+
diff --git a/api/upload_image.php b/api/upload_image.php
new file mode 100644
index 0000000..e8a95b3
--- /dev/null
+++ b/api/upload_image.php
@@ -0,0 +1,87 @@
+ false, 'error' => 'No file uploaded']);
+ exit;
+}
+
+$file = $_FILES['image'];
+if (!is_array($file) || !isset($file['error'])) {
+ http_response_code(400);
+ echo json_encode(['ok' => false, 'error' => 'Invalid upload']);
+ exit;
+}
+
+if ($file['error'] !== UPLOAD_ERR_OK) {
+ http_response_code(400);
+ echo json_encode(['ok' => false, 'error' => 'Upload error: ' . $file['error']]);
+ exit;
+}
+
+if (!isset($file['size']) || $file['size'] <= 0 || $file['size'] > $maxBytes) {
+ http_response_code(400);
+ echo json_encode(['ok' => false, 'error' => 'File too large (max 10MB)']);
+ exit;
+}
+
+// Validate image content
+$tmp = $file['tmp_name'];
+$info = @getimagesize($tmp);
+if ($info === false) {
+ http_response_code(400);
+ echo json_encode(['ok' => false, 'error' => 'Not a valid image']);
+ exit;
+}
+
+$mime = isset($info['mime']) ? strtolower($info['mime']) : '';
+$allowedMimeToExt = [
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/gif' => 'gif',
+ 'image/webp' => 'webp',
+];
+if (!isset($allowedMimeToExt[$mime])) {
+ http_response_code(400);
+ echo json_encode(['ok' => false, 'error' => 'Unsupported image type']);
+ exit;
+}
+
+$imgDir = realpath(__DIR__ . '/../img');
+if ($imgDir === false || !is_dir($imgDir)) {
+ http_response_code(500);
+ echo json_encode(['ok' => false, 'error' => 'img directory not found']);
+ exit;
+}
+
+// Generate safe unique filename
+$ext = $allowedMimeToExt[$mime];
+$base = 'upload_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4));
+$filename = $base . '.' . $ext;
+$dest = $imgDir . DIRECTORY_SEPARATOR . $filename;
+
+// Ensure we don't overwrite (extremely unlikely)
+$tries = 0;
+while (file_exists($dest) && $tries < 5) {
+ $filename = $base . '_' . bin2hex(random_bytes(2)) . '.' . $ext;
+ $dest = $imgDir . DIRECTORY_SEPARATOR . $filename;
+ $tries++;
+}
+
+if (!move_uploaded_file($tmp, $dest)) {
+ http_response_code(500);
+ echo json_encode(['ok' => false, 'error' => 'Failed to save file (check permissions)']);
+ exit;
+}
+
+// Conservative permissions
+@chmod($dest, 0644);
+
+echo json_encode(['ok' => true, 'filename' => $filename], JSON_UNESCAPED_UNICODE);
+
+
diff --git a/css/galleriffic-2.css b/css/galleriffic-2.css
index 81daa05..44f1aac 100644
--- a/css/galleriffic-2.css
+++ b/css/galleriffic-2.css
@@ -100,6 +100,84 @@ div.navigation {
/* The navigation style is set using jQuery so that the javascript specific styles won't be applied unless javascript is enabled. */
}
+/* Upload panel under thumbnails */
+.upload-panel {
+ margin-top: 14px;
+ padding-top: 12px;
+ border-top: 1px solid #e5e5e5;
+}
+.upload-title {
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 8px;
+}
+.dropzone {
+ position: relative;
+ border: 2px dashed #cfcfcf;
+ border-radius: 8px;
+ padding: 14px 12px;
+ background: #fafafa;
+ cursor: pointer;
+}
+.dropzone:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(46, 131, 255, 0.25);
+ border-color: #2e83ff;
+}
+.dropzone.dragover {
+ border-color: #2e83ff;
+ background: #f2f7ff;
+}
+.dropzone-text {
+ color: #666;
+ font-size: 12px;
+}
+.file-input {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+}
+.upload-preview {
+ margin-top: 10px;
+}
+.upload-preview img {
+ max-width: 100%;
+ max-height: 160px;
+ border: 1px solid #ddd;
+ background: #fff;
+ display: block;
+}
+.preview-meta {
+ font-size: 11px;
+ color: #666;
+ margin-top: 6px;
+ word-break: break-all;
+}
+.upload-actions {
+ margin-top: 10px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.upload-actions button {
+ padding: 7px 12px;
+ border-radius: 6px;
+ border: 1px solid #cfcfcf;
+ background: #fff;
+ cursor: pointer;
+}
+.upload-actions button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+.upload-status {
+ font-size: 12px;
+ color: #666;
+}
+
/* ---- Layout: slideshow (left) + thumbs (right), full width ---- */
.gallery-wrap {
display: flex;
diff --git a/index.html b/index.html
index 938a716..e126d04 100644
--- a/index.html
+++ b/index.html
@@ -13,6 +13,7 @@
+