From e5a88665cde414284c10cfa50910ccfc56df777e Mon Sep 17 00:00:00 2001
From: zuevav <34027267+zuevav@users.noreply.github.com>
Date: Thu, 30 Apr 2026 15:14:09 +0300
Subject: [PATCH] mailn
---
SYSTEM.md | 409 ++
api.php | 949 ++++
classes/Auth.php | 258 +
classes/FlickrAPI.php | 395 ++
classes/FlickrOAuth.php | 272 ++
classes/FlickrParser.php | 153 +
classes/FormatGenerator.php | 200 +
classes/TelegramBot.php | 283 ++
classes/VKAPI.php | 403 ++
config.example.php | 32 +
config.php | 32 +
cron_publish.php | 229 +
css/style.css | 4008 ++++++++++++++++
data/.htaccess | 1 +
debug.php | 579 +++
debug_urls.php | 111 +
image.png | Bin 0 -> 658920 bytes
js/app.js | 4210 +++++++++++++++++
setup.php | 113 +
test_single_download.php | 110 +
vh-flickr-mosaic.zip | Bin 0 -> 7289 bytes
widget_api.php | 246 +
.../vh-flickr-mosaic/assets/css/mosaic.css | 165 +
.../vh-flickr-mosaic/assets/js/mosaic.js | 235 +
.../vh-flickr-mosaic/vh-flickr-mosaic.php | 304 ++
25 files changed, 13697 insertions(+)
create mode 100644 SYSTEM.md
create mode 100644 api.php
create mode 100644 classes/Auth.php
create mode 100644 classes/FlickrAPI.php
create mode 100644 classes/FlickrOAuth.php
create mode 100644 classes/FlickrParser.php
create mode 100644 classes/FormatGenerator.php
create mode 100644 classes/TelegramBot.php
create mode 100644 classes/VKAPI.php
create mode 100644 config.example.php
create mode 100644 config.php
create mode 100644 cron_publish.php
create mode 100644 css/style.css
create mode 100644 data/.htaccess
create mode 100644 debug.php
create mode 100644 debug_urls.php
create mode 100644 image.png
create mode 100644 js/app.js
create mode 100644 setup.php
create mode 100644 test_single_download.php
create mode 100644 vh-flickr-mosaic.zip
create mode 100644 widget_api.php
create mode 100644 wordpress-plugin/vh-flickr-mosaic/assets/css/mosaic.css
create mode 100644 wordpress-plugin/vh-flickr-mosaic/assets/js/mosaic.js
create mode 100644 wordpress-plugin/vh-flickr-mosaic/vh-flickr-mosaic.php
diff --git a/SYSTEM.md b/SYSTEM.md
new file mode 100644
index 0000000..751221b
--- /dev/null
+++ b/SYSTEM.md
@@ -0,0 +1,409 @@
+# VH Posting System - Описание системы
+
+**Версия:** 1.0
+**Дата:** 2026-01-07
+**Совместимость:** PHP 7.2+
+**Хостинг:** reg.ru (shared hosting)
+
+---
+
+## Общее описание
+
+VH Posting System — веб-приложение для работы с изображениями Flickr. Позволяет конвертировать ссылки в различные форматы для публикации на форумах, блогах и социальных сетях, а также напрямую публиковать в Telegram-каналы.
+
+---
+
+## Структура проекта
+
+```
+VH_posting_system/
+├── index.php # Главная страница (требует авторизации)
+├── login.php # Страница входа
+├── setup.php # Первоначальная настройка (создание админа)
+├── api.php # API эндпоинты для AJAX-запросов
+├── config.php # Конфигурация (API ключи) - НЕ в git
+├── config.example.php # Пример конфигурации
+├── auth_config.php # Данные пользователей - НЕ в git, создаётся автоматически
+├── .htaccess # Настройки Apache
+├── .gitignore # Исключения для git
+│
+├── classes/ # PHP классы
+│ ├── Auth.php # Система авторизации
+│ ├── FlickrAPI.php # Клиент Flickr API
+│ ├── FlickrParser.php # Парсер ссылок Flickr
+│ ├── FormatGenerator.php # Генератор форматов вывода
+│ └── TelegramBot.php # Клиент Telegram Bot API
+│
+├── css/
+│ └── style.css # Стили интерфейса
+│
+├── js/
+│ └── app.js # Фронтенд логика
+│
+└── templates/ # (зарезервировано для шаблонов)
+```
+
+---
+
+## Функциональные модули
+
+### 1. Link Converter (Конвертер ссылок)
+
+**Назначение:** Преобразование ссылок Flickr в различные форматы.
+
+**Поддерживаемые входные форматы:**
+- Страница фото: `https://www.flickr.com/photos/username/12345678901/`
+- Короткая ссылка: `https://flic.kr/p/ABC123`
+- Прямая ссылка: `https://live.staticflickr.com/65535/12345678901_abcdef1234_b.jpg`
+
+**Форматы вывода:**
+
+| Ключ | Название | Шаблон | Применение |
+|------|----------|--------|------------|
+| `bbcode` | BBCode | `[img]{url}[/img]` | Форумы phpBB, vBulletin, IPB |
+| `bbcode_linked` | BBCode (clickable) | `[url={original}][img]{url}[/img][/url]` | Кликабельные превью |
+| `html` | HTML | `` | Веб-сайты |
+| `html_linked` | HTML (clickable) | `
',
+ 'separator' => "\n",
+ ],
+ 'html_linked' => [
+ 'name' => 'HTML (clickable)',
+ 'description' => 'HTML img with link to original',
+ 'template' => '
',
+ 'separator' => "\n",
+ ],
+ 'html_figure' => [
+ 'name' => 'HTML Figure',
+ 'description' => 'HTML5 figure with caption',
+ 'template' => "
\n
',
+ 'separator' => "\n\n",
+ 'category' => 'forum',
+ ],
+ 'babiki_simple' => [
+ 'name' => 'Babiki.ru (простой)',
+ 'description' => 'Только картинки для Бэбиков',
+ 'template' => '
',
+ 'separator' => "\n",
+ 'category' => 'forum',
+ ],
+ 'doll_forum' => [
+ 'name' => 'Кукольный форум',
+ 'description' => 'Универсальный BBCode для кукольных форумов',
+ 'template' => '[url={original}][img]{url}[/img][/url]',
+ 'separator' => "\n",
+ 'category' => 'forum',
+ ],
+ ];
+
+ /**
+ * Add a custom format
+ *
+ * @param string $key Format key
+ * @param string $name Display name
+ * @param string $template Template with placeholders
+ * @param string $separator Separator between multiple images
+ * @param string $description Format description
+ */
+ public function addFormat($key, $name, $template, $separator = "\n", $description = '')
+ {
+ $this->formats[$key] = [
+ 'name' => $name,
+ 'description' => $description,
+ 'template' => $template,
+ 'separator' => $separator,
+ ];
+ }
+
+ /**
+ * Get all available formats
+ *
+ * @return array Format definitions
+ */
+ public function getFormats()
+ {
+ return $this->formats;
+ }
+
+ /**
+ * Generate formatted output for a single image
+ *
+ * @param string $formatKey Format key
+ * @param array $imageData Image data with url, original, title
+ * @return string Formatted output
+ */
+ public function generate($formatKey, $imageData)
+ {
+ if (!isset($this->formats[$formatKey])) {
+ throw new InvalidArgumentException("Unknown format: {$formatKey}");
+ }
+
+ $template = $this->formats[$formatKey]['template'];
+
+ $replacements = [
+ '{url}' => isset($imageData['url']) ? $imageData['url'] : '',
+ '{original}' => isset($imageData['original']) ? $imageData['original'] : (isset($imageData['url']) ? $imageData['url'] : ''),
+ '{title}' => htmlspecialchars(isset($imageData['title']) ? $imageData['title'] : 'Image', ENT_QUOTES),
+ '{description}' => htmlspecialchars(isset($imageData['description']) ? $imageData['description'] : '', ENT_QUOTES),
+ '{photo_id}' => isset($imageData['photo_id']) ? $imageData['photo_id'] : '',
+ '{width}' => isset($imageData['width']) ? $imageData['width'] : '',
+ '{height}' => isset($imageData['height']) ? $imageData['height'] : '',
+ ];
+
+ return str_replace(array_keys($replacements), array_values($replacements), $template);
+ }
+
+ /**
+ * Generate formatted output for multiple images
+ *
+ * @param string $formatKey Format key
+ * @param array $images Array of image data
+ * @return string Formatted output
+ */
+ public function generateMultiple($formatKey, $images)
+ {
+ if (!isset($this->formats[$formatKey])) {
+ throw new InvalidArgumentException("Unknown format: {$formatKey}");
+ }
+
+ $separator = $this->formats[$formatKey]['separator'];
+ $outputs = [];
+
+ foreach ($images as $imageData) {
+ $outputs[] = $this->generate($formatKey, $imageData);
+ }
+
+ return implode($separator, $outputs);
+ }
+
+ /**
+ * Generate output in all formats at once
+ *
+ * @param array $images Array of image data
+ * @return array Keyed by format key
+ */
+ public function generateAll($images)
+ {
+ $result = [];
+
+ foreach (array_keys($this->formats) as $formatKey) {
+ $result[$formatKey] = [
+ 'name' => $this->formats[$formatKey]['name'],
+ 'description' => $this->formats[$formatKey]['description'],
+ 'output' => $this->generateMultiple($formatKey, $images),
+ ];
+ }
+
+ return $result;
+ }
+}
diff --git a/classes/TelegramBot.php b/classes/TelegramBot.php
new file mode 100644
index 0000000..41f0ea6
--- /dev/null
+++ b/classes/TelegramBot.php
@@ -0,0 +1,283 @@
+botToken = $botToken;
+ }
+
+ /**
+ * Set default channels for posting
+ *
+ * @param array $channels Array of channel usernames or IDs
+ */
+ public function setDefaultChannels($channels)
+ {
+ $this->defaultChannels = $channels;
+ }
+
+ /**
+ * Make API request to Telegram
+ *
+ * @param string $method API method
+ * @param array $params Parameters
+ * @return array Response
+ */
+ private function request($method, $params = [])
+ {
+ $url = $this->baseUrl . $this->botToken . '/' . $method;
+
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $params,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 60,
+ CURLOPT_SSL_VERIFYPEER => true,
+ ]);
+
+ $response = curl_exec($ch);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ if ($error) {
+ throw new RuntimeException("Telegram API error: {$error}");
+ }
+
+ $data = json_decode($response, true);
+
+ if (!$data['ok']) {
+ throw new RuntimeException("Telegram API error: " . (isset($data['description']) ? $data['description'] : 'Unknown error'));
+ }
+
+ return isset($data['result']) ? $data['result'] : [];
+ }
+
+ /**
+ * Send a text message
+ *
+ * @param string $chatId Chat/Channel ID or username
+ * @param string $text Message text
+ * @param string $parseMode Parse mode (HTML, Markdown, MarkdownV2)
+ * @param bool $disablePreview Disable link preview
+ * @return array Message info
+ */
+ public function sendMessage($chatId, $text, $parseMode = 'HTML', $disablePreview = false)
+ {
+ return $this->request('sendMessage', [
+ 'chat_id' => $chatId,
+ 'text' => $text,
+ 'parse_mode' => $parseMode,
+ 'disable_web_page_preview' => $disablePreview,
+ ]);
+ }
+
+ /**
+ * Send a single photo
+ *
+ * @param string $chatId Chat/Channel ID
+ * @param string $photo Photo URL or file_id
+ * @param string $caption Photo caption
+ * @param string $parseMode Parse mode for caption
+ * @return array Message info
+ */
+ public function sendPhoto($chatId, $photo, $caption = '', $parseMode = 'HTML')
+ {
+ $params = [
+ 'chat_id' => $chatId,
+ 'photo' => $photo,
+ ];
+
+ if ($caption) {
+ $params['caption'] = $caption;
+ $params['parse_mode'] = $parseMode;
+ }
+
+ return $this->request('sendPhoto', $params);
+ }
+
+ /**
+ * Send multiple photos as media group (album)
+ *
+ * @param string $chatId Chat/Channel ID
+ * @param array $photos Array of photo URLs
+ * @param string $caption Caption for first photo
+ * @param string $parseMode Parse mode
+ * @return array Messages info
+ */
+ public function sendMediaGroup($chatId, $photos, $caption = '', $parseMode = 'HTML')
+ {
+ $media = [];
+
+ foreach ($photos as $index => $photo) {
+ $item = [
+ 'type' => 'photo',
+ 'media' => $photo,
+ ];
+
+ // Caption only on first photo
+ if ($index === 0 && $caption) {
+ $item['caption'] = $caption;
+ $item['parse_mode'] = $parseMode;
+ }
+
+ $media[] = $item;
+ }
+
+ return $this->request('sendMediaGroup', [
+ 'chat_id' => $chatId,
+ 'media' => json_encode($media),
+ ]);
+ }
+
+ /**
+ * Post photos with text to a channel
+ * Smart method that chooses best approach based on content
+ *
+ * @param string $chatId Chat/Channel ID
+ * @param array $photos Array of photo URLs
+ * @param string $text Post text
+ * @param string $parseMode Parse mode
+ * @return array Result info
+ */
+ public function post($chatId, $photos, $text = '', $parseMode = 'HTML')
+ {
+ $photoCount = count($photos);
+
+ // Text only
+ if ($photoCount === 0) {
+ return ['type' => 'text', 'message' => $this->sendMessage($chatId, $text, $parseMode)];
+ }
+
+ // Single photo
+ if ($photoCount === 1) {
+ return ['type' => 'photo', 'message' => $this->sendPhoto($chatId, $photos[0], $text, $parseMode)];
+ }
+
+ // Multiple photos (2-10) - use media group
+ if ($photoCount <= 10) {
+ return ['type' => 'album', 'messages' => $this->sendMediaGroup($chatId, $photos, $text, $parseMode)];
+ }
+
+ // More than 10 photos - split into multiple albums
+ $results = [];
+ $chunks = array_chunk($photos, 10);
+
+ foreach ($chunks as $index => $chunk) {
+ $caption = ($index === 0) ? $text : '';
+ $results[] = $this->sendMediaGroup($chatId, $chunk, $caption, $parseMode);
+ }
+
+ return ['type' => 'multiple_albums', 'messages' => $results];
+ }
+
+ /**
+ * Post to multiple channels at once
+ *
+ * @param array $chatIds Array of Chat/Channel IDs
+ * @param array $photos Array of photo URLs
+ * @param string $text Post text
+ * @param string $parseMode Parse mode
+ * @return array Results for each channel
+ */
+ public function postToMultiple($chatIds, $photos, $text = '', $parseMode = 'HTML')
+ {
+ $results = [];
+
+ foreach ($chatIds as $chatId) {
+ try {
+ $results[$chatId] = [
+ 'success' => true,
+ 'result' => $this->post($chatId, $photos, $text, $parseMode),
+ ];
+ } catch (Exception $e) {
+ $results[$chatId] = [
+ 'success' => false,
+ 'error' => $e->getMessage(),
+ ];
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get bot info
+ *
+ * @return array Bot info
+ */
+ public function getMe()
+ {
+ return $this->request('getMe');
+ }
+
+ /**
+ * Get chat info
+ *
+ * @param string $chatId Chat/Channel ID
+ * @return array Chat info
+ */
+ public function getChat($chatId)
+ {
+ return $this->request('getChat', ['chat_id' => $chatId]);
+ }
+
+ /**
+ * Validate that bot has access to a channel
+ *
+ * @param string $chatId Channel ID or username
+ * @return array Validation result
+ */
+ public function validateChannel($chatId)
+ {
+ try {
+ $chat = $this->getChat($chatId);
+ return [
+ 'valid' => true,
+ 'title' => isset($chat['title']) ? $chat['title'] : (isset($chat['username']) ? $chat['username'] : $chatId),
+ 'type' => $chat['type'],
+ ];
+ } catch (Exception $e) {
+ return [
+ 'valid' => false,
+ 'error' => $e->getMessage(),
+ ];
+ }
+ }
+
+ /**
+ * Format text for Telegram HTML mode
+ *
+ * @param string $text Plain text
+ * @return string Escaped HTML
+ */
+ public static function escapeHtml($text)
+ {
+ return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ }
+
+ /**
+ * Format text for Telegram MarkdownV2 mode
+ *
+ * @param string $text Plain text
+ * @return string Escaped Markdown
+ */
+ public static function escapeMarkdown($text)
+ {
+ $chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
+ foreach ($chars as $char) {
+ $text = str_replace($char, '\\' . $char, $text);
+ }
+ return $text;
+ }
+}
diff --git a/classes/VKAPI.php b/classes/VKAPI.php
new file mode 100644
index 0000000..d6959a2
--- /dev/null
+++ b/classes/VKAPI.php
@@ -0,0 +1,403 @@
+accessToken = $accessToken;
+ }
+
+ /**
+ * Make API request to VK
+ *
+ * @param string $method API method
+ * @param array $params Parameters
+ * @return array Response
+ */
+ private function request($method, $params = [])
+ {
+ $params['access_token'] = $this->accessToken;
+ $params['v'] = $this->apiVersion;
+
+ $url = $this->baseUrl . $method;
+
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => http_build_query($params),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 60,
+ CURLOPT_SSL_VERIFYPEER => true,
+ ]);
+
+ $response = curl_exec($ch);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ if ($error) {
+ throw new RuntimeException("VK API connection error: {$error}");
+ }
+
+ $data = json_decode($response, true);
+
+ if (isset($data['error'])) {
+ $errorMsg = isset($data['error']['error_msg']) ? $data['error']['error_msg'] : 'Unknown error';
+ $errorCode = isset($data['error']['error_code']) ? $data['error']['error_code'] : 0;
+ throw new RuntimeException("VK API error [{$errorCode}]: {$errorMsg}");
+ }
+
+ return isset($data['response']) ? $data['response'] : [];
+ }
+
+ /**
+ * Get current user info
+ *
+ * @return array User info
+ */
+ public function getMe()
+ {
+ $result = $this->request('users.get', [
+ 'fields' => 'photo_100,screen_name'
+ ]);
+
+ if (!empty($result[0])) {
+ $this->userId = $result[0]['id'];
+ return $result[0];
+ }
+
+ return [];
+ }
+
+ /**
+ * Get groups where user can post
+ *
+ * @param int $count Number of groups to return
+ * @return array Groups list
+ */
+ public function getGroups($count = 100)
+ {
+ $result = $this->request('groups.get', [
+ 'extended' => 1,
+ 'filter' => 'admin,editor,moder',
+ 'fields' => 'name,screen_name,photo_100,can_post',
+ 'count' => $count
+ ]);
+
+ $groups = [];
+ if (isset($result['items'])) {
+ foreach ($result['items'] as $group) {
+ // Only groups where posting is allowed
+ if (!empty($group['can_post']) || isset($group['admin_level'])) {
+ $groups[] = [
+ 'id' => '-' . $group['id'], // Negative for group wall
+ 'name' => $group['name'],
+ 'screen_name' => isset($group['screen_name']) ? $group['screen_name'] : '',
+ 'photo' => isset($group['photo_100']) ? $group['photo_100'] : '',
+ ];
+ }
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Upload photo to VK from URL
+ *
+ * @param int $groupId Group ID (without minus, positive number)
+ * @param string $photoUrl Photo URL to upload
+ * @return string Attachment string (photo{owner}_{id})
+ */
+ public function uploadPhotoFromUrl($groupId, $photoUrl)
+ {
+ $groupId = abs((int)$groupId);
+
+ // Get upload server
+ try {
+ $uploadServer = $this->request('photos.getWallUploadServer', [
+ 'group_id' => $groupId
+ ]);
+ } catch (Exception $e) {
+ throw new RuntimeException('Не удалось получить сервер загрузки VK: ' . $e->getMessage());
+ }
+
+ if (!isset($uploadServer['upload_url'])) {
+ throw new RuntimeException('VK не вернул URL для загрузки фото');
+ }
+
+ // Download photo from URL with context for HTTPS
+ $context = stream_context_create([
+ 'http' => [
+ 'timeout' => 30,
+ 'user_agent' => 'VH-Posting-System/1.0'
+ ],
+ 'ssl' => [
+ 'verify_peer' => true,
+ 'verify_peer_name' => true
+ ]
+ ]);
+
+ $photoData = @file_get_contents($photoUrl, false, $context);
+ if ($photoData === false) {
+ throw new RuntimeException('Не удалось скачать фото с Flickr: ' . $photoUrl);
+ }
+
+ // Detect mime type
+ $finfo = new finfo(FILEINFO_MIME_TYPE);
+ $mimeType = $finfo->buffer($photoData) ?: 'image/jpeg';
+ $extension = $mimeType === 'image/png' ? 'png' : 'jpg';
+
+ // Save to temp file
+ $tempFile = tempnam(sys_get_temp_dir(), 'vk_photo_');
+ file_put_contents($tempFile, $photoData);
+
+ // Upload photo using CURLFile
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $uploadServer['upload_url'],
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => [
+ 'photo' => new CURLFile($tempFile, $mimeType, 'photo.' . $extension)
+ ],
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 120,
+ ]);
+
+ $uploadResponse = curl_exec($ch);
+ $curlError = curl_error($ch);
+ curl_close($ch);
+ unlink($tempFile);
+
+ if ($curlError) {
+ throw new RuntimeException('Ошибка загрузки фото на VK: ' . $curlError);
+ }
+
+ $uploadData = json_decode($uploadResponse, true);
+ if (!$uploadData) {
+ throw new RuntimeException('Неверный ответ от сервера VK при загрузке');
+ }
+
+ if (empty($uploadData['photo']) || $uploadData['photo'] === '[]') {
+ $errorMsg = isset($uploadData['error']) ? $uploadData['error'] : 'пустой ответ';
+ throw new RuntimeException('VK не принял фото: ' . $errorMsg);
+ }
+
+ // Save photo
+ try {
+ $savedPhoto = $this->request('photos.saveWallPhoto', [
+ 'group_id' => $groupId,
+ 'photo' => $uploadData['photo'],
+ 'server' => $uploadData['server'],
+ 'hash' => $uploadData['hash']
+ ]);
+ } catch (Exception $e) {
+ throw new RuntimeException('Не удалось сохранить фото в VK: ' . $e->getMessage());
+ }
+
+ if (empty($savedPhoto[0])) {
+ throw new RuntimeException('VK не вернул данные сохранённого фото');
+ }
+
+ $photo = $savedPhoto[0];
+ return 'photo' . $photo['owner_id'] . '_' . $photo['id'];
+ }
+
+ /**
+ * Post to wall/group
+ *
+ * @param int $ownerId User ID or Group ID (negative for groups)
+ * @param string $message Post text
+ * @param array $attachments Array of attachments
+ * @param bool $fromGroup Post from group name (only for groups)
+ * @return array Post info
+ */
+ public function wallPost($ownerId, $message = '', $attachments = [], $fromGroup = true)
+ {
+ $params = [
+ 'owner_id' => $ownerId,
+ 'message' => $message,
+ ];
+
+ if (!empty($attachments)) {
+ $params['attachments'] = implode(',', $attachments);
+ $params['primary_attachments_mode'] = 'grid';
+ }
+
+ // If posting to group, post from group name
+ if ($ownerId < 0 && $fromGroup) {
+ $params['from_group'] = 1;
+ }
+
+ return $this->request('wall.post', $params);
+ }
+
+ /**
+ * Post photos and text to a group
+ *
+ * @param int $groupId Group ID (will be converted to negative)
+ * @param array $photoUrls Array of photo URLs
+ * @param string $message Post text
+ * @return array Result
+ */
+ public function post($groupId, $photoUrls = [], $message = '')
+ {
+ $attachments = [];
+ $uploadErrors = [];
+ $permissionError = false;
+
+ // Make sure group ID is numeric
+ $numericGroupId = (int)$groupId;
+ if ($numericGroupId > 0) {
+ $numericGroupId = -$numericGroupId;
+ }
+
+ // Try to upload each photo
+ foreach ($photoUrls as $url) {
+ try {
+ $attachments[] = $this->uploadPhotoFromUrl(abs($numericGroupId), $url);
+ } catch (Exception $e) {
+ $errorMsg = $e->getMessage();
+ $uploadErrors[] = $errorMsg;
+ error_log("VK photo upload failed: " . $errorMsg);
+
+ // Check if it's a permission error (error 15 = Access denied, error 27 = group auth)
+ if (strpos($errorMsg, 'error [15]') !== false ||
+ strpos($errorMsg, 'error [27]') !== false ||
+ strpos($errorMsg, 'Access denied') !== false ||
+ strpos($errorMsg, 'group auth') !== false) {
+ $permissionError = true;
+ break; // Don't try other photos if it's a permission issue
+ }
+ }
+ }
+
+ // If permission error, try to post with photo links in text
+ if ($permissionError && !empty($photoUrls)) {
+ $photoLinks = "\n\n📷 Фото:\n" . implode("\n", $photoUrls);
+ $messageWithPhotos = $message . $photoLinks;
+
+ try {
+ $result = $this->wallPost($numericGroupId, $messageWithPhotos, []);
+ $result['warning'] = 'Фото добавлены как ссылки. Community-токен не поддерживает загрузку фото - нужен пользовательский токен.';
+ return $result;
+ } catch (Exception $e) {
+ throw new RuntimeException('Ошибка постинга: ' . $e->getMessage() . '. Также нет прав на загрузку фото.');
+ }
+ }
+
+ // If all photos failed to upload for non-permission reasons, report the first error
+ if (!empty($photoUrls) && empty($attachments) && !empty($uploadErrors)) {
+ throw new RuntimeException('Ошибка загрузки фото: ' . $uploadErrors[0]);
+ }
+
+ return $this->wallPost($numericGroupId, $message, $attachments);
+ }
+
+ /**
+ * Post to multiple groups at once
+ *
+ * @param array $groupIds Array of group IDs
+ * @param array $photoUrls Array of photo URLs
+ * @param string $message Post text
+ * @return array Results for each group
+ */
+ public function postToMultiple($groupIds, $photoUrls = [], $message = '')
+ {
+ $results = [];
+
+ foreach ($groupIds as $groupId) {
+ try {
+ $results[$groupId] = [
+ 'success' => true,
+ 'result' => $this->post($groupId, $photoUrls, $message),
+ ];
+ } catch (Exception $e) {
+ $results[$groupId] = [
+ 'success' => false,
+ 'error' => $e->getMessage(),
+ ];
+ }
+
+ // VK rate limit: max 3 requests per second
+ usleep(350000);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Validate access token (supports both user and community tokens)
+ *
+ * @return array Validation result
+ */
+ public function validateToken()
+ {
+ // First try user token validation
+ try {
+ $user = $this->getMe();
+ if (!empty($user)) {
+ return [
+ 'valid' => true,
+ 'type' => 'user',
+ 'user_id' => $user['id'],
+ 'user_name' => trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')),
+ 'screen_name' => $user['screen_name'] ?? '',
+ ];
+ }
+ } catch (Exception $e) {
+ // User token failed, try community token
+ }
+
+ // Try community token validation using groups.getById with group_id from token
+ try {
+ // For community tokens, we can get group info using groups.getById
+ // The token should have access to its own group
+ $result = $this->request('groups.getById', [
+ 'fields' => 'name,screen_name,photo_100'
+ ]);
+
+ if (!empty($result['groups'][0])) {
+ $group = $result['groups'][0];
+ return [
+ 'valid' => true,
+ 'type' => 'community',
+ 'user_id' => '-' . $group['id'],
+ 'user_name' => $group['name'] ?? 'Сообщество',
+ 'screen_name' => $group['screen_name'] ?? '',
+ ];
+ }
+ // VK API v5.199+ returns in different format
+ if (!empty($result[0])) {
+ $group = $result[0];
+ return [
+ 'valid' => true,
+ 'type' => 'community',
+ 'user_id' => '-' . $group['id'],
+ 'user_name' => $group['name'] ?? 'Сообщество',
+ 'screen_name' => $group['screen_name'] ?? '',
+ ];
+ }
+ } catch (Exception $e) {
+ return [
+ 'valid' => false,
+ 'error' => $e->getMessage(),
+ ];
+ }
+
+ return ['valid' => false, 'error' => 'Invalid token'];
+ }
+}
diff --git a/config.example.php b/config.example.php
new file mode 100644
index 0000000..cb0a15a
--- /dev/null
+++ b/config.example.php
@@ -0,0 +1,32 @@
+ [
+ 'api_key' => '733ecb91a48dce7be84410d49d96b57e',
+ 'api_secret' => 'c921aaf8b60c5603',
+ ],
+
+ // Your Flickr user ID (e.g., '12345678@N00')
+ // Find it at: https://www.flickr.com/services/api/explore/flickr.people.findByUsername
+ 'flickr_user_id' => '90307077@N07',
+
+ // Default image size for output
+ // Options: 'Square', 'Thumbnail', 'Small', 'Medium', 'Large', 'Original'
+ 'default_size' => 'Large',
+
+ // Available output formats
+ 'formats' => [
+ 'bbcode' => '[img]{url}[/img]',
+ 'bbcode_linked' => '[url={original}][img]{url}[/img][/url]',
+ 'html' => '
',
+ 'html_linked' => '
',
+ 'markdown' => '',
+ 'markdown_linked' => '[]({original})',
+ 'url' => '{url}',
+ ],
+];
diff --git a/config.php b/config.php
new file mode 100644
index 0000000..cb0a15a
--- /dev/null
+++ b/config.php
@@ -0,0 +1,32 @@
+ [
+ 'api_key' => '733ecb91a48dce7be84410d49d96b57e',
+ 'api_secret' => 'c921aaf8b60c5603',
+ ],
+
+ // Your Flickr user ID (e.g., '12345678@N00')
+ // Find it at: https://www.flickr.com/services/api/explore/flickr.people.findByUsername
+ 'flickr_user_id' => '90307077@N07',
+
+ // Default image size for output
+ // Options: 'Square', 'Thumbnail', 'Small', 'Medium', 'Large', 'Original'
+ 'default_size' => 'Large',
+
+ // Available output formats
+ 'formats' => [
+ 'bbcode' => '[img]{url}[/img]',
+ 'bbcode_linked' => '[url={original}][img]{url}[/img][/url]',
+ 'html' => '
',
+ 'html_linked' => '
',
+ 'markdown' => '',
+ 'markdown_linked' => '[]({original})',
+ 'url' => '{url}',
+ ],
+];
diff --git a/cron_publish.php b/cron_publish.php
new file mode 100644
index 0000000..bf81531
--- /dev/null
+++ b/cron_publish.php
@@ -0,0 +1,229 @@
+ $p['status'] === 'pending'));
+logMessage("Found $pendingCount pending posts");
+
+foreach ($posts as &$post) {
+ // Skip if not pending
+ if ($post['status'] !== 'pending') {
+ continue;
+ }
+
+ $scheduledTime = strtotime($post['scheduled_time']);
+ logMessage("Post {$post['id']}: scheduled for " . date('Y-m-d H:i:s', $scheduledTime) . " ($scheduledTime)");
+
+ if ($scheduledTime > $now) {
+ $diff = $scheduledTime - $now;
+ logMessage(" -> Not yet time (in $diff seconds)");
+ continue;
+ }
+
+ logMessage(" -> Time to publish!");
+
+ // Prepare text with tags
+ $baseText = $post['text'] ?? '';
+ $tags = $post['tags'] ?? [];
+ if (!empty($tags)) {
+ $tagsString = implode(' ', array_map(function($t) { return '#' . $t; }, $tags));
+ $baseText = $baseText ? $baseText . "\n\n" . $tagsString : $tagsString;
+ }
+
+ // Collect all photo URLs
+ $photoUrls = $post['photos'] ?? [];
+ $uploadedFiles = $post['uploaded_files'] ?? [];
+ foreach ($uploadedFiles as $file) {
+ if (!empty($file['url'])) {
+ $photoUrls[] = $file['url'];
+ }
+ }
+
+ logMessage(" Photos: " . count($photoUrls));
+
+ // Check if cross-promo was enabled for this post
+ $crossPromoEnabled = $post['cross_promo'] ?? false;
+ logMessage(" Cross-promo enabled: " . ($crossPromoEnabled ? 'yes' : 'no'));
+
+ // Load cross-promo settings if enabled
+ $crossPromoFile = __DIR__ . '/data/cross_promo.json';
+ $crossPromo = [];
+ if ($crossPromoEnabled && file_exists($crossPromoFile)) {
+ $crossPromo = json_decode(file_get_contents($crossPromoFile), true) ?: [];
+ }
+ $hasCrossPromo = $crossPromoEnabled && (!empty($crossPromo['telegramLink']) || !empty($crossPromo['vkLink']));
+
+ // Check which platforms we're posting to
+ $platforms = $post['platforms'] ?? [];
+ $postingToTelegram = false;
+ $postingToVk = false;
+ foreach ($platforms as $p) {
+ $pType = $p['type'] ?? $p;
+ if ($pType === 'telegram') $postingToTelegram = true;
+ if ($pType === 'vk') $postingToVk = true;
+ }
+
+ // Prepare platform-specific texts with cross-promo
+ $textForTelegram = $baseText;
+ $textForVk = $baseText;
+
+ if ($hasCrossPromo) {
+ // Add VK link to Telegram posts
+ if (!empty($crossPromo['vkLink']) && $postingToTelegram) {
+ $linkText = $crossPromo['textForTg'] ?? 'Мой канал ВКонтакте';
+ $textForTelegram .= "\n\n{$linkText}";
+ logMessage(" Cross-promo: VK link added to TG text");
+ }
+ // Add Telegram link to VK posts
+ if (!empty($crossPromo['telegramLink']) && $postingToVk) {
+ $linkText = $crossPromo['textForVk'] ?? 'Мой канал в Telegram';
+ $textForVk .= "\n\n{$linkText}: {$crossPromo['telegramLink']}";
+ logMessage(" Cross-promo: TG link added to VK text");
+ }
+ }
+
+ $results = [];
+
+ logMessage(" Platforms: " . json_encode($platforms));
+ logMessage(" TG token set: " . (!empty($config['telegram']['bot_token']) ? 'yes' : 'NO'));
+ logMessage(" VK token set: " . (!empty($config['vk']['access_token']) ? 'yes' : 'NO'));
+
+ if (empty($platforms)) {
+ logMessage(" WARNING: No platforms specified!");
+ }
+
+ foreach ($platforms as $platform) {
+ $type = $platform['type'] ?? $platform;
+ $target = $platform['target'] ?? '';
+
+ logMessage(" Processing platform: $type, target: $target");
+
+ if ($type === 'telegram') {
+ if (empty($config['telegram']['bot_token'])) {
+ logMessage(" Telegram: SKIPPED - no bot token in config");
+ continue;
+ }
+ try {
+ $telegram = new TelegramBot($config['telegram']['bot_token']);
+ // Get first channel if no target specified
+ if (empty($target)) {
+ $channels = $telegram->getChannels();
+ logMessage(" Telegram channels: " . count($channels));
+ if (!empty($channels)) {
+ $target = $channels[0]['id'];
+ }
+ }
+ if ($target) {
+ logMessage(" Posting to Telegram channel: $target");
+ $result = $telegram->post($target, $photoUrls, $textForTelegram, 'HTML');
+ $results['telegram'] = ['success' => true, 'result' => $result];
+ logMessage(" Telegram: OK");
+ } else {
+ $results['telegram'] = ['success' => false, 'error' => 'No target channel'];
+ logMessage(" Telegram: No target channel");
+ }
+ } catch (Exception $e) {
+ $results['telegram'] = ['success' => false, 'error' => $e->getMessage()];
+ logMessage(" Telegram: ERROR - {$e->getMessage()}");
+ }
+ }
+
+ if ($type === 'vk') {
+ if (empty($config['vk']['access_token'])) {
+ logMessage(" VK: SKIPPED - no access token in config");
+ continue;
+ }
+ try {
+ $vk = new VKAPI($config['vk']['access_token']);
+ // Get first group if no target specified
+ if (empty($target)) {
+ $validation = $vk->validateToken();
+ if ($validation['valid'] && ($validation['type'] ?? '') === 'community') {
+ $target = $validation['user_id'];
+ } else {
+ $groups = $vk->getGroups();
+ if (!empty($groups)) {
+ $target = $groups[0]['id'];
+ }
+ }
+ }
+ if ($target) {
+ logMessage(" Posting to VK group: $target");
+ $result = $vk->post($target, $photoUrls, strip_tags($textForVk));
+ $results['vk'] = ['success' => true, 'result' => $result];
+ logMessage(" VK: OK");
+ } else {
+ $results['vk'] = ['success' => false, 'error' => 'No target group'];
+ logMessage(" VK: No target group");
+ }
+ } catch (Exception $e) {
+ $results['vk'] = ['success' => false, 'error' => $e->getMessage()];
+ logMessage(" VK: ERROR - {$e->getMessage()}");
+ }
+ }
+ }
+
+ // Update post status
+ $post['status'] = 'published';
+ $post['published_at'] = date('Y-m-d H:i:s');
+ $post['results'] = $results;
+ $updated = true;
+ logMessage("Post {$post['id']} marked as published");
+}
+
+// Save updated posts
+if ($updated) {
+ file_put_contents($scheduledFile, json_encode($posts, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
+ logMessage("Posts file updated");
+}
+
+logMessage("Done\n---");
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..c6c3b06
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,4008 @@
+/* VH Posting System - Apple Liquid Glass Design */
+/* Русский интерфейс | Light & Dark Theme */
+
+/* ============ CSS Variables - Light Theme ============ */
+:root {
+ /* Core colors */
+ --accent-color: #007AFF;
+ --accent-hover: #0056CC;
+ --accent-light: rgba(0, 122, 255, 0.15);
+ --success-color: #34C759;
+ --error-color: #FF3B30;
+ --warning-color: #FF9500;
+
+ /* Backgrounds */
+ --bg-primary: #F2F2F7;
+ --bg-secondary: #FFFFFF;
+ --bg-tertiary: #E5E5EA;
+
+ /* Glass effect */
+ --glass-bg: rgba(255, 255, 255, 0.72);
+ --glass-bg-solid: rgba(255, 255, 255, 0.92);
+ --glass-border: rgba(255, 255, 255, 0.5);
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
+ --glass-shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.12);
+ --glass-blur: blur(20px);
+
+ /* Text */
+ --text-primary: #1C1C1E;
+ --text-secondary: #8E8E93;
+ --text-tertiary: #AEAEB2;
+ --text-inverse: #FFFFFF;
+
+ /* Borders */
+ --border-color: rgba(0, 0, 0, 0.08);
+ --border-light: rgba(0, 0, 0, 0.04);
+ --divider: rgba(60, 60, 67, 0.12);
+
+ /* Gradients */
+ --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ --gradient-accent: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
+ --gradient-bg: linear-gradient(180deg, #F2F2F7 0%, #E5E5EA 100%);
+
+ /* Radius */
+ --radius-sm: 8px;
+ --radius-md: 12px;
+ --radius-lg: 16px;
+ --radius-xl: 24px;
+
+ /* Transitions */
+ --transition-fast: 0.15s ease;
+ --transition-normal: 0.25s ease;
+ --transition-slow: 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+}
+
+/* ============ Dark Theme ============ */
+[data-theme="dark"] {
+ --bg-primary: #000000;
+ --bg-secondary: #1C1C1E;
+ --bg-tertiary: #2C2C2E;
+
+ --glass-bg: rgba(44, 44, 46, 0.72);
+ --glass-bg-solid: rgba(44, 44, 46, 0.92);
+ --glass-border: rgba(255, 255, 255, 0.1);
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+ --glass-shadow-hover: 0 12px 48px rgba(0, 0, 0, 0.4);
+
+ --text-primary: #FFFFFF;
+ --text-secondary: #8E8E93;
+ --text-tertiary: #636366;
+
+ --border-color: rgba(255, 255, 255, 0.1);
+ --border-light: rgba(255, 255, 255, 0.05);
+ --divider: rgba(84, 84, 88, 0.65);
+
+ --gradient-bg: linear-gradient(180deg, #1C1C1E 0%, #000000 100%);
+}
+
+/* ============ Base Styles ============ */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
+ background: var(--gradient-bg);
+ color: var(--text-primary);
+ line-height: 1.5;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transition: background var(--transition-normal), color var(--transition-normal);
+}
+
+/* ============ Glass Card Component ============ */
+.glass-card {
+ background: var(--glass-bg);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--glass-shadow);
+ transition: all var(--transition-normal);
+}
+
+.glass-card:hover {
+ box-shadow: var(--glass-shadow-hover);
+}
+
+.glass-card-solid {
+ background: var(--glass-bg-solid);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--glass-shadow);
+}
+
+/* ============ App Container ============ */
+.app-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+/* ============ Header ============ */
+.app-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 24px;
+ background: var(--glass-bg);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-xl);
+ box-shadow: var(--glass-shadow);
+ margin-bottom: 20px;
+ transition: all var(--transition-normal);
+}
+
+.app-header h1 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ background: var(--gradient-accent);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.user-menu {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.username {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+}
+
+/* Theme Toggle */
+.theme-toggle {
+ width: 44px;
+ height: 24px;
+ background: var(--bg-tertiary);
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ position: relative;
+ transition: all var(--transition-normal);
+}
+
+.theme-toggle::before {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ background: var(--bg-secondary);
+ border-radius: 50%;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ transition: all var(--transition-normal);
+}
+
+.theme-toggle.dark::before {
+ transform: translateX(20px);
+}
+
+.theme-toggle.dark {
+ background: var(--accent-color);
+}
+
+/* ============ Navigation ============ */
+.main-nav {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 20px;
+ padding: 8px;
+ background: var(--glass-bg);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-xl);
+ box-shadow: var(--glass-shadow);
+ flex-wrap: wrap;
+}
+
+.nav-btn {
+ flex: 1;
+ min-width: 120px;
+ padding: 12px 20px;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ font-size: 0.95rem;
+ font-weight: 500;
+ transition: all var(--transition-fast);
+}
+
+.nav-btn:hover {
+ background: var(--accent-light);
+ color: var(--accent-color);
+}
+
+.nav-btn.active {
+ background: var(--accent-color);
+ color: var(--text-inverse);
+ box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
+}
+
+/* ============ Panels ============ */
+.panel {
+ background: var(--glass-bg);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-xl);
+ padding: 28px;
+ box-shadow: var(--glass-shadow);
+ transition: all var(--transition-normal);
+}
+
+.panel h2 {
+ margin: 0 0 8px 0;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.panel h3 {
+ margin: 24px 0 16px 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+/* ============ Tabs ============ */
+.tab-content {
+ display: none;
+ animation: fadeIn var(--transition-slow);
+}
+
+.tab-content.active {
+ display: block;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ============ Forms ============ */
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+ color: var(--text-primary);
+ font-size: 0.95rem;
+}
+
+.form-row {
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+}
+
+.form-row .form-group {
+ flex: 1;
+ min-width: 200px;
+}
+
+input[type="text"],
+input[type="password"],
+input[type="email"],
+select,
+textarea {
+ width: 100%;
+ padding: 14px 18px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ font-size: 1rem;
+ font-family: inherit;
+ color: var(--text-primary);
+ transition: all var(--transition-fast);
+}
+
+input:focus,
+select:focus,
+textarea:focus {
+ outline: none;
+ border-color: var(--accent-color);
+ box-shadow: 0 0 0 4px var(--accent-light);
+}
+
+textarea {
+ resize: vertical;
+ min-height: 120px;
+ font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+select {
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238E8E93' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 16px center;
+ padding-right: 40px;
+}
+
+.hint {
+ display: block;
+ font-size: 0.85rem;
+ color: var(--text-tertiary);
+ margin-top: 6px;
+}
+
+.help-text {
+ color: var(--text-secondary);
+ margin-bottom: 20px;
+ font-size: 0.95rem;
+}
+
+/* ============ Buttons ============ */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 12px 24px;
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ text-decoration: none;
+}
+
+.btn-primary {
+ background: var(--accent-color);
+ color: var(--text-inverse);
+ box-shadow: 0 4px 12px rgba(0, 122, 255, 0.25);
+}
+
+.btn-primary:hover {
+ background: var(--accent-hover);
+ box-shadow: 0 6px 16px rgba(0, 122, 255, 0.35);
+ transform: translateY(-1px);
+}
+
+.btn-secondary {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.btn-secondary:hover {
+ background: var(--border-color);
+}
+
+.btn-accent {
+ background: linear-gradient(135deg, #FF9500 0%, #FF3B30 100%);
+ color: var(--text-inverse);
+ box-shadow: 0 4px 12px rgba(255, 149, 0, 0.25);
+}
+
+.btn-accent:hover {
+ box-shadow: 0 6px 16px rgba(255, 149, 0, 0.35);
+ transform: translateY(-1px);
+}
+
+.btn-success {
+ background: var(--success-color);
+ color: var(--text-inverse);
+}
+
+.btn-small {
+ padding: 8px 16px;
+ font-size: 0.875rem;
+}
+
+.btn-large {
+ padding: 16px 32px;
+ font-size: 1.1rem;
+ border-radius: var(--radius-lg);
+}
+
+.btn-block {
+ width: 100%;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+.btn:active:not(:disabled) {
+ transform: scale(0.98);
+}
+
+/* ============ Alerts ============ */
+.alert {
+ padding: 16px 20px;
+ border-radius: var(--radius-md);
+ margin-bottom: 20px;
+ font-size: 0.95rem;
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+}
+
+.alert-error {
+ background: rgba(255, 59, 48, 0.15);
+ color: var(--error-color);
+ border: 1px solid rgba(255, 59, 48, 0.3);
+}
+
+.alert-success {
+ background: rgba(52, 199, 89, 0.15);
+ color: var(--success-color);
+ border: 1px solid rgba(52, 199, 89, 0.3);
+}
+
+.alert-warning {
+ background: rgba(255, 149, 0, 0.15);
+ color: var(--warning-color);
+ border: 1px solid rgba(255, 149, 0, 0.3);
+}
+
+/* ============ Status Badges ============ */
+.status {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 14px;
+ border-radius: 20px;
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.status::before {
+ content: '';
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+}
+
+.status.connected {
+ background: rgba(52, 199, 89, 0.15);
+ color: var(--success-color);
+}
+
+.status.connected::before {
+ background: var(--success-color);
+ box-shadow: 0 0 8px var(--success-color);
+}
+
+.status.disconnected {
+ background: rgba(255, 59, 48, 0.15);
+ color: var(--error-color);
+}
+
+.status.disconnected::before {
+ background: var(--error-color);
+}
+
+/* ============ Login Page ============ */
+.login-page {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ background: var(--gradient-primary);
+ padding: 20px;
+}
+
+.login-container {
+ width: 100%;
+ max-width: 420px;
+}
+
+.login-box {
+ background: var(--glass-bg-solid);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-xl);
+ padding: 48px 40px;
+ box-shadow: var(--glass-shadow-hover);
+}
+
+.login-logo {
+ text-align: center;
+ margin-bottom: 24px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.login-logo-img {
+ width: 100px !important;
+ height: 100px !important;
+ max-width: 100px !important;
+ max-height: 100px !important;
+ object-fit: contain;
+ border-radius: var(--radius-lg);
+ display: block;
+}
+
+.login-box h1 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ text-align: center;
+ margin: 0 0 8px 0;
+ background: var(--gradient-accent);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.login-box h2 {
+ color: var(--text-secondary);
+ text-align: center;
+ font-weight: 400;
+ font-size: 1.1rem;
+ margin: 0 0 32px 0;
+}
+
+/* Login page mobile */
+@media (max-width: 480px) {
+ .login-page {
+ padding: 16px;
+ }
+
+ .login-box {
+ padding: 24px 20px;
+ }
+
+ .login-logo-img {
+ width: 80px !important;
+ height: 80px !important;
+ max-width: 80px !important;
+ max-height: 80px !important;
+ }
+
+ .login-box h1 {
+ font-size: 1.4rem;
+ }
+
+ .login-box h2 {
+ font-size: 1rem;
+ margin-bottom: 24px;
+ }
+}
+
+/* ============ Photo Gallery ============ */
+.gallery-controls {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+ margin-bottom: 20px;
+ align-items: flex-end;
+}
+
+.gallery-controls .form-group {
+ margin-bottom: 0;
+ flex: 1;
+ min-width: 150px;
+}
+
+.selection-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 20px;
+ background: var(--accent-light);
+ border: 1px solid rgba(0, 122, 255, 0.2);
+ border-radius: var(--radius-lg);
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+
+#selected-count {
+ font-weight: 600;
+ color: var(--accent-color);
+}
+
+.photo-gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 12px;
+ margin-bottom: 24px;
+}
+
+.photo-item {
+ position: relative;
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ cursor: pointer;
+ aspect-ratio: 1;
+ background: var(--bg-tertiary);
+ transition: all var(--transition-fast);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.photo-item:hover {
+ transform: scale(1.03);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+}
+
+.photo-item img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.photo-item .checkbox {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ width: 26px;
+ height: 26px;
+ background: var(--glass-bg-solid);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 2px solid var(--glass-border);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-fast);
+}
+
+.photo-item.selected .checkbox {
+ background: var(--accent-color);
+ border-color: var(--accent-color);
+ color: var(--text-inverse);
+ box-shadow: 0 2px 8px rgba(0, 122, 255, 0.4);
+}
+
+.photo-item.selected .checkbox::after {
+ content: '✓';
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.photo-item .title {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 10px 12px;
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
+ color: white;
+ font-size: 0.75rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.photo-preview-btn {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 32px;
+ height: 32px;
+ background: var(--glass-bg-solid);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 2px solid var(--glass-border);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 14px;
+ opacity: 0;
+ transition: all var(--transition-fast);
+ z-index: 5;
+}
+
+.photo-item:hover .photo-preview-btn {
+ opacity: 1;
+}
+
+.photo-preview-btn:hover {
+ background: var(--accent-color);
+ border-color: var(--accent-color);
+ transform: scale(1.1);
+}
+
+/* Video Badge */
+.photo-item.is-video {
+ position: relative;
+}
+
+.video-badge {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 48px;
+ height: 48px;
+ background: rgba(0, 0, 0, 0.7);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 3;
+ pointer-events: none;
+ transition: all 0.2s ease;
+}
+
+.photo-item:hover .video-badge {
+ background: rgba(234, 67, 53, 0.9);
+ transform: translate(-50%, -50%) scale(1.1);
+}
+
+.video-icon {
+ color: white;
+ font-size: 20px;
+ margin-left: 4px;
+}
+
+.photo-item.is-video .photo-preview-btn {
+ background: rgba(234, 67, 53, 0.9);
+ border-color: rgba(234, 67, 53, 0.9);
+}
+
+/* Pagination */
+.pagination {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+}
+
+#page-info {
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+/* Photos Preview */
+.photos-preview {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ padding: 20px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+ min-height: 100px;
+}
+
+.photos-preview .placeholder {
+ color: var(--text-tertiary);
+ width: 100%;
+ text-align: center;
+ padding: 24px;
+}
+
+.preview-thumb {
+ width: 72px;
+ height: 72px;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ position: relative;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.preview-thumb img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.preview-thumb .remove-btn {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ width: 22px;
+ height: 22px;
+ background: var(--error-color);
+ color: white;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: 14px;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity var(--transition-fast);
+}
+
+.preview-thumb:hover .remove-btn {
+ opacity: 1;
+}
+
+/* Result Message */
+.result-message {
+ padding: 16px 20px;
+ border-radius: var(--radius-md);
+ margin-top: 20px;
+ display: none;
+ animation: fadeIn var(--transition-normal);
+}
+
+.result-message.success {
+ display: block;
+ background: rgba(52, 199, 89, 0.15);
+ color: var(--success-color);
+ border: 1px solid rgba(52, 199, 89, 0.3);
+}
+
+.result-message.error {
+ display: block;
+ background: rgba(255, 59, 48, 0.15);
+ color: var(--error-color);
+ border: 1px solid rgba(255, 59, 48, 0.3);
+}
+
+/* ============ Settings ============ */
+.settings-section {
+ border-bottom: 1px solid var(--divider);
+ padding-bottom: 28px;
+ margin-bottom: 28px;
+}
+
+.settings-section:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+/* ============ Loading State ============ */
+.loading {
+ text-align: center;
+ padding: 48px;
+ color: var(--text-secondary);
+}
+
+.loading::after {
+ content: '';
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ border: 3px solid var(--border-color);
+ border-top-color: var(--accent-color);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin-left: 12px;
+ vertical-align: middle;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Placeholder */
+.placeholder {
+ color: var(--text-tertiary);
+ text-align: center;
+ padding: 24px;
+}
+
+/* ============ Platform Cards ============ */
+.platforms-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 16px;
+ margin-top: 12px;
+}
+
+.platform-card {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 16px;
+ transition: all var(--transition-fast);
+}
+
+.platform-card:hover {
+ border-color: var(--accent-color);
+ box-shadow: 0 4px 12px rgba(0, 122, 255, 0.1);
+}
+
+.platform-card.platform-disabled {
+ opacity: 0.6;
+}
+
+.platform-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.platform-checkbox {
+ position: relative;
+ display: flex;
+ cursor: pointer;
+}
+
+.platform-checkbox input {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.platform-checkbox .checkmark {
+ width: 24px;
+ height: 24px;
+ background: var(--bg-tertiary);
+ border: 2px solid var(--border-color);
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-fast);
+}
+
+.platform-checkbox input:checked + .checkmark {
+ background: var(--accent-color);
+ border-color: var(--accent-color);
+}
+
+.platform-checkbox input:checked + .checkmark::after {
+ content: '✓';
+ color: white;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.platform-info {
+ flex: 1;
+}
+
+.platform-name {
+ display: block;
+ font-weight: 600;
+ color: var(--text-primary);
+ font-size: 1rem;
+}
+
+.status-mini {
+ display: block;
+ font-size: 0.8rem;
+ color: var(--text-tertiary);
+ margin-top: 2px;
+}
+
+.status-mini.connected {
+ color: var(--success-color);
+}
+
+.platform-target {
+ width: 100%;
+ padding: 10px 14px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ font-size: 0.9rem;
+ color: var(--text-primary);
+}
+
+.platform-note {
+ font-size: 0.8rem;
+ color: var(--text-tertiary);
+ margin: 0;
+ padding: 8px 0 0 0;
+}
+
+/* ============ Gallery Header & Toolbar ============ */
+.gallery-header {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 20px;
+}
+
+.gallery-header h2 {
+ margin: 0;
+}
+
+.gallery-toolbar {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.album-controls {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 200px;
+}
+
+.toolbar-select {
+ flex: 1;
+ min-width: 150px;
+ max-width: 280px;
+}
+
+.btn-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border: none;
+ border-radius: var(--radius-sm);
+ background: var(--glass-bg);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ color: var(--text-primary);
+ font-size: 16px;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ flex-shrink: 0;
+}
+
+.btn-icon:hover {
+ background: var(--accent-light);
+ color: var(--accent-color);
+ transform: scale(1.05);
+}
+
+.btn-icon:active {
+ transform: scale(0.95);
+}
+
+.btn-icon.active {
+ background: var(--accent-color);
+ color: white;
+}
+
+.toolbar-search {
+ flex: 1;
+ min-width: 150px;
+ max-width: 250px;
+}
+
+.toolbar-search input {
+ width: 100%;
+ margin: 0;
+}
+
+/* ============ Gallery Views ============ */
+.gallery-view {
+ animation: fadeIn 0.3s ease;
+}
+
+.gallery-view.hidden {
+ display: none;
+}
+
+/* ============ Albums Grid ============ */
+.albums-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ gap: 16px;
+ padding: 8px 0;
+}
+
+@media (min-width: 768px) {
+ .albums-grid {
+ grid-template-columns: repeat(5, 1fr);
+ }
+}
+
+@media (min-width: 1024px) {
+ .albums-grid {
+ grid-template-columns: repeat(6, 1fr);
+ }
+}
+
+@media (min-width: 1400px) {
+ .albums-grid {
+ grid-template-columns: repeat(7, 1fr);
+ }
+}
+
+.album-card {
+ position: relative;
+ aspect-ratio: 1;
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ cursor: pointer;
+ background: var(--bg-tertiary);
+ box-shadow: var(--glass-shadow);
+ transition: all var(--transition-normal);
+ user-select: none;
+}
+
+.album-card:hover {
+ transform: translateY(-4px) scale(1.02);
+ box-shadow: var(--glass-shadow-hover);
+}
+
+.album-card.dragging {
+ opacity: 0.6;
+ transform: scale(1.05) rotate(2deg);
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ z-index: 100;
+}
+
+.album-card.drag-over {
+ transform: scale(0.95);
+ border: 3px solid var(--accent-color);
+}
+
+.album-card-cover {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform var(--transition-normal);
+}
+
+.album-card:hover .album-card-cover {
+ transform: scale(1.1);
+}
+
+.album-card-overlay {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 12px;
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.85));
+ color: white;
+}
+
+.album-card-title {
+ font-size: 0.85rem;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 2px;
+}
+
+.album-card-count {
+ font-size: 0.7rem;
+ opacity: 0.8;
+}
+
+.album-card-drag-handle {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 26px;
+ height: 26px;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(10px);
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 12px;
+ opacity: 0;
+ transition: opacity var(--transition-fast);
+ cursor: grab;
+}
+
+.album-card:hover .album-card-drag-handle {
+ opacity: 1;
+}
+
+.album-card-drag-handle:active {
+ cursor: grabbing;
+}
+
+/* Placeholder for empty cover */
+.album-card-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-tertiary);
+ color: var(--text-tertiary);
+ font-size: 2rem;
+}
+
+/* Drag hint */
+.drag-hint {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ text-align: center;
+ margin: 8px 0 16px;
+ opacity: 0.7;
+}
+
+.drag-hint.hidden {
+ display: none;
+}
+
+/* ============ Breadcrumb Navigation ============ */
+.breadcrumb {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.breadcrumb-separator {
+ color: var(--text-tertiary);
+}
+
+.breadcrumb-current {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.btn-text {
+ background: none;
+ border: none;
+ color: var(--accent-color);
+ font-size: 0.95rem;
+ cursor: pointer;
+ padding: 8px 12px;
+ border-radius: var(--radius-sm);
+ transition: all var(--transition-fast);
+}
+
+.btn-text:hover {
+ background: var(--accent-light);
+}
+
+.btn-icon-text {
+ margin-right: 6px;
+}
+
+.photos-count {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ white-space: nowrap;
+}
+
+/* ============ Loading Spinner ============ */
+.loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60px 20px;
+ gap: 16px;
+ color: var(--text-secondary);
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--border-color);
+ border-top-color: var(--accent-color);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(-10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ============ Floating Action Bar ============ */
+.floating-action-bar {
+ position: fixed;
+ bottom: 24px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 12px 20px;
+ background: var(--glass-bg-solid);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: 20px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+ z-index: 1000;
+ animation: slideUp 0.3s ease;
+}
+
+.floating-action-bar.hidden {
+ display: none;
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+.action-bar-left {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ padding-right: 16px;
+ border-right: 1px solid var(--divider);
+}
+
+.selection-count {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--accent-color);
+}
+
+.selection-label {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+
+.action-bar-center,
+.action-bar-right {
+ display: flex;
+ gap: 8px;
+}
+
+.action-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ padding: 8px 16px;
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ color: var(--text-primary);
+}
+
+.action-btn:hover {
+ background: var(--accent-light);
+}
+
+.action-btn:active {
+ transform: scale(0.95);
+}
+
+.action-icon {
+ font-size: 1.25rem;
+ line-height: 1;
+}
+
+.action-text {
+ font-size: 0.7rem;
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.action-btn.action-primary {
+ background: var(--accent-color);
+ color: white;
+ box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
+}
+
+.action-btn.action-primary:hover {
+ background: var(--accent-hover);
+}
+
+.action-btn.action-secondary {
+ background: var(--bg-tertiary);
+}
+
+.action-btn.action-secondary:hover {
+ background: var(--border-color);
+}
+
+/* Mobile floating bar */
+@media (max-width: 768px) {
+ .floating-action-bar {
+ left: 12px;
+ right: 12px;
+ transform: none;
+ bottom: 12px;
+ padding: 10px 16px;
+ gap: 8px;
+ }
+
+ .action-bar-left {
+ padding-right: 12px;
+ }
+
+ .selection-count {
+ font-size: 1.2rem;
+ }
+
+ .action-btn {
+ padding: 6px 10px;
+ }
+
+ .action-text {
+ display: none;
+ }
+
+ .action-icon {
+ font-size: 1.4rem;
+ }
+}
+
+/* ============ Scrollbar Styling ============ */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--text-tertiary);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-secondary);
+}
+
+/* ============ Selection ============ */
+::selection {
+ background: var(--accent-light);
+ color: var(--accent-color);
+}
+
+/* ============ Responsive ============ */
+@media (max-width: 768px) {
+ .app-container {
+ padding: 12px;
+ }
+
+ .app-header {
+ flex-direction: column;
+ text-align: center;
+ gap: 16px;
+ padding: 20px;
+ }
+
+ .main-nav {
+ padding: 6px;
+ }
+
+ .nav-btn {
+ min-width: auto;
+ padding: 10px 16px;
+ font-size: 0.9rem;
+ }
+
+ .panel {
+ padding: 20px;
+ }
+
+ .form-row {
+ flex-direction: column;
+ }
+
+ .form-row .form-group {
+ min-width: 100%;
+ }
+
+ .photo-gallery {
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 8px;
+ }
+
+ .selection-bar {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 10px;
+ }
+
+ .login-box {
+ padding: 32px 24px;
+ }
+}
+
+/* ============ Print Styles ============ */
+@media print {
+ .app-header,
+ .main-nav,
+ .btn,
+ .selection-bar,
+ .pagination {
+ display: none !important;
+ }
+
+ .panel {
+ box-shadow: none;
+ border: 1px solid #ccc;
+ }
+}
+
+/* ============ Photo Lightbox ============ */
+.lightbox {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.lightbox.hidden {
+ display: none;
+}
+
+.lightbox-backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.85);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+}
+
+.lightbox-content {
+ position: relative;
+ max-width: 90vw;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ z-index: 1;
+}
+
+.lightbox-close {
+ position: absolute;
+ top: -40px;
+ right: -40px;
+ width: 36px;
+ height: 36px;
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ border-radius: 50%;
+ color: white;
+ font-size: 24px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-fast);
+ z-index: 10;
+}
+
+.lightbox-close:hover {
+ background: rgba(255, 255, 255, 0.3);
+ transform: scale(1.1);
+}
+
+.lightbox-image-container {
+ position: relative;
+ max-width: 85vw;
+ max-height: 70vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.lightbox-image {
+ max-width: 100%;
+ max-height: 70vh;
+ object-fit: contain;
+ transition: opacity 0.3s ease;
+}
+
+.lightbox-video-container {
+ width: 100%;
+ max-width: 85vw;
+ aspect-ratio: 16 / 9;
+ background: #000;
+ border-radius: var(--radius-md);
+ overflow: hidden;
+}
+
+.lightbox-video {
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+/* Video Play Overlay in Lightbox */
+.video-play-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.5);
+ cursor: pointer;
+ transition: background 0.2s ease;
+ z-index: 10;
+}
+
+.video-play-overlay:hover {
+ background: rgba(0, 0, 0, 0.3);
+}
+
+.video-play-overlay:hover .video-play-button {
+ transform: scale(1.1);
+ background: rgba(234, 67, 53, 1);
+}
+
+.video-play-button {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: rgba(234, 67, 53, 0.9);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32px;
+ color: white;
+ padding-left: 6px;
+ transition: all 0.2s ease;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+}
+
+.video-play-text {
+ margin-top: 16px;
+ color: white;
+ font-size: 14px;
+ font-weight: 500;
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
+}
+
+.lightbox-loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.lightbox-loading.hidden {
+ display: none;
+}
+
+.lightbox-info {
+ margin-top: 16px;
+ text-align: center;
+}
+
+.lightbox-title {
+ color: white;
+ font-size: 1rem;
+ font-weight: 500;
+ margin: 0;
+ max-width: 60vw;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.lightbox-actions {
+ margin-top: 20px;
+ display: flex;
+ gap: 12px;
+}
+
+.lightbox-actions .btn {
+ min-width: 130px;
+}
+
+.lightbox-actions .btn-success {
+ background: var(--success-color);
+}
+
+.lightbox-nav {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 50px;
+ height: 50px;
+ background: rgba(255, 255, 255, 0.15);
+ border: none;
+ border-radius: 50%;
+ color: white;
+ font-size: 28px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-fast);
+}
+
+.lightbox-nav:hover {
+ background: rgba(255, 255, 255, 0.3);
+ transform: translateY(-50%) scale(1.1);
+}
+
+.lightbox-prev {
+ left: -80px;
+}
+
+.lightbox-next {
+ right: -80px;
+}
+
+/* Mobile lightbox adjustments */
+@media (max-width: 768px) {
+ .lightbox-close {
+ top: 10px;
+ right: 10px;
+ }
+
+ .lightbox-nav {
+ width: 40px;
+ height: 40px;
+ font-size: 22px;
+ }
+
+ .lightbox-prev {
+ left: 10px;
+ }
+
+ .lightbox-next {
+ right: 10px;
+ }
+
+ .lightbox-actions {
+ flex-direction: column;
+ width: 100%;
+ padding: 0 20px;
+ }
+
+ .lightbox-actions .btn {
+ width: 100%;
+ }
+
+ .lightbox-title {
+ max-width: 90vw;
+ font-size: 0.9rem;
+ }
+}
+
+/* ============ Download Choice Dialog ============ */
+.download-dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ animation: fadeIn 0.2s ease;
+}
+
+.download-dialog {
+ background: var(--glass-bg-solid);
+ backdrop-filter: var(--glass-blur);
+ -webkit-backdrop-filter: var(--glass-blur);
+ border-radius: var(--radius-xl);
+ padding: 32px;
+ min-width: 320px;
+ max-width: 90vw;
+ box-shadow: var(--glass-shadow-hover);
+ border: 1px solid var(--glass-border);
+ position: relative;
+ text-align: center;
+ animation: slideUp 0.3s ease;
+}
+
+.download-dialog h3 {
+ margin: 0 0 8px 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.download-dialog p {
+ margin: 0 0 24px 0;
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+}
+
+.download-dialog-buttons {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+}
+
+.download-dialog-buttons .btn {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 16px 24px;
+ min-width: 130px;
+}
+
+.download-dialog-buttons .btn-icon {
+ font-size: 1.5rem;
+}
+
+.download-dialog-close {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: var(--bg-tertiary);
+ border-radius: 50%;
+ font-size: 1.25rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: var(--transition-fast);
+}
+
+.download-dialog-close:hover {
+ background: var(--error-color);
+ color: white;
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* ============ OAuth Banner ============ */
+.oauth-banner {
+ background: linear-gradient(135deg, #FF9500 0%, #FF6B00 100%);
+ border-radius: var(--radius-md);
+ padding: 12px 16px;
+ margin-bottom: 16px;
+ animation: slideDown 0.3s ease;
+}
+
+.oauth-banner.hidden {
+ display: none;
+}
+
+.oauth-banner-content {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.oauth-banner-icon {
+ font-size: 1.25rem;
+}
+
+.oauth-banner-text {
+ flex: 1;
+ color: white;
+ font-size: 0.9rem;
+}
+
+.oauth-banner-text strong {
+ display: block;
+ font-size: 1rem;
+}
+
+.oauth-banner .btn {
+ background: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: white;
+ white-space: nowrap;
+}
+
+.oauth-banner .btn:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* ============ Text Editor Toolbar ============ */
+.text-editor {
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ background: var(--bg-secondary);
+}
+
+.editor-toolbar {
+ display: flex;
+ gap: 4px;
+ padding: 8px;
+ background: var(--bg-tertiary);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.toolbar-btn {
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: transparent;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ font-size: 14px;
+ color: var(--text-primary);
+ transition: var(--transition-fast);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.toolbar-btn:hover {
+ background: var(--accent-light);
+ color: var(--accent-color);
+}
+
+.toolbar-btn.active {
+ background: var(--accent-color);
+ color: white;
+}
+
+.toolbar-separator {
+ width: 1px;
+ background: var(--border-color);
+ margin: 0 4px;
+}
+
+.text-editor textarea {
+ border: none;
+ border-radius: 0;
+ resize: vertical;
+}
+
+.text-editor textarea:focus {
+ box-shadow: none;
+}
+
+/* ============ Tags System ============ */
+.tags-container {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 8px;
+ min-height: 44px;
+}
+
+.tags-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 8px;
+}
+
+.tags-list:empty {
+ display: none;
+}
+
+.tags-list + .tags-input-wrapper {
+ margin-top: 0;
+}
+
+.tag-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ background: var(--accent-light);
+ color: var(--accent-color);
+ border-radius: 20px;
+ font-size: 0.85rem;
+ animation: tagAppear 0.2s ease;
+}
+
+@keyframes tagAppear {
+ from { opacity: 0; transform: scale(0.8); }
+ to { opacity: 1; transform: scale(1); }
+}
+
+.tag-chip .tag-remove {
+ width: 16px;
+ height: 16px;
+ border: none;
+ background: transparent;
+ color: var(--accent-color);
+ cursor: pointer;
+ font-size: 14px;
+ line-height: 1;
+ padding: 0;
+ opacity: 0.6;
+ transition: var(--transition-fast);
+ border-radius: 50%;
+}
+
+.tag-chip .tag-remove:hover {
+ opacity: 1;
+ background: var(--accent-color);
+ color: white;
+}
+
+.tags-input-wrapper {
+ position: relative;
+}
+
+.tags-input {
+ border: none !important;
+ background: transparent !important;
+ padding: 4px 8px !important;
+ font-size: 0.9rem;
+ width: 100%;
+ min-width: 120px;
+}
+
+.tags-input:focus {
+ box-shadow: none !important;
+ outline: none;
+}
+
+.tags-suggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: var(--glass-shadow);
+ z-index: 100;
+ max-height: 200px;
+ overflow-y: auto;
+ display: none;
+}
+
+.tags-suggestions.visible {
+ display: block;
+}
+
+.tag-suggestion {
+ padding: 8px 12px;
+ cursor: pointer;
+ transition: var(--transition-fast);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.tag-suggestion:hover,
+.tag-suggestion.selected {
+ background: var(--accent-light);
+}
+
+.tag-suggestion .tag-count {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.tags-presets {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 8px;
+ flex-wrap: wrap;
+}
+
+.tags-presets-label {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
+
+.tag-preset {
+ padding: 4px 10px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-radius: 20px;
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: var(--transition-fast);
+ color: var(--text-primary);
+}
+
+.tag-preset:hover {
+ background: var(--accent-light);
+ border-color: var(--accent-color);
+ color: var(--accent-color);
+}
+
+.presets-list {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.preset-add-btn,
+.preset-manage-btn {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 1px dashed var(--border-color);
+ background: transparent;
+ color: var(--text-secondary);
+ font-size: 16px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.preset-add-btn:hover,
+.preset-manage-btn:hover {
+ border-color: var(--accent-color);
+ color: var(--accent-color);
+ background: var(--accent-light);
+}
+
+/* ============ Modal Styles ============ */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ backdrop-filter: blur(4px);
+}
+
+.modal-content {
+ background: var(--bg-primary);
+ border-radius: 12px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ max-height: 90vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: 1.1rem;
+ color: var(--text-primary);
+}
+
+.modal-close {
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: transparent;
+ font-size: 24px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.modal-close:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.modal-body {
+ padding: 20px;
+ overflow-y: auto;
+}
+
+/* Preset Modal */
+.preset-modal {
+ max-width: 500px;
+ width: 90%;
+}
+
+.preset-manager-list {
+ max-height: 300px;
+ overflow-y: auto;
+ margin-bottom: 16px;
+}
+
+.preset-manager-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ margin-bottom: 8px;
+ background: var(--bg-secondary);
+}
+
+.preset-manager-item:hover {
+ border-color: var(--accent-color);
+}
+
+.preset-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.preset-info .preset-name {
+ display: block;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.preset-info .preset-tags-preview {
+ display: block;
+ font-size: 12px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.preset-actions {
+ display: flex;
+ gap: 8px;
+ margin-left: 12px;
+}
+
+.btn-icon {
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ transition: background 0.2s ease;
+}
+
+.btn-icon:hover {
+ background: var(--bg-hover);
+}
+
+.btn-icon.preset-delete:hover {
+ background: rgba(239, 68, 68, 0.1);
+}
+
+.preset-form-actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 16px;
+}
+
+.btn-block {
+ width: 100%;
+}
+
+/* ============ Infinite Scroll Loading ============ */
+.albums-loading-more,
+.photos-loading-more {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 24px;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ grid-column: 1 / -1;
+ width: 100%;
+}
+
+.albums-loading-more .loading-spinner,
+.photos-loading-more .loading-spinner {
+ width: 24px;
+ height: 24px;
+}
+
+#albums-scroll-sentinel,
+#photos-scroll-sentinel {
+ visibility: hidden;
+}
+
+/* ============ Converter Grid Layout ============ */
+.converter-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+ margin-bottom: 24px;
+}
+
+.converter-input-section,
+.converter-text-section {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.converter-output-section {
+ border-top: 1px solid var(--border-color);
+ padding-top: 20px;
+}
+
+.output-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ margin-top: 8px;
+}
+
+.copy-status {
+ font-size: 0.85rem;
+ color: var(--success-color);
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+
+.copy-status.visible {
+ opacity: 1;
+}
+
+@media (max-width: 768px) {
+ .converter-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .tags-presets {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
+/* ============ Widget Settings ============ */
+.widget-albums-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 12px;
+ max-height: 400px;
+ overflow-y: auto;
+ padding: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+}
+
+.widget-album-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 12px;
+ border: 2px solid transparent;
+ border-radius: var(--radius-md);
+ background: var(--glass-bg);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-align: center;
+}
+
+.widget-album-item:hover {
+ background: var(--bg-hover);
+}
+
+.widget-album-item.selected {
+ border-color: var(--primary);
+ background: rgba(var(--primary-rgb), 0.1);
+}
+
+.widget-album-item input[type="checkbox"] {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.widget-album-thumb {
+ width: 80px;
+ height: 80px;
+ object-fit: cover;
+ border-radius: var(--radius-sm);
+ margin-bottom: 8px;
+}
+
+.widget-album-title {
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--text-primary);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ margin-bottom: 4px;
+}
+
+.widget-album-count {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.code-block {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 16px;
+ margin-bottom: 12px;
+}
+
+.code-block p {
+ margin: 0 0 8px 0;
+ color: var(--text-secondary);
+}
+
+.code-block code {
+ display: block;
+ background: var(--bg-tertiary);
+ padding: 8px 12px;
+ border-radius: var(--radius-sm);
+ font-family: 'SF Mono', monospace;
+ font-size: 0.85rem;
+ word-break: break-all;
+ color: var(--text-primary);
+}
+
+.save-status {
+ margin-left: 12px;
+ font-size: 0.9rem;
+}
+
+.save-status.success {
+ color: var(--success);
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+}
+
+.checkbox-label input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+}
+
+/* ============ Photo Source Buttons ============ */
+.photo-source-buttons {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 12px;
+ flex-wrap: wrap;
+}
+
+.photo-source-buttons .btn {
+ flex: 1;
+ min-width: 140px;
+}
+
+/* Combined preview for all photos */
+.combined-preview {
+ min-height: 80px;
+ border: 2px dashed transparent;
+ transition: all 0.2s ease;
+}
+
+.combined-preview.drag-over {
+ border-color: var(--accent-color);
+ background: var(--bg-tertiary);
+}
+
+.combined-preview .preview-thumb .source-badge {
+ position: absolute;
+ bottom: 2px;
+ left: 2px;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ font-size: 8px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ text-transform: uppercase;
+}
+
+.combined-preview .preview-thumb.uploading {
+ opacity: 0.6;
+}
+
+.combined-preview .preview-thumb .upload-spinner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--bg-primary);
+ border-top-color: var(--accent-color);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: translate(-50%, -50%) rotate(360deg); }
+}
+
+/* ============ File Upload Area ============ */
+.upload-area {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px;
+ border: 2px dashed var(--divider);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ transition: all 0.2s ease;
+}
+
+.upload-area.drag-over {
+ border-color: var(--accent-color);
+ background: var(--bg-tertiary);
+}
+
+.uploaded-preview {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.uploaded-item {
+ position: relative;
+ width: 80px;
+ height: 80px;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ background: var(--bg-tertiary);
+}
+
+.uploaded-item .preview-thumb {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.uploaded-item .remove-uploaded {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ width: 20px;
+ height: 20px;
+ border: none;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ font-size: 14px;
+ line-height: 1;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.uploaded-item .video-badge {
+ position: absolute;
+ bottom: 4px;
+ left: 4px;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 10px;
+}
+
+.uploaded-item .file-name {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ font-size: 9px;
+ padding: 2px 4px;
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.uploaded-item.uploading {
+ opacity: 0.7;
+}
+
+.uploaded-item .upload-spinner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 24px;
+ height: 24px;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-top-color: white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: translate(-50%, -50%) rotate(360deg); }
+}
+
+/* ============ Post Options Grid ============ */
+.post-options-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.checkbox-label.compact {
+ font-size: 0.9rem;
+}
+
+.schedule-options {
+ background: var(--bg-tertiary);
+ padding: 16px;
+ border-radius: var(--radius-md);
+ margin-bottom: 16px;
+ border: 2px solid var(--accent-color);
+}
+
+.schedule-options.hidden {
+ display: none;
+}
+
+.schedule-label {
+ display: block;
+ font-weight: 600;
+ margin-bottom: 12px;
+ color: var(--text-primary);
+}
+
+.schedule-presets {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.schedule-presets .preset-btn {
+ padding: 8px 14px;
+ border: 1px solid var(--divider);
+ border-radius: var(--radius-md);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.schedule-presets .preset-btn:hover {
+ background: var(--accent-light);
+ border-color: var(--accent-color);
+}
+
+.schedule-presets .preset-btn.active {
+ background: var(--accent-color);
+ color: white;
+ border-color: var(--accent-color);
+}
+
+.schedule-custom {
+ background: var(--bg-secondary);
+ padding: 12px;
+ border-radius: var(--radius-sm);
+}
+
+.schedule-date-row {
+ display: flex;
+ gap: 12px;
+}
+
+.schedule-field {
+ flex: 1;
+}
+
+.schedule-field label {
+ display: block;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-bottom: 4px;
+}
+
+.schedule-input {
+ width: 100%;
+ padding: 10px 12px;
+ font-size: 16px;
+ border: 1px solid var(--divider);
+ border-radius: var(--radius-sm);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+.schedule-input:focus {
+ outline: none;
+ border-color: var(--accent-color);
+}
+
+.datetime-input {
+ width: 100%;
+ padding: 12px;
+ font-size: 1rem;
+ border: 1px solid var(--divider);
+ border-radius: var(--radius-md);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+.post-actions {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+.post-actions .btn {
+ flex: 1;
+}
+
+/* ============ Scheduled Posts Section ============ */
+.scheduled-section {
+ margin-top: 32px;
+ padding-top: 24px;
+ border-top: 1px solid var(--divider);
+}
+
+.scheduled-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.scheduled-header h3 {
+ margin: 0;
+ font-size: 1.1rem;
+}
+
+.badge {
+ background: var(--accent-color);
+ color: white;
+ padding: 2px 10px;
+ border-radius: 12px;
+ font-size: 0.85rem;
+ font-weight: 600;
+}
+
+.scheduled-posts-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.scheduled-post-card {
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ padding: 16px;
+ border: 1px solid var(--divider);
+}
+
+.scheduled-post-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.scheduled-time {
+ font-weight: 600;
+ color: var(--accent-color);
+ font-size: 0.9rem;
+}
+
+.scheduled-platforms {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ background: var(--bg-tertiary);
+ padding: 3px 8px;
+ border-radius: 4px;
+ font-weight: 600;
+}
+
+/* Photo previews in scheduled posts */
+.scheduled-photos-preview {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 10px;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.scheduled-thumb {
+ width: 60px;
+ height: 60px;
+ object-fit: cover;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--divider);
+}
+
+.more-photos {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 60px;
+ height: 60px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.scheduled-post-content {
+ margin-bottom: 10px;
+}
+
+.scheduled-text {
+ margin: 0 0 8px 0;
+ color: var(--text-primary);
+}
+
+.scheduled-photos,
+.scheduled-tags {
+ display: inline-block;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-right: 12px;
+}
+
+.scheduled-post-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.scheduled-platforms label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+/* Inline editor for scheduled posts */
+.scheduled-post-card.editing {
+ border-color: var(--accent-color);
+ box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb, 0,122,255), 0.15);
+}
+
+.inline-editor {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.inline-editor-photos {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ flex-direction: row;
+}
+
+.inline-editor-photos .preview-thumb {
+ width: 80px;
+ height: 80px;
+ position: relative;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.inline-editor-photos .preview-thumb img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.inline-editor-photos .remove-btn {
+ position: absolute;
+ top: 3px;
+ right: 3px;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: rgba(220,40,40,0.85);
+ color: white;
+ border: none;
+ cursor: pointer;
+ font-size: 12px;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.inline-editor-text {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid var(--divider);
+ border-radius: var(--radius-sm);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ font-family: inherit;
+ font-size: 0.95rem;
+ resize: vertical;
+ box-sizing: border-box;
+}
+
+.inline-editor-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+}
+
+.inline-tags-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.inline-tags-input {
+ flex: 1;
+ min-width: 100px;
+ padding: 4px 8px;
+ border: 1px solid var(--divider);
+ border-radius: var(--radius-sm);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ font-size: 0.85rem;
+}
+
+.inline-editor-row {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.inline-editor-field {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.inline-editor-field label {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ white-space: nowrap;
+}
+
+.inline-editor-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.btn-accent {
+ background: var(--success, #34c759);
+ color: white;
+}
+
+.btn-accent:hover {
+ opacity: 0.9;
+}
+
+.btn-danger {
+ background: var(--error);
+ color: white;
+}
+
+.btn-danger:hover {
+ background: #c0392b;
+}
+
+/* Photo counter on posting page */
+.photo-counter {
+ font-weight: 600;
+ font-size: 0.9em;
+ padding: 2px 8px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+}
+
+.photo-counter.at-limit {
+ background: var(--error);
+ color: white;
+}
+
+/* ============ Published Posts Archive ============ */
+.archive-section {
+ margin-top: 24px;
+ padding-top: 20px;
+ border-top: 1px solid var(--divider);
+}
+
+.archive-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.archive-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ color: var(--text-secondary);
+}
+
+.published-posts-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.archive-post-card {
+ background: var(--bg-secondary);
+ border-radius: var(--radius-sm);
+ padding: 10px 12px;
+ border: 1px solid var(--divider);
+ opacity: 0.85;
+}
+
+.archive-post-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 6px;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.archive-time {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
+
+.archive-results {
+ font-size: 0.75rem;
+ display: flex;
+ gap: 8px;
+}
+
+.archive-results .result-success {
+ color: var(--success-color);
+}
+
+.archive-results .result-error {
+ color: var(--error-color);
+}
+
+.archive-photos-preview {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 6px;
+ align-items: center;
+}
+
+.archive-thumb {
+ width: 40px;
+ height: 40px;
+ object-fit: cover;
+ border-radius: 4px;
+ border: 1px solid var(--divider);
+}
+
+.archive-text {
+ margin: 0;
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ line-height: 1.3;
+}
+
+/* ============ Reduced Motion ============ */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* ============ Mobile Responsive Styles ============ */
+
+/* Tablet and smaller */
+@media (max-width: 1024px) {
+ .converter-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .platforms-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+/* Mobile devices */
+@media (max-width: 768px) {
+ .app-container {
+ padding: 8px;
+ }
+
+ /* Header - compact horizontal layout */
+ .app-header {
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding: 10px 12px;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .app-header h1 {
+ font-size: 1.1rem;
+ flex: 1;
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .user-menu {
+ gap: 6px;
+ flex-shrink: 0;
+ }
+
+ .user-menu .username {
+ display: none;
+ }
+
+ .user-menu .btn {
+ padding: 6px 10px;
+ font-size: 0.8rem;
+ }
+
+ .theme-toggle {
+ width: 36px;
+ height: 20px;
+ }
+
+ .theme-toggle::before {
+ width: 16px;
+ height: 16px;
+ }
+
+ .theme-toggle.dark::before {
+ transform: translateX(16px);
+ }
+
+ /* Navigation - grid layout for mobile */
+ .main-nav {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 4px;
+ padding: 6px;
+ margin-bottom: 12px;
+ }
+
+ .nav-btn {
+ padding: 10px 6px;
+ font-size: 0.75rem;
+ text-align: center;
+ min-width: 0;
+ flex: none;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ /* Make settings button span 2 columns for 5 items grid */
+ .nav-btn:nth-child(4) {
+ grid-column: 1 / 2;
+ }
+ .nav-btn:nth-child(5) {
+ grid-column: 2 / 4;
+ }
+
+ .panel {
+ padding: 12px;
+ border-radius: var(--radius-md);
+ }
+
+ .panel h2 {
+ font-size: 1.1rem;
+ margin-bottom: 12px;
+ }
+
+ .panel h3 {
+ font-size: 1rem;
+ margin: 16px 0 10px;
+ }
+
+ .help-text {
+ font-size: 0.85rem;
+ margin-bottom: 12px;
+ }
+
+ /* Post options grid - horizontal on mobile */
+ .post-options-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .post-options-grid .form-group {
+ margin-bottom: 0;
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+
+ .post-options-grid .form-group:first-child {
+ flex: 1 1 100%;
+ }
+
+ .checkbox-label.compact {
+ padding: 10px 12px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.85rem;
+ white-space: nowrap;
+ }
+
+ /* Platforms grid - single column on mobile */
+ .platforms-grid {
+ grid-template-columns: 1fr;
+ gap: 8px;
+ }
+
+ .platform-card {
+ padding: 10px;
+ }
+
+ .platform-header {
+ margin-bottom: 8px;
+ }
+
+ .platform-name {
+ font-size: 0.9rem;
+ }
+
+ .platform-target {
+ padding: 8px 10px;
+ font-size: 0.85rem;
+ }
+
+ /* Schedule options */
+ .schedule-options {
+ padding: 10px;
+ margin-bottom: 12px;
+ }
+
+ .schedule-label {
+ font-size: 0.9rem;
+ margin-bottom: 8px;
+ }
+
+ .schedule-presets {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 6px;
+ margin-bottom: 12px;
+ }
+
+ .schedule-presets .preset-btn {
+ padding: 10px 8px;
+ font-size: 0.75rem;
+ text-align: center;
+ }
+
+ .schedule-date-row {
+ flex-direction: row;
+ gap: 8px;
+ }
+
+ .schedule-field {
+ flex: 1;
+ }
+
+ .schedule-field label {
+ font-size: 0.75rem;
+ }
+
+ .schedule-input {
+ font-size: 16px;
+ padding: 8px 10px;
+ }
+
+ .schedule-custom {
+ padding: 10px;
+ }
+
+ /* Scheduled posts */
+ .scheduled-section {
+ margin-top: 20px;
+ padding-top: 16px;
+ }
+
+ .scheduled-header {
+ margin-bottom: 10px;
+ }
+
+ .scheduled-header h3 {
+ font-size: 0.95rem;
+ margin: 0;
+ }
+
+ .badge {
+ font-size: 0.75rem;
+ padding: 2px 8px;
+ }
+
+ .scheduled-post-card {
+ padding: 10px;
+ }
+
+ .scheduled-post-header {
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ }
+
+ .scheduled-time {
+ font-size: 0.8rem;
+ }
+
+ .scheduled-platforms {
+ font-size: 0.65rem;
+ padding: 2px 6px;
+ }
+
+ .scheduled-photos-preview {
+ gap: 4px;
+ margin-bottom: 8px;
+ }
+
+ .scheduled-thumb,
+ .more-photos {
+ width: 44px;
+ height: 44px;
+ }
+
+ .more-photos {
+ font-size: 0.75rem;
+ }
+
+ .scheduled-text {
+ font-size: 0.85rem;
+ margin-bottom: 6px;
+ }
+
+ .scheduled-photos,
+ .scheduled-tags {
+ font-size: 0.75rem;
+ }
+
+ .scheduled-post-actions {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 6px;
+ }
+
+ .scheduled-post-actions .btn {
+ padding: 8px 4px;
+ font-size: 0.75rem;
+ justify-content: center;
+ }
+
+ /* Archive section */
+ .archive-section {
+ margin-top: 16px;
+ padding-top: 14px;
+ }
+
+ .archive-header h3 {
+ font-size: 0.9rem;
+ }
+
+ .archive-post-card {
+ padding: 8px 10px;
+ }
+
+ .archive-thumb {
+ width: 32px;
+ height: 32px;
+ }
+
+ .archive-text {
+ font-size: 0.8rem;
+ }
+
+ /* Photos preview */
+ .photos-preview {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ padding: 12px;
+ min-height: 80px;
+ }
+
+ .preview-thumb {
+ width: 56px;
+ height: 56px;
+ }
+
+ /* Upload area */
+ .upload-area {
+ flex-direction: column;
+ padding: 12px;
+ gap: 8px;
+ text-align: center;
+ }
+
+ .upload-area .hint {
+ font-size: 0.75rem;
+ }
+
+ .uploaded-preview {
+ margin-top: 6px;
+ }
+
+ .uploaded-item {
+ width: 60px;
+ height: 60px;
+ }
+
+ /* Text editor */
+ .text-editor textarea {
+ min-height: 80px;
+ padding: 10px;
+ font-size: 16px;
+ }
+
+ .editor-toolbar {
+ padding: 6px;
+ gap: 2px;
+ }
+
+ .toolbar-btn {
+ width: 28px;
+ height: 28px;
+ font-size: 12px;
+ }
+
+ .toolbar-separator {
+ margin: 0 2px;
+ }
+
+ /* Tags */
+ .tags-container {
+ padding: 6px;
+ }
+
+ .tag-chip {
+ font-size: 0.75rem;
+ padding: 3px 6px;
+ }
+
+ .tags-input {
+ font-size: 16px !important;
+ padding: 6px !important;
+ }
+
+ .tags-presets {
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 6px;
+ }
+
+ .tags-presets-label {
+ font-size: 0.7rem;
+ width: 100%;
+ }
+
+ .tag-preset {
+ padding: 4px 8px;
+ font-size: 0.7rem;
+ }
+
+ .preset-add-btn,
+ .preset-manage-btn {
+ width: 24px;
+ height: 24px;
+ font-size: 14px;
+ }
+
+ /* Form elements */
+ .form-group {
+ margin-bottom: 12px;
+ }
+
+ .form-group label {
+ font-size: 0.85rem;
+ margin-bottom: 4px;
+ }
+
+ input[type="text"],
+ input[type="password"],
+ textarea,
+ select {
+ font-size: 16px;
+ padding: 10px 12px;
+ }
+
+ .hint {
+ font-size: 0.75rem;
+ margin-top: 4px;
+ }
+
+ /* Buttons */
+ .btn {
+ padding: 10px 16px;
+ font-size: 0.9rem;
+ }
+
+ .btn-small {
+ padding: 6px 10px;
+ font-size: 0.8rem;
+ }
+
+ .btn-large {
+ padding: 14px 20px;
+ font-size: 0.95rem;
+ }
+
+ .post-actions {
+ gap: 8px;
+ margin-bottom: 16px;
+ }
+
+ .post-actions .btn {
+ flex: 1;
+ }
+
+ /* Gallery */
+ .gallery-header {
+ flex-direction: column;
+ gap: 10px;
+ margin-bottom: 12px;
+ }
+
+ .gallery-header h2 {
+ font-size: 1.1rem;
+ margin: 0;
+ }
+
+ .gallery-toolbar {
+ width: 100%;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .toolbar-search {
+ flex: 1;
+ min-width: 120px;
+ }
+
+ .toolbar-search input {
+ padding: 8px 10px;
+ }
+
+ #btn-load-albums {
+ padding: 8px 12px;
+ font-size: 0.85rem;
+ }
+
+ .albums-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
+ }
+
+ .album-card-overlay {
+ padding: 8px;
+ }
+
+ .album-card-title {
+ font-size: 0.75rem;
+ }
+
+ .album-card-count {
+ font-size: 0.6rem;
+ }
+
+ .photo-gallery {
+ grid-template-columns: repeat(3, 1fr);
+ gap: 4px;
+ }
+
+ .photo-item .checkbox {
+ width: 22px;
+ height: 22px;
+ top: 4px;
+ left: 4px;
+ }
+
+ .photo-item .title {
+ padding: 6px 8px;
+ font-size: 0.65rem;
+ }
+
+ /* Breadcrumb */
+ .breadcrumb {
+ flex-wrap: wrap;
+ gap: 4px;
+ }
+
+ #btn-back-to-albums {
+ padding: 6px 10px;
+ font-size: 0.85rem;
+ }
+
+ .breadcrumb-current {
+ font-size: 0.85rem;
+ }
+
+ .photos-count {
+ font-size: 0.75rem;
+ }
+
+ #btn-download-album {
+ font-size: 0.75rem;
+ padding: 6px 10px;
+ }
+
+ /* Pagination */
+ .pagination {
+ gap: 8px;
+ margin-top: 12px;
+ }
+
+ #page-info {
+ font-size: 0.85rem;
+ }
+
+ /* Floating action bar - compact horizontal */
+ .floating-action-bar {
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding: 10px 12px;
+ bottom: 8px;
+ left: 8px;
+ right: 8px;
+ border-radius: 16px;
+ }
+
+ .action-bar-left {
+ padding-right: 10px;
+ border-right: 1px solid var(--divider);
+ flex-shrink: 0;
+ }
+
+ .selection-count {
+ font-size: 1.1rem;
+ }
+
+ .selection-label {
+ font-size: 0.7rem;
+ }
+
+ .action-bar-center,
+ .action-bar-right {
+ display: flex;
+ gap: 4px;
+ }
+
+ .action-btn {
+ padding: 8px;
+ flex-direction: row;
+ gap: 0;
+ }
+
+ .action-icon {
+ font-size: 1.2rem;
+ }
+
+ .action-text {
+ display: none;
+ }
+
+ /* Settings sections */
+ .settings-section {
+ padding-bottom: 16px;
+ margin-bottom: 16px;
+ }
+
+ .settings-section h3 {
+ font-size: 0.95rem;
+ }
+
+ /* Status badges */
+ .status {
+ padding: 4px 10px;
+ font-size: 0.75rem;
+ }
+
+ /* Modal */
+ .modal-content {
+ margin: 8px;
+ max-height: calc(100vh - 16px);
+ width: calc(100% - 16px);
+ }
+
+ .modal-header {
+ padding: 12px 16px;
+ }
+
+ .modal-header h3 {
+ font-size: 1rem;
+ }
+
+ .modal-body {
+ padding: 16px;
+ }
+
+ /* Drag hint */
+ .drag-hint {
+ font-size: 0.75rem;
+ margin: 6px 0 12px;
+ }
+
+ /* Lightbox mobile */
+ .lightbox-image-container {
+ max-width: 95vw;
+ max-height: 60vh;
+ }
+
+ .lightbox-image {
+ max-height: 60vh;
+ }
+
+ /* Converter */
+ .converter-input-section,
+ .converter-text-section {
+ gap: 10px;
+ }
+
+ .output-actions {
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+
+ .output-actions .btn {
+ flex: 1 1 auto;
+ min-width: 100px;
+ }
+}
+
+/* Small phones */
+@media (max-width: 480px) {
+ .app-container {
+ padding: 6px;
+ }
+
+ .app-header {
+ padding: 8px 10px;
+ }
+
+ .app-header h1 {
+ font-size: 1rem;
+ }
+
+ .main-nav {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 4px;
+ }
+
+ /* Adjust nav buttons for 2-column grid */
+ .nav-btn:nth-child(5) {
+ grid-column: 1 / 3;
+ }
+
+ .nav-btn {
+ padding: 10px 4px;
+ font-size: 0.7rem;
+ }
+
+ .panel {
+ padding: 10px;
+ }
+
+ .panel h2 {
+ font-size: 1rem;
+ }
+
+ .albums-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 6px;
+ }
+
+ .photo-gallery {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 4px;
+ }
+
+ .scheduled-thumb,
+ .more-photos {
+ width: 40px;
+ height: 40px;
+ }
+
+ .scheduled-post-actions {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .scheduled-post-actions .btn:last-child {
+ grid-column: 1 / 3;
+ }
+
+ .floating-action-bar {
+ padding: 8px 10px;
+ }
+
+ .action-bar-left {
+ padding-right: 8px;
+ }
+
+ .selection-count {
+ font-size: 1rem;
+ }
+
+ .action-icon {
+ font-size: 1.1rem;
+ }
+
+ .action-btn {
+ padding: 6px;
+ }
+}
+
+/* Extra small phones */
+@media (max-width: 360px) {
+ .app-header h1 {
+ font-size: 0.9rem;
+ }
+
+ .nav-btn {
+ font-size: 0.65rem;
+ padding: 8px 2px;
+ }
+
+ .panel {
+ padding: 8px;
+ }
+
+ .btn {
+ padding: 8px 12px;
+ font-size: 0.8rem;
+ }
+
+ .photo-gallery {
+ gap: 3px;
+ }
+}
+
+/* Touch devices optimization */
+@media (hover: none) and (pointer: coarse) {
+ .btn,
+ .nav-btn,
+ .action-btn,
+ .toolbar-btn {
+ min-height: 44px;
+ }
+
+ .album-card:hover,
+ .photo-item:hover {
+ transform: none;
+ }
+
+ /* Remove hover shadows on touch */
+ .album-card:hover,
+ .platform-card:hover {
+ box-shadow: var(--glass-shadow);
+ }
+
+ /* Better touch targets */
+ .photo-item .checkbox {
+ width: 28px;
+ height: 28px;
+ }
+
+ .platform-checkbox .checkmark {
+ width: 28px;
+ height: 28px;
+ }
+
+ /* Visible preview button on touch */
+ .photo-preview-btn {
+ opacity: 0.8;
+ }
+}
+
+/* Landscape phones */
+@media (max-width: 768px) and (orientation: landscape) {
+ .main-nav {
+ grid-template-columns: repeat(5, 1fr);
+ }
+
+ .nav-btn:nth-child(4),
+ .nav-btn:nth-child(5) {
+ grid-column: auto;
+ }
+
+ .floating-action-bar {
+ bottom: 6px;
+ padding: 8px 16px;
+ }
+
+ .photo-gallery {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .albums-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .lightbox-image-container {
+ max-height: 80vh;
+ }
+
+ .lightbox-image {
+ max-height: 75vh;
+ }
+}
diff --git a/data/.htaccess b/data/.htaccess
new file mode 100644
index 0000000..3a42882
--- /dev/null
+++ b/data/.htaccess
@@ -0,0 +1 @@
+Deny from all
diff --git a/debug.php b/debug.php
new file mode 100644
index 0000000..9605356
--- /dev/null
+++ b/debug.php
@@ -0,0 +1,579 @@
+
+ Order Allow,Deny
+ Deny from all
+
+
+# Block direct access to class files
+RewriteEngine On
+RewriteRule ^classes/ - [F,L]
+
+# Security headers (if mod_headers available)
+
Click buttons to automatically fix common issues:
+ + + + + + + + + +PHP Version: " . phpversion() . "
"; + +if (version_compare(PHP_VERSION, '7.2.0', '<')) { + echo "WARNING: PHP 7.2+ required!
"; +} else { + echo "OK: PHP version is compatible
"; +} + +// Required extensions +echo "OK: {$ext}
"; + } else { + echo "MISSING: {$ext}
"; + } +} + +// BCMath (for base58 decoding) +echo ""; +if (function_exists('bcmul')) { + echo "OK: bcmath"; +} else { + echo "WARNING: bcmath not available (short URLs won't work)"; +} +echo "
"; + +// Password algorithms +echo "OK: Argon2ID available
"; +} else { + echo "INFO: Argon2ID not available, using bcrypt (OK)
"; +} + +// Config file +echo "OK: config.php exists
"; + + try { + $config = require __DIR__ . '/config.php'; + echo "OK: config.php is valid PHP
"; + + if (!empty($config['flickr']['api_key'])) { + echo "OK: Flickr API key set
"; + } else { + echo "INFO: Flickr API key not set
"; + } + + if (!empty($config['telegram']['bot_token'])) { + echo "OK: Telegram bot token set
"; + } else { + echo "INFO: Telegram bot token not set
"; + } + } catch (Throwable $e) { + echo "ERROR in config.php: " . htmlspecialchars($e->getMessage()) . "
"; + } +} else { + echo "MISSING: config.php — use Quick Fix above!
"; +} + +// Writable directories +echo "OK: Root directory is writable
"; +} else { + echo "ERROR: Root directory is not writable (needed for auth_config.php)
"; +} + +// Test class loading +echo "OK: {$class}
"; + } else { + echo "ERROR: {$class} - file loaded but class not found
"; + } + } catch (Throwable $e) { + echo "ERROR loading {$class}: " . htmlspecialchars($e->getMessage()) . "
"; + echo "" . htmlspecialchars($e->getTraceAsString()) . ""; + } + } else { + echo "
MISSING: {$file}
"; + } +} + +// Test Auth instantiation +echo "OK: Auth class instantiated
"; + + if ($auth->hasUsers()) { + echo "OK: Users exist, login page should work
"; + } else { + echo "INFO: No users yet, setup.php should appear
"; + } +} catch (Throwable $e) { + echo "ERROR: " . htmlspecialchars($e->getMessage()) . "
"; + echo "" . htmlspecialchars($e->getTraceAsString()) . ""; +} + +// Security checks +echo "
OK: HTTPS enabled
"; +} else { + echo "WARNING: HTTPS not detected (recommended for production)
"; +} + +// .htaccess exists +if (file_exists(__DIR__ . '/.htaccess')) { + echo "OK: .htaccess exists
"; +} else { + echo "WARNING: .htaccess missing — use Quick Fix above!
"; +} + +// ========== LEAK DETECTION ========== +echo "OK: {$file} is protected (HTTP {$httpCode})
"; + } elseif ($httpCode == 200) { + // Check if content contains sensitive data + $hasSensitiveData = false; + if (strpos($content, 'api_key') !== false || + strpos($content, 'api_secret') !== false || + strpos($content, 'bot_token') !== false || + strpos($content, 'password_hash') !== false || + strpos($content, 'password') !== false) { + $hasSensitiveData = true; + } + + if ($hasSensitiveData) { + echo "CRITICAL LEAK: {$file} is ACCESSIBLE and contains sensitive data! ({$desc})
"; + $leaksFound++; + } else { + echo "WARNING: {$file} is accessible (HTTP 200) - {$desc}
"; + } + } else { + echo "INFO: {$file} returned HTTP {$httpCode}
"; + } +} + +// Check for common info disclosure files +echo "WARNING: {$file} exists - consider removing ({$desc})
"; + } +} + +// Check if secrets are exposed in JS files +echo "DANGER: {$file} may contain: " . implode(', ', $foundSecrets) . "
"; + $leaksFound++; + } else { + echo "OK: {$file} - no secrets found
"; + } + } +} + +// Check config.php for exposed secrets (verify it's not outputting) +if (file_exists(__DIR__ . '/config.php')) { + $configContent = file_get_contents(__DIR__ . '/config.php'); + + // Check if config has echo/print statements + if (preg_match('/(echo|print|var_dump|print_r)\s*\(/i', $configContent)) { + echo "DANGER: config.php contains output statements!
"; + $leaksFound++; + } else { + echo "OK: config.php has no output statements
"; + } + + // Check if config returns array (proper format) + if (strpos($configContent, 'return') !== false) { + echo "OK: config.php uses return statement (good)
"; + } else { + echo "WARNING: config.php may not return array properly
"; + } +} + +// Summary +if ($leaksFound > 0) { + echo "Use Quick Fix buttons above or manually fix the issues.
"; + echo "No critical leaks detected.
"; +} + +// File permissions +echo "WARNING: {$file} is world-readable ({$perms}), recommended: {$recommended}
"; + } else { + echo "OK: {$file} permissions: {$perms}
"; + } + } +} + +// PHP security settings +echo "These are hosting settings - may not be changeable on shared hosting
"; + +$securitySettings = array( + 'expose_php' => array('recommended' => '0', 'desc' => 'Hide PHP version'), + 'display_errors' => array('recommended' => '0', 'desc' => 'Hide errors in production'), + 'allow_url_include' => array('recommended' => '0', 'desc' => 'Prevent remote file inclusion'), + 'session.cookie_httponly' => array('recommended' => '1', 'desc' => 'Protect session cookie'), + 'session.cookie_secure' => array('recommended' => '1', 'desc' => 'HTTPS-only cookies'), + 'session.use_strict_mode' => array('recommended' => '1', 'desc' => 'Strict session mode'), +); + +foreach ($securitySettings as $setting => $info) { + $value = ini_get($setting); + $valueStr = ($value === '' || $value === '0' || $value === false) ? '0' : '1'; + + if ($valueStr === $info['recommended']) { + echo "OK: {$setting} = {$valueStr} ({$info['desc']})
"; + } else { + echo "INFO: {$setting} = {$valueStr}, recommended: {$info['recommended']} ({$info['desc']})
"; + } +} + +// Cryptographic Functions +echo "OK: random_bytes() works
"; + } catch (Exception $e) { + echo "ERROR: random_bytes() failed
"; + } +} else { + echo "WARNING: random_bytes() not available
"; +} + +if (function_exists('openssl_random_pseudo_bytes')) { + echo "OK: openssl_random_pseudo_bytes() available
"; +} else { + echo "WARNING: openssl_random_pseudo_bytes() not available
"; +} + +if (function_exists('password_hash')) { + echo "OK: password_hash() available
"; +} else { + echo "ERROR: password_hash() not available!
"; +} + +// Server info (be careful not to expose too much) +echo "Server software: " . htmlspecialchars(isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : 'Unknown') . "
Document root: " . htmlspecialchars(isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : 'Unknown') . "
Script path: " . htmlspecialchars(__DIR__) . "
If all green: Use 'Delete debug.php & Go to Site' button above
"; +echo "If errors: Use Quick Fix buttons, then refresh this page
"; +echo "IMPORTANT: Delete this file after debugging!
"; +?> + + + diff --git a/debug_urls.php b/debug_urls.php new file mode 100644 index 0000000..2ba2349 --- /dev/null +++ b/debug_urls.php @@ -0,0 +1,111 @@ + $url, + CURLOPT_NOBODY => true, // HEAD request + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_USERAGENT => 'Mozilla/5.0', + CURLOPT_HTTPHEADER => ['Referer: https://www.flickr.com/'], + ]); + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $size = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); + curl_close($ch); + + $sizeStr = $size > 0 ? formatSize($size) : '?'; + return ['status' => $httpCode, 'size' => $sizeStr, 'bytes' => $size]; +} + +function formatSize($bytes) { + if ($bytes >= 1048576) return round($bytes / 1048576, 1) . ' MB'; + if ($bytes >= 1024) return round($bytes / 1024) . ' KB'; + return $bytes . ' B'; +} + +$flickr = new FlickrAPI( + $config['flickr']['api_key'], + $config['flickr']['api_secret'] ?? '', + $config['flickr_user_id'] ?? '' +); + +echo "=== Testing Flickr URL Formats ===\n\n"; + +// Get one album +$albums = $flickr->getPhotosets(1, 1); +if (empty($albums)) { + die("No albums found\n"); +} + +$albumId = $albums[0]['id']; +$albumTitle = $albums[0]['title']['_content'] ?? 'Unknown'; +echo "Album: $albumTitle (ID: $albumId)\n\n"; + +// Get photos from album +$result = $flickr->getPhotosetPhotos($albumId, 1, 3); + +if (empty($result['photos'])) { + die("No photos in album\n"); +} + +foreach ($result['photos'] as $i => $photo) { + echo "--- Photo " . ($i + 1) . " ---\n"; + echo "ID: " . $photo['id'] . "\n"; + echo "Title: " . $photo['title'] . "\n"; + echo "Server: " . $photo['server'] . "\n"; + echo "Secret: " . $photo['secret'] . "\n"; + echo "Original Secret: " . $photo['original_secret'] . "\n"; + echo "Original Format: " . $photo['original_format'] . "\n"; + + echo "\nURLs (with sizes):\n"; + + // Test each URL size + $sizes = ['medium640', 'large', 'large2048', 'original']; + foreach ($sizes as $size) { + $url = $photo['urls'][$size] ?? null; + if ($url) { + $info = checkUrl($url); + $status = $info['status'] == 200 ? '✓' : '✗ ' . $info['status']; + echo " $size: $status - {$info['size']}\n"; + echo " $url\n"; + } else { + echo " $size: (not available)\n"; + } + } + + // If original secret differs from secret, note it + if ($photo['original_secret'] !== $photo['secret']) { + echo "\n *** Original secret is DIFFERENT - originals should be accessible ***\n"; + } else { + echo "\n Note: Original secret = Secret (originals may not be enabled in Flickr settings)\n"; + } + + echo "\n"; +} + +echo "=== Summary ===\n"; +echo "If 'original' shows ✗ 403 or ✗ 404:\n"; +echo " -> Enable original downloads in Flickr: Settings > Privacy > Allow downloads\n"; +echo "If 'original' shows ✓ with large size:\n"; +echo " -> Downloads should work at full quality!\n"; diff --git a/image.png b/image.png new file mode 100644 index 0000000000000000000000000000000000000000..4c5e42f10ce7521b91e936ff8ffaaac969e06d81 GIT binary patch literal 658920 zcmeEvbyytRvhUyoOK^wa4g(DC?(P<3u)$qJ2oT(Ylfm5~I0Og@9wfL1LU0dGfFN(! zJ7=GB-@W_X``-87f9d(4d-c?+RrRZ?CB3>JQe9OJ6O9B7003Yr$V+Pi0En8ZTv#RxWje9meKbvaC?t)g1EDwiU#Rna4BnN)D%W?HjT>rGnev zM3aoukkmQ88D8eGjd7AEvZzd4iVl5^t3R+)I4T)nX#&w*a7>mZ_3T!3PffX1d%Mqq zJi@p#QNb#`w5V>*UF3e&qD3ISJsIQ7EdIb7fI-8Q-BfPGP)QulOB$hv$775dU=c zoFQAz+ikcfQDGh0QZQb{gFi%`Fz{6^^VEpe*VEc^goDR+Ejda{L{m<K3v1HL$DkpZS*^Zy%Tc=Plfw3?x&3Ln!R9u* zM0T$8=InAupE&yOb{oN$6?ZbXN_QQwSK`K0cv@&q{rIS4{NxnPMxX#iWO~~t1yum$ zf+(vzIm0m9H`Ela@)N8rktZRleFNFFr49>>&3L|r&Ztn{*O_t52CyMZqNqzb3Q)Q3 z2LhMvc0N8cidE8Rjskv^Lh-uWjXMu*6m*>{H3xpyTH5z||8!DMW47jy-4X~H1IBLj zvx@gklaKJm(;!9qJO?pJJad~_s+qnbdMBFiz~Q7hxHw|(dg6-%o9eA4d`&zN!O$iB zG8tXLq9~%b>L^z}QesaM+E*TpH>+D&M&hRa4GGa}=WF?e@Q=_~KfIOjX<5a?PPfl_ z#YSo?2cnL$J5?i
+p1?fC-z`(|*>ue?yl4`Qz^$mdze-_O6UV<0Y_W o zDU^HudE%>)5%oE@DEApck|YOq#075yS;js@HuW8~wa28V5`;8~?3H|{>Z=#p7XrrR z7CEgWs!H$1l&TcJD0VhybpJ@B&>kSsm$*urRwH6KDsT+OvxzKWv{ uCmSqgP9Z)}6TN*<7)~EZX1QTN ~ zG)xzG>~yk+ upaKn_EOCkFKU{Gq zqKf>~0M~A6XqzBbX|Na#JI2ml^!Bwn&M1A{loXj-e>@mFSfR9=P@q)^3KYAfCrELd zNuy^95oySw9R~|gw49TdQN+ gWY6fcNSt=r53W6gn|`R@u6$fgjNF~7sD9 &(dOO&mCrF+16Or5YAlO#Bkt% zu1%&pda#;Cg;@kyF}ls1GLy39MEs|Fu}6tNmXxbhIab9z+f#>bdZn)FB}z?aMKT;$ zX)BW* n%B&;+3TBH(oN= Wz|O~|a2hsrEPIb>fLnO?tCP)Da+ zF7Q;iLMx)$UcZi|uMcT%&yZ%gG-h76QE`iGkeh?Yi0p)UKp>B(jjP)8%tp16&onk0w2cE^hXVR~jzsEn2VNy}hXd%|8^V?4LenkhQ5znE=xT)_uH zw=X`y^F`07*{BBYc9U+I*z~P6mCzZxt8@H?>Zup?yV3hBTLLCjzdiqG=@2oy^j0*4 z$0lTOMPo!r# VW{K#cS2ic|Cl3`y_+)qy;QC&o>9$b38phON9?`4QI3Ic zH#@z|dK32$hGK6~o|6d2`AQL#oDX!*h`=5~a|vOh1yJAepkJ*-^({tX417E67Z(|M zgl|fTp~EMeY?KMhnK?fLHd00$;GHT!N_LJFhZTo5tX{(24flsu7AI2|Xcj^b0v?MV zqeOl`R| ?8dv*~J2i9zXS_seDFvG|8^CRR|S;Q%37!YI0^OOzr_i}0q6pm}8 z#PxSA++6&0 f`ZrT=H^!(Zs`JiZN2ky*S?lU5~lat>@S- z+&U>!{dfQ=8e+HFoV@wWyE=WaVd=>{=pwOiFa2FBHkyFDK~ xd1N#-x6&Z@&5( z)o+Xn_U}i;G3+Eo({E!sr!Id@!&doZ@x4W}$yqW74F4vn2Z6LV^U@pkvQiC1> kaIH(#8+0vghr4uB=7QVf(?g_I)OUdwic4aRA?6sJjWyH8E@ReXWc9I zBth&V9`9gzRFVB2=Xn*S!X?crTh_=cUg!c27*5x$b!{ xwAz?hFn(DRLhWu4pDQJAt-|y1y*JPz1Q!EQ}{HdstA74HJ{%Du-r*@4Q&+@M!~Kw2|2I%=W+RdL{j=pddby< zc)C^%a%(vI+uaH$^!TLp@#zgxdt-}-pkqkc!JU;Yp4*yjo1H731U%a!b*%zgNNK5q zQki>&YMv+`?{hURD<5m;f>{kB-An6}PIaRg$o3ZV*ycrcTUEWaqYn_L^`DKosVMd` zrLj3R0R_rzzh)V6w;Y8>(ob?wwKOL=Bs9rg>Z<4;z8J+-8Ly}pk<)IdKQ5{;-EPxU zM}N0EmWdPC{=Sj7b!)6bxb`QRYQxuwTjGuD>I_z_J!*U_{--h6D%{hVcxfO{S)^e^ zo2Y5|wPa#7M3S~Q`px!Kn#_ivklF>?XVk&yGEJ=$2khd)^1UgYKzl+`(w4!(8j?6M z&rUqYWgo*eUHCPH#-P_RDZv|-w~whyw5^Ym%YHVlA8wHbeh@$(;o2Nr$!`f=OHf67 z`w`!GR?s3j4P-K#P*7m3U@z=OgYRKB7=T*lM^ehv+hxREa~W?>P+PFDH!~>q0?|F_ zh6_$@_{8n29alDTppDM9PimE&Mn=>OE6ZfPz)<0yVZlhI@(ym2cAP*@P;%5bQA qk#Mhhrrfk*~%E_O#>TCR!~JOQ4{@o3WEj`kST~hM1j~ zv76hox_r94Cs$>S#yGs${bYCP6^om3I;yd@GbOy0zU*ZI?`5A(%QDDPH_nIie55@! zEOb${5@7gI0?yV!n@|q0tF+jC0xB-=5s0drcsuopK$VdtGf_Yu1ZC%8wVnTpM}wSK zP@Q5=UJ}+WB@)`B^<{W`Ae<`sgoTilqR>vVfm|?ZC%YcGB>(dBJh<~@ztIBNjPUC9 zJ)m(%(|0$HhW+`O$hFD48mWY1z7S`RSy2xZ^PhDb%*#?T3nFMlm7-jH#pef8YwRAg zv2ohdWr*7#U4s3RB2&&B8kq>ULhZWMN_&IlAvh$mGKz3~Oj4$KgRJ=Qb8<0F&!(BD z-0iQJ7kNJF#veOzYJs!gm7)Gr230xN+n4;X6EZz|Tc7Id@ny)U&Gw}m_>EFfzxrbf z2#;CUCR$xo1q!hkgLC`Dk?Y;$L?au)v*+74+wKc8Tk)mEjbr^>FS@FP4Y&7UG4z$L z_GH~Ws ci+!2*QsQH$`K;-!8!dLG)vYnuxW*c*JSqdyJ;lczsL zED@`@@4`*A4iNlZ&Sc-Q=}q*zoLj$7ur~yt_AX+)G|yNjc;#QSA?cg6PcTt^aF Q(~O-jjCI31icuQ zD454b8Iv~+D>6@I1a#jFQP;;o((AY-s5YOplxuEVuj=`s+D1T8Tl(xIThF%k4Thhw z-UD9DUa1(T$Bt)+=EH_z$-m1)Bc_1ut0ep{_TD 4ev_gErF&ui_?I7%+5-o9jRJwQ{GUQjXxDl@@vb3Ts;=-9jyEbt;ZiK) ziT5;cTs0zDnJKI@#yLXmO%d|#WQ&GP6_TF@2TYW6AgD3$yU3(*qp@q~4)%nnq@z%o zD2s{P$d7CRG3aJ86AF)1$3clyMQ&vlU)h;HII72367W)L6zY6YHlajTU2#>b8SgNy z9%vz~AVS%8Z;JWUox{;NIdIK6C?Vv5O7ELDrCm@l(Y^BJz}Qc))ZC`hLgyS;qv-`h z=yxZZuFc!xqOUwTgt)=FGa#q_OZ|HQI51$`uYPZFVz($I5sImL!m;gWXG{3Q4XPzm zeld|zkBoY0QA$GO;?m(0h7m!+1twdY6^)iA2pS2z`3~%(E7JEr%3Ejgd0l+6&QmJC zRNQLaK#t_(mf|$Dm>^j>=7^WxxqpP^xLocGj+q)Z=Z)V8_NCo!|8iD3uT+#W>B3dt zp>%5QfZZdNkH0O@uMDErd@9+2t(n@>sC0FsP-a^=-Y7SMt70G|8-M; 7Cp2R``$AXVp}}Jh#6++Q{yMZd z=fSI98I>KYXE%*9YmDHgj^JX8!l2)EQov8d$cxoM@8v943E_YZOH)<|8(}BFJ4O3A z3QA|EE*hRa{jsZcSK6C<0G=N=Uvc& zS%*@j+yKZMgPN>nNQs1}o1YbL#4yXRckgL^;=+`T7VZoG) ~t;Gcb23@Y!SIn)2i}5zpy@@{x z3E>5lzRTDc4-Qw9eJPA6K48+!22ZdK1@r5QLxFE2YcB8v7=UhK9Nik;^*;=9LSbry znGej^K3H+Qvqw8p-2GZDFTgp%Qxr{ep;z(7 (%XHcS-KhqPA_0<;-g2CROsouG)UIymw>2j<;}UFSFNP+( zjr^YdV&(6^4w)d-jQS)c(PXn&jYE`*<|v@m`I50)vhvdL$Tj+R%orL&1@Kj~Nt8JQq#g-o>4!w%X4K&@44u zTm+mS V;%@g1>FZ!+qc~(9~7OOaWcG0c{*g*=!&qJ(I$41ZYexzSEBdRQOaXWK?&Z zi*pZnXLVCj!!Nm&;v>nc#U1k7Io=KUgXN#SQr3iMYlU}}L?vG0;%&qPp{Xam3fUwE zB^#;h$TvjV*GQN18WLtGOT@n~H#Z?OXlP=b>6J4Vr_)`in OCBvnRQC&~>d>B?bthGyFT57qOpGz6DAgp@`!K0>9(llrU{-%9 z5EeKk@vN2N?pUz^VoP9a=A;lYz2S1R82in>nLD2 +naj2`s7`Lw))u%MUk z-EBYCDusdS=kQ6kZOi+X{|#%eBEoi~mKJyy)Bhz?(Bja>R%)yRi^0=5lkP2{$iwne z0i`N>hE!}M=Ij1DxZyUd>eT3lC9D?>=l1|NuP8b6GuRBDQO`82+-X+!`WyWIi|#ID zYr#n~iIJ$ReqI#Rx>Lca=~t#t uk#Yvdl2Qth618XcT^{c`}@g9aOk94p|k0pe-Do4 zrt0G v zs5!1WnQnD_++t;XZGaJa!ko!2db=U&VeljADE6)qRk=&3;<`=BeC_&5Xs0~A)GBdq zX?FA*uxS@A{YE}_L9lZvwd5(sn l7ZB%OA(;I!X@ i0MEN^Sb#6KQ8fL*fw}sutnus+`hw1Sc}}8sNx8?}(Jq42Ne`1*6>M)(6Fe+02n8 z4JQQ!Yq_$c@@K45%=@3r#=Y4}-EI3AMm+d;`Uwl(z>!AY147I =u2&o3Qiczzeq_-PYNv63WGev6*Xv z8ADQtQT&W@>NC+ynp$s{? $aHyZ`XKtL-DpOeDz`(B_yVAr>9;h zhsGs^re1{u?>W1|$v)kj>Mw;AwZWQta)dGqnM-Yb#L-OfsauwG=fs<1bBZ43t*tw~ zX`7JVsn+C=u+c~LJ2~BVIlmhFa=HxDKRc<>3`gOvkzz_bDk(yIbXF;USQ_X~(0 ~A)_6odUX(EXL&Bf(_Ehs_m4Gl+&>2>Zv-*%=OR|ne_Vk>=si{>qlE{&)JTS>i zKn&y(FsgAIu&3k`;hv=rJA@7DwmJg`WCY1QJ07TNp|_VR2H_L8M9_bU8D3*v`P$b6 zUN{o%^DeB`;ogYY>(ybk2-)HwH Pd>qCH3wjW{ik_(71ev3(LHy6W8(RaQ5PJL#A2gfb-6MlWhf4$t zZU!4lwK@yWIolmE>gRIjhfBjLN7aLN{c; D)n!3D|PVPin5sey?VWPc$|7hFAk+nW+s(RIEYY!0B0~u zJ==N34rHGiF98llRi|6XnLHelix{-!Y@6wRG0Ut>x*B&oerv^1NU}ai#Kd)=bf_oy zM&FXlt(hP{pwze1P;>}4|8%v T%}37-kMv#@N#NAl=>W2jhG zg2M(^bKZfv<8&=S`xNbWcovV^LEoZIuWdnrV2@DYR3w7BeXLcX5{u5FYtC0g`YCVi z4!C2()Zo6<&hZQ%+>8j8nh`rLgKDW9HS71%oEEF|yEM%!E^I>Uzm3Q#akI}EZIkMA z8(ykA^BgH{UwnKX($MkciYPjHD#fkhs8vex e?sBh{~R2rP^7 zy{|8Y1V61^r=PRVfii|wd%bPe1BH93#RkqzM@5JaIV7rFG_&Ou;P6Gi*TkN>Hm?}p zXiX##>ucu$KG;K#zupq_J3CP_V{EFd4OU6*B_ >d-_knm#fYaL2<%3IeZsWzfrL z#(*f=v+2^_!14gm9S~L`F&*;8*i9@YN}fx-FS0CBB5EHSuQJhZZefVAdKOdR!szU& zST@dn^Es-(bH-bVZaW9>s7O_ZI=f2hyb|g?^v#i1`CW2yb*qMbMO8-=@$u9RY5C{g z!hRx*dC+mTlvI{YlEmE`7DA4-H!kdL}wGV2lin{^PRN)RB0kZ9_ zeRo@XoYHEwA7yFlBiQku2UPeCyes$n(vW+FP|^(TYoB*1;mNow9v_e)8BeWDZ( S?4tl&p^OFjO7gR&Jejw^Fi-O#6(LVgvxVR26-)LRMh$*p0RRVom^?YncCn zN|j$&!Cub|ikz5jiR27
iHQ5AwxCgv;l=4`( z1$%KpA5yGpq^e{JYGf(#admRvIg5LAcVLV14HkK%M$`;{aU4)$?2TR3!hBny91*zX z6hCxwR?U>(S6u!*7qO8Xw!`v`6H)1)!|uGM7uyEbZ6}K6B5!Sc_R5f0@lL91e~}|L zsr-?e@tbHn8&PbpLU7bjrKv~Bg2mUQ0=x}FD%G5DvejH;srcULQWf f^}QO1L`RV>wQ^ z@ndjfqwZLSYhHY_@ZE!c=*EHcuwnsEqMd{*y;8FF+b7H;n9`Jef$eXj?Rq3?ac%OL zFM5tFdkV%)GzW>CB=GwtB;Xi77M-XY%@k$`hc?4SK%N8o0_NkNUrp3mif^4ptA`h+ zBuduBdoF$}Cz^KU{XSLKYFyS}@^VU+CSSHAW~jT9xou)o#>U)Y-jf7%%B`x^+Evl{ z2yLyZCg#@Lt#k(`aOuTlfsvy3j_b^~t8Z)SLL3=62J{@8-5Sk_pK%JC#~T?eR$hO> zri$7?cWomPvgr;KivPa!G?8V_$YNx)n4$C?bNNjq2~F5mCq!q=_Wsuwk(J2~>)d>y z?9!0I)%hc$Z)>BYmT?O48p}^Ms<}O0V7IH3P#4dJux~U |shs3MWd`}Xlm{92?OGH^E3a{7J*UO4~lM-@B2 z$~}M#S?aI7Qp%HP!L3^HJ<*^b)gkdRk8-m*#$Hut@prcEQ`zd{;~>_{s353_aE{O2 zU7Jle)DuQ>;f;K~z10yOML8kwg8dA--ssd@PZX7p$B^Xqs?4oVZ80+We(~nCk!Kim zqkDG9c70d181`-=i2k!rvQWj?xP;}k2g@_Fiy$2vDg76*?`)vx`-1#6l)h)AjJD$- z7#@xi%D%XIcTA@Z@(gYE-GgSK99U1Udq23XWU1LYu7xcIr$E<6WpPoc-f*}de8rSh z$u#wf%du8I_8ayK3y%vOr(VsgJyVO?5Z}_s?PfFWw9O7HEIa*L4q6GCk?97Wq?oo3 zKahz}nNbj5+YL;!3m}r*JjMM?oZAOPTG2Koei}`6Gvyb;T#BQ{a!y+_QrmOlevpA6 zf~@h;xuU&l=Z15YZ-H0XW*=YuOzdNBSsAE0u$msPaY%>v$F^I_E2Sfw8wl%Q)$+uTZwHiBjX5wzD7GKvoP3x%`WtD7KL&d-jX4i<>u_6;O zEGVs}6`zF!V{Zl<+Iq&k6?N#Kr{!IneU4Z^30??gBg{le;Kp?6$WGp5@Z^SHp-tM& ze>l }Q4bsa= zm+6GYSi`eOG7X#91!gQ_f@|yHMNY#-x1hodv1G}w AvLPxZ?brNqD6kP}R2=j`2R^MK8ID)K~Se|}u*_Or?^+eHR^C^bt; z3J!{^n_b)fQcVft)w__>_`66u&YSLwQk(KiL>=OsZejzSdS!i``rPR|@}+svF%c@) zlm-4uml+|!hBH&O_l6dNuC2uj+a9Bpl?|h_wv9zbdb$e3Jm!nDBNp-9bo0**m=uEb zM*F*j8>e2naaWmnrrBVR{#fOK(vIdUZnwHpKjU`H{qgL2Fo(;F`6+j`3g=U^rya9u z)48;g#nB5~+6lO-H6p|)Ry3GRDU^i*QW8hKF{M_-x#4-Zh=mBwS+cpSYPOqHui1mM zb+K9vjyrNMacjJpn^iJvQ5T-xzSFC4pt#j6O`6WJ+Fh#e?T^1B_2BGn$k+@nonSUJ zESrG>5~e;Gypg-Lj1SIYy(rda`QZZe?Z7{|>wrt<4|u!L9NA(FI_4wl=vRAD*j=U? zZ#n*nO3NX_j0t^`42FyZBCic~#vx~Fmu)D5k=0nua2}!;MmJt3QKB4!v4!&)P_2$E z>uPFdT%8IRZaNZs59$+52M&r?tPIbNd@=d{t)xi7`wh_-5%AXK98L;EC-K_lY3$D| z-N=)ds$s2-ol6~IF;(Fvh3Z~iy~FYAcvzOQmfY&A`5B#;{@Z2LfAwi3%`Ed((T|@9 zhmiw{kMj_j6-_Yr95n-@S1A=g>at=p?iF&(3ZhzhK8vj&95LZJj%y%SCwgan*BG2q zTC0iefM+flDbsN GQ7*ob^ECa$pze-Rv%v9Vn<>665=o;gYqCh7 zfhYVX XsC8gm0eLPtt{p>3sO%Z zHfb0Pksgu=6(c#?+n;k2ICwSswI5pz3b}h!PjFkxslFWwny%=R6(8jJq?1V?neU3U zA@AK++_M#J7VU+KrmHjXM#O>2mKWOyzI>hQd1Z~ZWlXo?MSb;*ze1NT8taoSyx9I~ zX0@0mLIe#fBu_pPn-ygFrG+8By(LN tF9dfRGHEvxY{q9mNgQ5=m89gtH#~#I^D-mW@f)MX1MS@zmG~zKRGnwf z=;RYe-|xuwUW;-#I(;bpsMLhEkmXXCl={4us^>?g4xViB0EwuC#_$UYb0FF*zNMK> z_;i4-m@s L|Nq!;X=zI5EjtMj*ySzWUrxo zMu(IpIu+s&Queei$^soTL%a$6Spbe`g@(6)`d&NpZr{EBcE@0y(@|>yZ)*M_qU=fU z?^cP~y_LWd8YZ#3j wUIZPDU|=A}G8ug@F-B@JCaWae#D+59c+mU*(bEJ;3&1&$01`B**+! zuyYmsZx<#8j)ZRfT}ZEf{25DZ-Q}xuio7!DlwmrrY{7&nB|VN2fh1a U zlDjE08$ziV>bi306~K*JkcZ31e^8uQ2w^1v&4?vd1Er6-OWub~0kHX;BAB7g(#eB_ zYp_iM_?7z|8lljp8h~K$yWRxRi-A764)K}!F@_j5c~D}H8k~!qS$wl9KAy>HF~tkY zwMn#K0!H%S35d{mD&{HtCU88*KNd#UYu~b`Y)*+4zm)#1f2~$H+F*eJ+4dMW_|VFd zW~dVdZpaRdaf(Jt^?jXi5&4N{5t>*;1ja3gQ&{O#Vh-h;;ULzmG3y4Z0Ye3BuP6rj za0+Uy;dJ!1Npk;{bS^5a_sDGX9IAY;fd0D5ti7zSl@+^`Ayddyi3h-Lz|No!|4P|4 znOW!xm8Ol*>b#_yP-TcLHSGSi3;=u)D+pzzp$bLkxzQ@8bP~fVR!?6*e1-{nU4BSE zHNC8u31y=kJ{~Y|<@0k YgwCyzTqPFS_rKkl#%77*XMR=qL>X?wX37jH-N-O7 zMm<)ZNUJD&T-?&52!taWBZZ&WFo<0*-!O 48SgO#pSba=n7->jf1CaAX3B)i05E6J*UT3rf zCpdy4rV&l*P-RV;csUfiR7o}?7zK?mK$s38b;7#FJV3-1STbdW9wuRUqK&wHI-VlF zFj}M-Y%$n8ygp7wEy6@qRa{`GbOQR=q*zOF{UE1kD`Au63rOm6Ofuhw;S9m1+i05z zcMk0qN)p@r`kf}=Jz%9WrgOtlFp`)7{kL{r#8FU8m^5n%-T9>0{iH|pIhk0QVrB0B z!BanZEpOtLbP& mJLtimNE_zX%E#73Gx{m$S2B>1kg_*(Iz0+gu89n*rI!(>)>dx zFc`|!MpP+eyG43c(riZ{9I8NU@tDFjPsw#bd9WD(`3b-XSxal4Nqmk+IWTOKl1Rld zJ1iRdxvUtJFgz*{rr#{me^T05Cx9In7`5+HeN8t*s1$ ;YSn1eZM(iyU!~RHQuecC>l`j aTM$X0tJX`+_R)g4zbdVumkvrA8Pc)k{rqQ2zL*i#mWxZ^ z*d!A*E^K*9+IOl&DQp-nhqMyun=*Oh3^*V7SXog#2}p=(e=6lX@V ~= z09x+yI+@5siHMepqA} =Q(V4*KsSNQM(6d)h z4@)I^QT?5fIme3PwEL)3d3gwP?*Sy#?U#IQ>BP~$J);t$hMg8_V`&bxd(UC{mQpW- zeOj%ty$JPNoC)SGzA*UwX3bOL?8wl~k1o@ZMr9lR(oPN{qEMoc>cEc@A^aLRq4g$H zb@N=R#9*GU!7pKmg-xqr2Kld35irAN>t?tyv1 H^09Ea3 zufx(-ikKyJCvaI8W_N@$(K3u$x<{ZR)K}7(&~uR4A^jl~5bEY)*m4BuB32dH#e1bO z=O9KwgrZ$~5ML9CB1!_Gf&MU1ST1=M^>}zAs$vKZ5qQ{{)gqK2geXfGhu|^l_c?U% z+k#l`KJ6+;xS|v1wjujCGW~XRf^N51v3+mK8N>;N#BEX$8Nw2Je{;SCKp9{Gm2wfX zJm9Ud%%mpK^8UpDA_Mvl(h9C`?@7C|3>aFNP(LAyj;<<9F v;6(;AWRwjJ|CJhc--dsSS(z3V zCuGaFsXjS;xbj}|0tw(p`DC4(?pE5=h;iwFqK%@;XyhPiH|(O7$dCb8b& z;)qXs3ix$N!9NeYWk6r7d?_#KJ%QH+f|u|CpN=@-8DIZ%IuHyT Jqb zm@uNom+dbrh;5mNN!@Z+I|!QpqJV#!qG~>JHpWUF0FMks1wBc}_OY+1EoLFZ(4jj6 z1k-PfwHV;2S24iKQ|u&19X9KrxHcmqpPElGiIe~(pT7-WTXVN80-JYSyb)<;GFv*B z-VrvoqA@uWYxg3CXs}vBhBUUILS|#rYJ{YD4-d~jNvJ7`SRljSU5~AW9h#947r}0+ zT_i&l6 51Fn#liD5r=zeSEoE|q#SG4V7U&MBtoIx#`1oU?0T zVFv7K^Pe5+4;X`Wtu?eOo0-?q?@IhS8p>nrknBPUP1+URDC~`Yl)AjTgxan;U7@4E zyu&^#zaYDrWOtrQMyf1w5V2n@=q&I_$XYYXSb`$%NEneW${rXhL+GR#EG^y3meAFe zpD8ZGs)Pj{ie_13HkS9990fKYCGV@+V$n1m+Q56ZUZ7cDDiB^p9s!#he3k(hHxR84 zCoevDj3LwlDFm`b4Q+xhOrwZGL>(%i;2Se&9%cZjYa!uHkSH!YvF;$d?u7j!QF |+`%VktV* hhdo7 ;SzjA%vKpphql&YE)jlfMB>OvLmZ#gRPy*K&xxYCMw{ehrJYVArWzPsgcBA9Ung zX%RFD?6B~%-yV+m*ZE^sw3Hcu?jzDyr+aRSDg9b+0*@zDLL*G(1q;Wn& hV&aiQe)s7||DC|ujviV?P6880mmUF^NxU|(jHV_YSxk@S zdh`f@7Wm$i1bhOg5u0#dXfGh0T~__2nksQ$y;*YCDnf}Ijka*?V#R>0oe{;xqAcj4>JBX{C zjaGA&;gnBWp=kaN$`qcziD&p{ydmI609s&livb?*V(&06%=@0MPgU_}ZrlS>hg2z3 zkGQ-2-Un1f%ZP(!F7E+-r1h`DoL7!79FLbYf*mOm(d4eOOHW8;zjCIkw#HI?Y3Hc9 z@D aBHWjfrw~Sv&V@x{LBCe9 ziWMP`AEJtBETIBj$BjG!BQfJ#1OL8=hc9j;J+ACMn)C=DJFRGK0s`c(OkQ*%jG|d7 zFIF$yhg05y%p?s*^#QCU^@4`;m|2??*qH%GBGORG eHak5>|P3NN{rh8+qkQ(ZStF%q6{fQAn9XYa#y){g?$l6 zq=l6I--7>-!2l^+K+*Spv!(UM3VW(7$PhMvt?Aik|kxahK9$wDaaE;b_*1LB}M$ zz EQY>cU5VwvwIWI&x}~;F7%Ha zxw+1+4Tx)2(9$WZf!|UvIDW}@;T-$@-7v4O`w{YwJE+o&@x3L(6Sx}=9Ps2$?60hs z8$?280(^f~BsER?a)U}~HdS72w8PtPFTyXHGt4I_P4YG!xZ=op{Y_{m&@Y Zf3gtlIGfY0}F0h2EH`WmMD4nL(EOx4m2oWW7y4(e-N zg9|eSVC*I8lKEW=_@a%o)EN O~haf!5R*yBry9dl#obPIJ zTE8=AK@^MS_0d_K-P9nx*hbV}6x%SDE55-vln8&40nNdE;6yo+`yrgPSgw+5N{i2j zw42v8nKZQobe;klt8Qn&y9HFu**OrhX;+-*At7MYS_0w_0l)K$`e)7YPbqn2NE?zQ zKq%XHVTAQqSpa|CcVV~`0b0QEmkEDVIv?;uD)@ov;i116K=S-F4{FE&{9f_H1GuDz z)kA^KKOT_&tO8-Zwiv+jGuwE=AM A%1uC%YQbddHn5n ztAl^pU)p~f>c`ao1I_;bE3lyMX8{11jbB&uF+N<)XKZBW2tWs*`2zqI0Pai&sGnE! z{o_JDX4i-N|6txcyjN!JXliZf46?IzvH;mz*c&>6Z|qYtvoJB{bp| =U5cVc5}Ly(Oc$lTV@*~QTmd_$lU$i!6hp&pR4hp~&JwY!3oskIs9Lto4U zSRSZmadEWeeR$yI;xKt;WXfg6#AC?H$^_(QGh{Mi19CGN8S`+ln{pUEduC?(vk0@( zpDkFKy4zbDf?EUk&gEz0WPkle_LKf!l;QmgWxx7nYV!XjQTT>VPNt3zv@=^6I$4;S zXsF6CKcH{%lPdlH6Hxutgwr3;J&Yij6PR7u|Bjuz;2#8#xj2Hzeo|m+!b|qI_J8u% z@9X-7@_*4Gy}woX-yZvaCE|f_W-yx?{!@Ph{yY8otExYl=C8JXHX!}b(m#gh|6XtG z9=b>NK;0iaZ3D9XLrlRVX=v?lZU(-M6D)52mSA~vHncT1l@WJmbhokR1%XU>+03|E zIXKLWnT(8#IGBJYX536XTwH8SoJQQ-tfri%+$OB1|0>r%*-!BQ-DnvbTAQ0WSs1c& zJ~MeB{W%#He|e@PlMC>JG$y-Fgr-gW4Q~>5vaHI*7J>>FZtQE@f0oh0{{hkbk3 V0*a6B_dS_H9o#<5Wx|G??TiO6U$2a_P>uWfB6=U-z9B{G 2Y9{J^V|{|8ZmMzt!^pkYMop zfXoLKpKtyvGoZlik0-02wAtC4+M1eJf>pkujj6M#qtiR9f7l48fIoV~^k4PpA4R}s z B$alYprTIxLyRr3u zG_OC|6RbGFWgz1oXf6G>0%yp|X3AsA&ILAOY-~(GRztABaq_S-nX;R483Ij>IJv;$ z{If0tbMWvu8amrK8azn!yPthC7qc*QmQq%CvvV|OHny{M23u=G7iSCRhY>Y!bTVaS z2LHVW{blF=Gn4wib5ZcWm{dn&M^i&*Qxk)~l%G@Z|2*se53=GPRnz}hT>cNS`o-ce z+LxHk7YGp>UM+80*u(K|)mn|l$tF|ObG>4zxU``Cqh7lIs@}g?{8zPKhx#(&&&*X- z#pR#!WD>E<%04lZrSIMjdU5uej6_%C-^|OLz}GB;$@`}<@&6ms0|4x|zu5N=Sz|YL z`j71U7sG)0mwn`ce TL&(R942wV?DjoU*83N)741iUZjer#o7D@ve>Iu4*sgc;9vp=xL5EEfT zW<`4V4e1HAys51d8yYLhLlW@`oVufnsXfS;4T%-uAqN%{QbUcE1Q(f|la-B)gPoP* z8St47IQi_aB&QCm=0Cm8h6<+TXKk=bU?nFa^Jn!)PvCwD5E~UM#ltI5PY8cCNcKPn znV7r`8OWCG*ITUGteCKH;7*MSKtjO6|9pUgfP)eRKx-1a_tPt*`s_mwV4YPKxq3zJ zO%_rLd@d2?n3bnP5M^V9mn-H>-ilvkKKz =od@Z>X78#7J@*H&S`C)R*r z)+b;hh%q4`?x7)B0j#7CDN1NORvdpU9a1Z^$)wHKB0)Y`PrUSco#f=|SruU68DJq` zVBjDiphZ}PSOxw}vqFTjSvWh}^RlqG8ZtZofgxB!*sz#d8W|h1*t-~6gN)rwjaa}+ z)7iy|#m?Bto|X8ay{FJPtXO~60|oup6V)F-qd?<8V|~=eWM(AfEU+<>P~Z@QG1^VM zp#M2y#HbInLqkEr`TxuKLj^)W06Z)Ny#!VRmuK}_TWs6%zhH=urDD;Y!KpD#{ n#MKBqEVxdoWyFKC^p})5yNqV>;BHKC-1*)j+)e)PO7y{HrpUzv>Dnv3x zWdd?JCWCf5n7%(R;lfQk$k#XTK@}@<9n>I`fnBO2h|Xxng(UES^7J+#eXA0hcb6St z)Hz=sCbR#3{D!)bFdB2vN^yl%YVkB?3jG?FpSCJU+Cg4h-sIS%uWB2ruvV_a@}h`7 z$TD$s!!$}SoE(WW**c!v-rkg{*!1mwU*071K}|=EfFxpv0W|h0O$ORQhInz42t3^_ zOPFv%JM%5pl(Ad6g*hr7#7AvtU)DfqPq5?|{gNEeO m10;_6g=ltIi7dAF_Rvxgp@ca@NaFY9H5-cwNCq*+5g5vM=L%;yQBaaGT z4TM01d|-hOI2hff${*5(N3AYmijI0MuN+gtW?(q3BX$FiP5n1TvNC#>Ut;$|Z_!$v z^?zMNjG(ycEiF7pq`iYP;zJxl@G$j2d7hEPpTfdo^cm;*a70<^N9j07?H%}Mo3$*( z0@r?t2X1wpTb|q(SR~@O$`;F)=H<9DXYUj~UZ1xI z8YSLRX(YN$+!rOzgB*ZN*sc3-PhQ z`vCdI4ibFJI7Fc_t061uU$PE9wg@aX|0M3u-yW48BX3H;BEF_8)k$UDrV&8;n*jb> zZSbHn{}Mom(7z7@SOG%?LP7vsxI;a0(M1onykTFrxh?QThkOKq4}#O-h$8n4b?}qe zi}Y3-tJq{~aT0P Gt;f0PLii5TR_vd@p z<(Q>xIu2o@?Wcubsj)Q%*S(cl^Mz#dTNdIy<-C!~s0OAOR+1-EAcrd;KchDGb-nq~ zB@7^|Ul|e{pB@~n7L7F2tLhZn{86b4mmkv>xR+=rvTa?K6$M9hN}AI1bXl*xAGx>+ za<4V98bp7w%kYfbN~p^>EZl|&O*zMHh&o@YVU2K>Teed!s7Zk0%>uNLYDj@y16p`3 zpMLQckMj*IvmIsc)9VC|O5FOAmuTotOC5rdjbwW~%w=xSilRO3I87}~A{6RbIVAG$ zbu%#hG!}vjvV_wtBGy6oYj4^z6I<_P &aoANZr<^2Lc@_{|sTo`1 zjzGAeaqO(daf!XjS}KDn-ibrmo_ycxvh6R4-REfB1}Gea)zhoxJs+wAapwO3YH! zmq~BH7$}XsIEgJK?F02DIRY`kX0wj{vu~g2Fmj8!>>_$?R@)Hh?gJy-HEImQ>D#=% z=iVyc=je(XH7x|vd>_w$n9OR^H~cx3ef<9sfky-$5qL!45rIbp9uas%;1PjG1RfE1 zMBovDM+6=bctqe4fky-$5qL!45rIbp9uas%;1PjG1RfE1MBovDM+6=bctqe4fky-$ z5qL!45rIbp9uas%;1PlUDFn`+3l|YNVW73Mtit11sUR0Jbi|(4v0fwGHhS2WkT *C`?EbUc_UE>sdSAM>__lp+jxH 1776(#^Rm@HmD zUNDRT10I1)L|LW95uLKE0RqF3GJq;B7+7j|zGyX~vP7z(q%q=F-Jz6tzN?|6IPIHx zIm(kkV9ftHihzAnyl~tCNDwVqINmZ%?ho1K=EC+f^^n=ehth>>c^~DbA@a98%o2zO z$gy9AEOaa55=k5Xk KfvCZ!Zt^ zG%_StKR^&t95XXZtO^PPq<^Rv9x(?E^DKQfwE&I+oO4sQQsyJW;_o^>5t>7JW#2WS z6(% VxX @F-fifb-x)cebUs*edr!Gtzke1gk;!{BgseSJlNfNtV~{JE zqjYDjgztVGk=^s|O>%SPjd1PFdFOKH%}No^)v0fV&7J?(leM!Sy_Jp J+s zWOTJV@d=K AQj@d)zeFdw`VRiedA11*TNb zyH>&Tpl_$Suzpo9h#~pjCY gb+ ze8JKy_FV6qyG*s@#XW$~QRU=D&~@3_!+c;K?qT$FVRVe(NTi)RMPc5wqB|r+RMH3U zWX;xD8#4P|91a`%y*g|RxE4p}f|i`2_WIFM@bm{g&R8??56{i(fox|v_kCkfZIwCy z`|U;42^Txu`K9xBZyT={ecv8P%&wkmeF-|)Lq`?&-e-yLWZ84V7V3d&yXP|!opplQ zO*I_)%ys1tNrOoqHXy?iDCH CVedGqI_y{|YWcCXHG}|~Q3`WlVzSbVM zYBixO!4-aosyX+eKlmQHcsmeveLG)0pgZnx^!}{e>#Sp*u%-$&m+^ihJxaed!+7Q6 zBC){Hifr*aT$*Xnx*y@!6GYVVlRi4Jme(hk>$5FzirI>aC=OXIca|>bvkj}7i(HFw z?eX(+_4>rmuA2nMEWq@0d%;iG3<-O@_yN60 E7H7NOXWoZyV$m+T!g_`_ z^|@c>joQUri;~Wa`WfQg`8A%21BK-`db4TAp~>>@L4|zp`q@Me7|%L`VH1Q*xZRTs z$3pem^PINwqmI;jh-F}NkFVHP_|>9RX99NYT$W<*es8V)Q(kXTxH K_O)Kk z$KIAA#>OAQkF#7aZ;fAN9%pIZT~B}hA?vhyN0)O0Ix;vqJHMjHrRgJEzjXfaR#j@q ztY(r|zG1p}l>EnJ5}Fgr=JS9oiZB(a+4d3JLCM!V+B3rI%x|(eepFi4uegprX%HOa zZnJ>NbJ)nM7{pTKjlAXiL7nRv#83`*v^)qi*m?YEsA3e49L7 8r{$3!`cxA0iPbjtHdde*X_gbs8E*|rT zQ1O%NGlAPk2FJZ3Q>~YyrFX8C=(^#A)5kj-32G7cra3Pd$FwSj^e?$E9NzdT2?kVI zy~~n_+F= A$3n*=h&7xJxb~@i_``*zZIa7w5za(95;-NU)t%7*h{izw R2W)F+@-~u><-zb zBOFbXJGd+GIX>~c{SJZI+MtHpO5f3H$3PXiLsk5op-@wmUX|2UW6`HVwwo_pn)CHO zpK{s!-sAzpRNg%C3o9$Zyd 9ax;c?ZK8s)!Y(=aNZY z8w?8A>P=Pbu~@R57wyWz%rV5KT<5l|)2$Y(Ll6k5m%Y}PAd5Vw;h8vwm1vvNnPp6} zu8F=>dq$kOO(r_`dgI;F{H<)g&ki8E-d__y@P)CW_#WbxL@OGKT`)J&==td~ffR-t zIbWJ(cl9>;$1iJ(A5CRkwh;&7>a t}=U>iE69DUt%Ko18gqNp7lH z3HBsbbK6d(OED};rxXb2mwST~ z5fU#g?%vya9y4EtWKB<;HEC^*U%wMPdR=*MfB#W!aDOobgQ@O*mtnCp=?(4C(fJyO zVylGAlGocXGn4C_ikAC4hKlx5bmdvx-j0~1cY7q)i(Y=JQj2QvdKyAJs4WEpe%}&5 z`?| WJ{uO-&a7D%G|AU1m| zzUF(ghs_MPo`khRNcb+v?>m>bHC)2hE-BE$qLznd1$!~u7LJbU-pU3PuCDTYS#{ym z`Vh(~=CVCiUYAul%5xh#3POSRGIl#f(=(8>5mAt8myUx)Xrf|d++OjPWuR{Se5+LG zdsatSSTq% 5+!5b#WfBKyU*2CS4wJ-l|H>P z8Ar$_clEj*E};ul&Y=0~H1|R04pqYXBz|v;DKRzFoqmLD@f~~UXV)ash)=jqirG%| z1-DP I5h`cbW)tnwwNll^w0-%D88 zbu=19u|>&6v(dXWY1k?X@bY%9p7R~Fti}wbG8?qJAJw!SwC;?yG3&RxUrwyA7M@KA zr0&1HuF16X-FdO(IaJNH g&Lw!)m&dYiO^i;#sc!?RFpbLY}qARDNF}2m-BcCv`&&dpDy{*{h zvg|B9nKQkk5uIT|B*OMg75X`CsgAy@sYnHS6a7;U9#G>uc$ {%}xa~l{99TU=gLqRF$0@=bo2O!-!Z_xUA7-CA zN6-_Bs?5O;i(8gS43Z&~D!!T@)jyL*F_hITOZ0!~UtOM%vMsyS%OHn PKOH^Pj}>VE0kdWkKGPoYJjiW4H~`RwiGr=A5dRE*{-}+Z}Is?dxy3 z`L@d!FW+;|#anN_?dF?rx%n-(+ ciwU5EjQo%);GW9&O7e9^RBxtUb=Ys`0_f} zb*>7mdOR_KiNcl!2H+Ij>4fkf6za(k8juL3?KiD)8zNy1lI`LY4}gf4ACY6|o`SIX zO{x%s)@7rC!~P@fD5{13bgC+J0uwZ=!#{-PL%9IUmi-4GtESJXJc@x&b;j>%VzsY2 z6-sTb+8y~6t#7JmZ4ul;_C73km(4dCwaD&5vKDFvP9v-Kx8I8tiH)^eH5nx->RwNa zt3^dU;S-~U*otPFNBNJ3SmfXMIT<#DbkS}fEkvcA ihv5 zvZm=2O)FYNct3TAN7XS)UzR$0dxk!yDG8*Pc29|asVU?$sM1t%kn(?E+f!FS$de1t zoS|`Q*0g$d%9u@Q;}8>T&8gA;VhW{QXQ7N!=fq0QXnMV}2X%-{oz~sU*oC9uV0Uog z!gUXQ=tHl&?!w{ubIY=vyYAdWZoKi)kABP}ANh#KJ>fA=ddd@@@TAAz_>dcRyWMUq z=g*%%I6N@ljOGAw=cYFR4KAC(ac`)0{o-e^wctt=-&Ti?3u-**(F#E0K?!p7H0mH% zvCW0pBmJ9{{dJ;u3X%phztruaBC69&$g(WU7{gSB K4JnhtG8e533v#NV(hBUZh z<@;HGMd6ppsW-GNLRJc|lx%;Y>atY~1ck8hMIpRl1GlzuinE`^h3qalzBA`?g=VX5 zr9krz3doRA++eAwZ1orsYIkP)r|BGgF}&;JGs-|%p9|v(mYJTd%~;l`z(&|pBZyC^ z7fSj75sy}+3~bvjG_0^07iDi_I?$@*gsg=uxZzy*i)uN@M^K)jsYr+3vz5PijHDtI z_1%Xgd#&vjn?Y2v Dl?5Ks^StP)Mz#>FFNOhP+`Cf_C9r8 zbv@SO6?4U^bk-?!$Z|N&-E_lE7cN|X;f4!0-SqIMzV}m~^0X&E@ySnk)MFlb(<2^s z<3nz|{=$Xx=gwV!{e|7Z0>xDGzf4HYnO4cYuN3hYTlRnQ?S9+74c|5z@1AG{pz+|t z9?_R8=k2MV*T9MLd08q25hpDowseWeZdoFfb a?z#K6i?>}`FHWqB?1nDq#`y)iF<|yE50JnR89K>w#X}#g z9Fz1_fYmn*LqsgDSPB`j;?p?}F4(X&djb%dsv_frB{*vxOfNhH!fW^}k{-*XgkiD3 zlWVE9o$FFuT$Su!?1hLbhKxmI;o@v2WF!8j4NRS@Du5|E&BP~|z;r{O#VX98q)O{= zs7c1ftktX~Cnl1MlcozM>sZ5MSjQmd3iWsc#q_2ZQ74~9e>Nddg*n?rPDbmeRrx6* znB7t6oyYM6l^o8Yg&r$G-aq owfDRefwocAC2K;(J6%*=V6{O~D9}@zSCl+gn zN45 wBI2;TJnBl_W?UB^g#s8DrOj0U zk%8jYV>|f9da4IN?6X%b`W>5qJ!3I6W*Fe*K2Ftjh}^QTI_-K}YEy4e_Xvay8}SrV zL8ASmM>WTIj7?CBOtm638NZLw;#vAm1TypbP(l4YCmnOXiO2$ssXAoX2jQJ_itp4v zRGXL>s-EPYweV<(k102LaDa;f>s+U B?0@yLh& zvBy338P9zGr@zlrAM@BpJ>;P`KH?D%f5ammesH+6x~%KET0m1M+GVl>Gy MQOkf9ZT#)AOl?)Ngio>T))DgdNC_DM3>v>ORv_IL<@doEqP z_2%2&^7@-^z4`Xn{oWg1`8%)r&6oY=Yk&83H{X2A9T#uAG%pV97VHk>a9|va-ELWS zV*zA{%xQczBMRKEHjVChxVyTs+bO0x*FhyZWbaOAStaP^8ei+C0wNYj 8_inAGKA*DSo2vGkN;6~0w@IQ)Cukt ztn~&XyV*8YP1+ij(F@hY3DDpg?@n4$LshVL2v ft@k z_A%PG;ris?wWPI6HD6Fen$# wm z&=O+&3Y$7zpy~u&CCm^I8B@)5M?I9v;nZnHeH*y~sIED1ylgh7xn`v76jul> hRKKR3)`ItZUs7F8M zkq>{wO*h_n!!pdQ N<>k(${@jx)2DziPpg)V z)M9%GebEKXY%DTlELMPw8Yky7>aWwZ7XWMQ0f;D(ygx`P>=RxfFoLyjS``wJ%KzDt zB)l7fClswphfCN8Haf;8e1mFNHWEu2v@>WWd*o#Q@~gD(nmIp=+*6^#?c0 9^e%}^vyCVk_6g(e812P< zM^sQnp>xhD(@C@YOr}{hByYHBB?T0dvg8^gl5 5=k_B#K9KV}Id?D)7BTDo z7&2mIOXsqZQh}@?)e-@%`|yj@BVsYRf6ggTy5^H(#IK6mIpdC#=qkCN0&8-X5xm8y z6?u $k8ZGcJiH!>d`^!B^WMw(2 zy5v2*E}LK0Ld7cuu*eYgzEE);pb!yi8R}eagaV*SaTF)%3P7eYQ5Lm^hizXt1q(jp z{|Q88$l|2m6-(RiVH)FL>v+k<)xtE_Q^8Z^2GjCfRK1AQ(5Y=GO`1JeTHL!JMkw#- z!_^B?JQ5V`V#fB#Cxh_-$}LN@?K&2Qq!m;EbP= s~!y*JK5cE;l@Wk>fulS6Hj~J_kH>YJoEjY z_VlMb{*OQI!VMR8y9L0U$LnhD5eU6iDJ~lYIJ>_}LV0^duHR}v DuY93UotPq+?#_mC z!Btx7ot=}ohN;w X<(082U>y83(lCbk?#=WVWsVXC2R4Jf+pW3rRKYZVz9fcv zpkXN^fuvtW{I6)_0r%b FOgs!GNH6uA|xCNqNH$?}SoCsk- zS71WYQhTaufho*R6}Pj1_#wkSnTnP2# ||svvgnWD`+ymK zgSdll9@t~eChf}cYkf{ryE-dpC5tbMl7|qz+78hRF4ZB$QfOHW2S$HI3_L>UeP6nD zsH;gnTKBq=6BqLC6gLrp+NM|&)2U@xLuA@+Rz~iLcN>-V9BZ5_QgRS_d}w(bj#-|# zo)@~GEjk+1l2oDQOOQn{zQW2*&hVPoHmn3bMB=xE~;jJ2iUKyGq1^2{+_e!UNz{ zp!%{=TI%44297DrXMcgnI6hvd&ZBvxI942=Ke+x8H$D8l-uub#`~FY=;1B!2XFTIi zyvKVy;fCukEXz=x>+#W?YVNBc`>Og0y3bN=$bFHFy-#b28;y4lv;xq0cfn~yfAcly zldab~6HwjGyL7vg!tTaS#yCD+Z@KmM*S+R-zx+$T_R|0Qi!c9;m%sUs-g3(wHy`T} zAm`=!o$PkYPA~? |Zr)|GdGF9gV1CVbWLoOX^%D&`WI1Y!+pc z>$+DR9$3FIbWiSQ;84omIiUcA5&|%aiC6mPDa^wC&Y3th6oA|iznP3GjZVeMZ)+WC zYR|O9z8XRp^OviL5%5OAFuS_Ck($v`MXrTc(+U!Mdw^2r)P=m(!e;|6@JdxkeI`)| z#(-G{)KxG0wF2v0VN;57w-cs7r@HH|cCk;=B7pB|*pvC}Yw`3|wohWZqLo#P$Qstb z<;&F!*Bus%F%*c(6mu8O_42%YtjCI@!`=BuKH?Eid;h1u&wD@ZLq7Bap83Ha@PsEk z{`}#=7(?f}dd3CLSU~9a+0p4M2$?yl5^SJ-u5jbs39SG$-d&K3=}8j`_9%WX30l}M zX_J%Uv5bS=PGsD3>Ei3(@TOn>#b5ugKmXES`R~8-2e18ux8DAisZ%h{$@xW=Wh`P$ ziI#F(YDpA~)(l%9J?p {bHRjJV;iXcXA g~O+h5<^=E-bbl-r^A*vdmAg)iw9102BQ!U3)jsBMuE^yQi+h)&F3{C?kdm%;P z`kQ6Di(M)-fmfdZ82F@4h^y98DQXr?YT>MeGOUo+`>FWnNHLeg5E)b5>f!Yj0mP4M z!&F`E#Z(2Rz;@tFdrk94?Jo@acG%Dg_!y)kDB6@skU0U?I-Qt*a?$|ciE7=5W7T9& z2o&@3@zJqfUiISn-3^a_+#maZ4}Qk`z2DP6=tDo?{oen5AN$xxA0F&f^=LhIg@UtR zj#b%y$WfJ}^fXN%bcRObU5-`&8t+P6#p<85L^sz GPGFpq3kPzrTNbE@%A7SiHn@?05;{O^ZA9cg zH~y1lwQqx-8l3Ohnmx@_=JeF;RTitFp7kDJMj$S4zUppRX;FsUnrQ#Cq&%elwf!Pt zrNr`xU|A%NEccdz09^aGRI~h}^_IS(y9;Jy_^zi|u?2Lm&A}|Fa1Dy&H`qU<5J4%A zURixtzG7RbVfAneCF_%Hf2tz9m~@R@m=DWs5&-=Y8MhOuNh?I-$SY-7Niqi4P{>1_ za)A#C1prX@%viugaw7#q>h#SXjILD6loDEOR7b @5%snCe%ld)6o} zA*0kBV&Dk!{X9|B7#8A!?|Ot I`bCSy^Y%nrWCoA@_mrp;Oq7nxd{z0Z?|dy|HOw&guOt!*m{%sZ0!UdA)1Z%Odiy z3y*xyCqMZkKI+3i^uwO{jAy>j O49+);Zk;mU4u>e~|RP%#~MQ{8#Z6 zG#c*&v;xq0*Wl^_KUescktM42ud2>D$1)BM4xxJUt+)TqD_-@V|I>f{rC<7Qzy2$~ z@#b6KFz4~W;ZCkQTy_F1Dyn|2tRR=^VqBMS^uxC;nt^kWJWv5hqj%c3HpDU4RM2>= zO!#9<@>I;JQ)2Bbw!6a9Jf*c1>mO(}52{T2uXz?|G4v8fE5^!`I>Ojb9#R#d)FZ-P z&U*@76r^<9Xbklvj7o;0Ma^PcZHh^ T>EkdxHPsi{V` zBCtiH6O(_|jf~!=_Jm@F+}Oz34xui1N__fyicD0}Le7{Mm|Uqa|C2z(H<8PpKBA@l zg?!XryS=keC2aaDDlEXL0s;ybjh$pv7=woL*n<)Wfw_V}o!RNW8Qhh&cgq))W5nBG zmdC5@Qb@uo>2-0{C54oO0jg_`-(-}=Y}w6>ZDW~3me^1twCmfw%&g$`h2`(f0g+mu zGG7*57c#h4h5}!e8f9RfmYpg({bla_2~iA WR`5ThXhOBfNu;V^J z8p&l04WbUwauKKic(L8&uS`f_Tdr1?wrVdisN#q~@$qY-Livn7oJG?yIbjmj7)Lq9 zZ9M?2ox(g8_SW{XaGL_BAHfirimCJRyk{A6x9pzu9#4A42fW{({n(HEus`|1@AcI8 zI)ComI_Krfm!Y<|$xe3J_XO}&ZLPxf%WP9a(Hf0+K3V~2ybExW-|IFFc3wuBTd%Q| zc3thLE9`a$B69b|OTYE2ulR+Z|D}Kb)Bo|;e)(7LxcF8;c5>ap?!56&nZ7%;Zljdq z^|ovhPW5QvQDDd-V%r(T^PISKPm*hrQRf0smO79=f^^W=JZBZF>Wou&wymR1J!ti; z-}r*39^~VeEdY?%+gE5-G-Q~nl1bDT+qbcZlGIw#ls&>oOy?+GfXzA#1;gtLjVSdA zstqKSvHGXMJouNhR7u(qk2GzHZLYRomTktG)5oIGd0~OxG=jIFf6K>O*NnJ6k6X1L zvV|m{>s&(s3;hvlZN}908(&qZ(@?fxdTWGg6iVjGmyxxR{H~J7f^0`&5Yb-$JH?`w zX5KA`j6r&8dyD;GyKsofFg?NQQ8R&3GpU+(j3Kx0vVqz2MQr)kYBa@JDF;D`bT1WD zk&O=uLG-B_ag2|61}|2oh0k+s%@;y3t%nI{%8hK=Kl_O_l~OPD!~Q&*=A<7?OM&;= zK)Iu)c4pBB?3LRZic^Vg#VQH_8Cs?+jysezT{e8AJgd-d^E2oKbTu8oH{Iw|Rb2s{ zI$f=zjQUtp+Yau+Ri#+yBlXxn4kNSKC}X82s)4{Ot#L+Q4r|(6Y;@C3q5#ibiN-eJ zTTFb{d3-!Ct+;q@x$d!#e%y!rsSo=9{OJ$-vw!|0-}5Q&x!Wzr$47IWfck `MrvpQ?O5szp84+69#)v!%vE~>1_l`Su*Gy@KV~|~0UZZ$a=_4yJw>ZNs@mWY zX53P-y=6GoQsZy>Z2QMX|N09*!PZCC-NcJOlR>lquL6wd@g(m_ p480MOp{EZ?N zfYMj~4M|lzaPOK8f#dT }qGverqlL1kQ(pdvX{&6H18R!R5_wImA^Nt9WP= zG0&N=A{8dn;+*|krM1>Io*2jTDO4xY4=^WdR+5DB=P~FZ7|Wifobz$iu8xjui ~DCw%PF-{ !qBpEgwz!OKsF$-Y18_3qw&r`D*%mm3JN7HADUTI^P)OKm-Uhs zTAg!_A!F aYIBcYo*i{o;Rl>F>VwRR9*b{%|=^h+xb$LrI;oScW(CtUj_? zIHl+SVW<+?4u^Yt5!-*$^m~FUN4#&+TJY>7r;={%_lzIH2wGc@L>Si3q8M49G-!ii zKeV-rh(t1d<#Ian7&Xw4rlq{JU&@~1Rjh_Z=g{rjQ~H*h1cH<>bOf3!9eXxcz#0o) zx+{3SKX=ylBU=zR`LPHV{y)fmOt%p~Kmg{K(Aa~4++?|x1Ys;tZ4*G9N=8Ub Y3j WZtR5t2l~kCDf)6e!E%yIMEx;}0}BM5MNi5Gj+VRB0@C6m?-$U7;#@HgHnr z+V3RKaC#0=U9)YnU+w9d=RG6%bR_T%_mTT5kk^XYNk+_Xf|8IFQ4c>GA?6|gbMjaK zPY>!rK}z>q^qp;GQ9IDuRE_G0@V;ajm6eIw+DsT>b!w@QIrr5H#@NgX+WeUcPZ1iI zsXAZ &u1Cj5B6g%?%`vtA ze~y&X=hD^z8t+WB0?>G8px9TodO)TB?ibH9Ba5%uqi=W10YqN^`ZxaE&;6Gl`Qab? zh5!7Ew_kkgg7dp^eu${bz?fm@E|0RMZ!jL0H{VRRB`(P55tEuj#7?jB3EGZ5${_nN z_iXJu_OXWtSDi+6in;#HUL~8vdFiA!6L_&E@gAxk8XH=SoA_dL0McfbdQ`k6MT>}G zDhQ>I#)eo>mDtnH=nn8qg+>h7{5;XA*tUh`B&jN#D_FW
m;Bt)8 KOY7T!f_Zwb zNy`fsgi?8i2CJ%}@H+dHLj8=gTH4dSxd`Oxel(#oSDnz+#M=opYamqk-T&eG7B%F( zIh8&Tgj)ji=hzk*MmBA-oY56R1BX4F%1&xQ;8^wuItE|a`CsPEh% M$1Yj8p!{CHB0SyG@!;;jEQrhPSH{N+@ z1)%Yc#U_kr?{ld$dcLS{&Cax2ma*)n&R6~J@BhH}zvzYk@`bPaqu-x;bZ$I!$j-z# zOo>5yjD`hO5<16&pd@z#01R0;1-mjpKd@)QHpV8A26SsE%3G0ih3{f(N;gxbDz~z^ zyWDp|X2S^Cx>J6EQ3VlKaw0S+%zi-)W3{#z+{jWxvKaT?wD+E^ns3}^5{YiAhGqXw z&s-EWbtYMKfT*eDQkU18Y1{WVvJRR}NzcYsm_z`HuxWM6B14AA2DP-mAe{c86HO1C zsQswes6fFHH+qQCijbK&TFa$4=v%aj=K4RW08l%cba(&&&HpADE(4)p#PArDE>GF1 zj6#xa<9XSc3=uI%T;o-U9R}>;>l_bMd&rH`uCFQB0$^k#i-;^pTeznIg@CCfM>J`) zUJ*B;nxtSrOH%)h0L-b3hR?$>$i+#)OYBV q1}_j&r^!E$tTSyjy&5R5-O9QBj-1~lG f9Y1EMvL#_S^rTpLyv|{=~okx&Qm;U;n1p2@VdIb9U`y5NKIoqz0Dw zBx~oD(iQG{0vUrb7tdcF{^*Dgmc=cdiXqDwV!4BYP-`r@Ty6_f+05d~YrK}C%;7VY zajnAU34 S(WgE;rp?@iK}>H~s9$cEFnozjSv-Cj|DD+1d&>FPu%K->cm?U11ng<>e%lpzFc zXS;;C^M(DG`!{$8Dc3U&1ukS}04_>SP!qPBWTe&W+rb?-pV~*b3R%Q69>AI0;fk zD8$r?5%z2by|62lCknHD!tbG=rM7-9Vt1nwp@63cwGu7PkkpA1{bot^hAS7Q;R~-v zbg%aXb7O&ed5}k12av%|n*SFV`Gy8vrUPRMeMDl&c(zOGZz|RK$fKw{Wu?SJW{5`_ zJ8AE?98h6UAUpRuM=vR>I7zYBJj2@S?&^@6x0$(YpC)S&E5P%Hh3tj=Q(i=j?2^sG z_5*}!4@!0MVHS3s(i~$`Euq<*nb&ywapaUPADiX2-sw^zHU*g6&y($`9J4$yfg(~= z6ZK8i8|^`Z>T~`CJbOT>n(?)5jON;5%3e%aN#Q*ol&I~OI9`t~&$}LW<0Jm`|Mua3 z^V2^0WB&X{-FU-=qod1n&M`a_uV8a0$-Cv9PomX;#ybwJ05sk)IL-3E_n9Knx8zsT z(#okGo;wuC>)-gsAAj+`|Dk{N!k7K_%Py}Mha8^UU8juX*t@^6|KLcmDhe<>{Kwq- z+y=~tll~f;3O~NkR #2H}jl7p>zf14Q)p=&ttNRtqP$* z_csEPO{v9bmrX1H)B^h@_j~{}3qtm9SV7&(*Z#bJ(71?Ch`-S$*oy&U(^B(b95z*= zLMemNDG(hVjjz!xPTQk6&UYmW=0 6frj950?j#*L+lK>`$RpF*eHA}bqt@DZiaWW|UOGU<*q4+9Hz&gGv@lL%EJ z>4*3s-YTJ1q_Vd^t4!ZZLkC@yQGIA?c#7zHGB3wO>f0`pLk17c&AG?gditDT#`CE> zGuSJo$#LQSir~aWRq-ev6HeP5F|s{r)?n{vv2AYi2~a{}b3A%OQQoM^L@U`$W*&@4 z461e-K=cb(cPqI&NQxYC9##*?$p_ezXKZYrsw?7v!ZPsrt*IT8g^Ta&gb ba_2K9%GC# zay)L`0RfIR?4i|w#ybM705skK*e=-EQvbczT)WKVuT%B>xpV8f{`SjX`J+GZlArvE zfA`AYdnFWy P zTjnD9tg;lQO ` zjbYn>^w{h(zHKA3;i5db3pwwRu>tCC7^Piys{oYW=Vw7s*h+f}fH4%EEQ1s>G=)JS z`|Yy@ZHl$DMu@z9g{JIm^vqWSL|Xx<%$aHc%O4sAijt6Z=X3p*r3r)zfMzRc#7~Dk z(C1R2^G#wqh|XY8#Ncj66%_12Jn!>dln!Ig0_1gZ2VzM&aw>N zlb@5D8pGf0(+C;kO_T!)(Wuc)egz4-tM7n}F;aU`Q0Ypo&wFQ<-O)OoCHmk|FaRb; z38xGvg-|fAT}zq8H@5mj79`$n=4Lx*+`kEuy1vMBh+QD)CqWf-tCP}iA|)3nqKikw z_lF;clbdeZpNdl%lXN=Tb3^78sw+1EE9T05O+pV_C@PvV6U`1p>>u?$L|q^#yS+^+ zA`t?KcwEmZ)%N_vw5SUDzBu|OXMXWR7(?gTZi>>-< Ry6I j^-!DeY+D7`Jb51=tH~{dQzwwG6{FgucZ(jV8*T3}-AP48hbt1!(F2{n;Og?4J zcf=wd?`?i}yS|1+un@I!Bu&tr!FAE-%NShW%?~hS?9JLr>X3&AbHyt!eW_4izb&u0 zsICk-CUveS1S2^CmuH3e zVw{AMYi%hQ-)mW3?K5tVHsf>hQ58E)ReXb^-;P;;B<~RIP@z>JnPe)F8ztD&2ziV8 zk)~I*0m-rQNs`sl2)Sm?`&!4?zR9aIVm_tSIT|~|jk-|=;}jY-&k!bLe0$IGT1 zfG4Giptv%k!nvTvtx4Y9I?N~_doLnOY6YaoC~_s#0r>meMr&qfqt4UQ0EqRO^HcAU zSi+o?7ySSs2JW2@84z%HvWG?StmV|!6}GFDRNgSzN9u1G`xms5DpS10V0Rds4h!$} z=)Vy*?CCx|U0vaEoBm{#m77(z-V(1lKd=c<`MM5*$|9NPhy5ByIOhh6Y)T(j5_?D_ zw;b8P_gd&`@jnx)x&oNGGDP1=IXI9RjYRpZ9fk G`|HuFM Qz%mLVq4%|ggY z)szbIC{jRM{Fd;K6TS&Jxm|gY=a{+yE$MOSFt8Ay^p*H=wj{`YCa5=CQbl$9ock6? z&p>lzQ2_S(VUK4kvRzaG3_+gOlk|*ik*07e9=s?jCa%)t^Sr#YQ!2OHZr0Jw>$0Ts zPuU8Q_D;6=5I^%vowS8^KxR&Uoa^@Hd4)lVodb%gh{z3LaERwcN1n+*isrgcwNG!V zQa)EUqBFT5YuVHreu0#M{4-q+8EmE*A>_;q<-P-<4TKT5YKB3ilP#jiif})S?EM6G zB3V+!LP|I^1W*jwQAZH*Y`BbSVrLBuKQ|Y7pBeNk_K?^H*`hr7zzDCJ3&Eb8JME(d zml=TU4bsC>MzS!1rK)l^FeWq=fS6B)b5S-3TWi=E%Pdh5Wv>b)*LK0feI2Tk`DwMN z)S5&;W;WdnY>tr}6s#%BS$+)&hRTvm&;@ns^PWMXZup(HFCh%s7NR&NJ2(&~+Mp zpQ>|W<;0-1K{eXL9i9UCTu7%=7)N+EvkSLRlu;-(c}e?#7gST0z=X?wA5~SJRX7;) zR?8N43I%+RO2}T!+0mhOvV$_nY77y@a-^3bM^Al^Cx6Cs{>CSL(qDS)V;>FZoU3?5 z(1y_O@&41eeyk~t#@mWk02*&A?v45@%;lZar1D>#bMAHrBI6JL@QpwEq96Z(@BM+7 zzv|@&a^Y|}1jg}d=^eH3q%Bz_CB^3T#$?IKa^IpNVL{|IHL8bU2?EHF1#a|mpRF;6 zl0_sr6-{TpL=BP{F5#@Zt)4>r88pwY1^7vlL@=ZP?olD#Wqr3x7{rtI$hO@I_bXX< zD(zOPJs?fsCs{oJf)SjT9m+D3f-Au6Lr!I=0BjnpP ^qOS&MoXk4Jeedl-*M*Sw8seFWHnw zlY+pt7`Jhn(Y@}K7Lq3^xp}bJ0?RA*)sh@Ln}F^V*kt5VAD9Y&vuTf#wtLY su2W7j>7NNVDEp3WF5V=yC3%FLW-m}>GO1VU6ZBxG0!?{9 zQ9mdXRaas#)v^p#y)^Fuj^Fnw@B6o&^?&@ezxGLwdCbF)*W;;bd4f`#sKac_@;7<{ zvZ)%42aHw#8gCPnG{IS*<}ppD*!#iQTGct{GM3$L_vW|W@}ocWWB>YJyx=!~`{e>T zzg!25b=@o~_NYP=Rf4oQZA%_*Hc?3Y=2Qh>pMFNr0>aX7--`&`T^qL=;%5VEA<%vw zf0KO9S-WISTmc}(=U3B1i=9I0b~)jCOO|4F1t0CbFlV95$uCQgkS4iz<9K#t 5Iz2$5Ci^f^1VuZ=}dsIR>Ek1;;N(I3DIaLAh!feZy r1TMxoi~lsU~tm+UTf4a5^=GRJ?|=8e_S#1B!};o0yY zI>foWI>e4mN?sX(b{gm7deYl%KKYOox24ZvLe05?N&113#YpUE*Eryh`i77=3W5mQ z-KOLl`si%jq`s#VMXS83$}o4D1u>D><>2SQ4>_13P+g%bHzH5W5(KlS?|9+%#$ZNF zaRf#$bAA96%NP<$8bk#PhABmq)leK<6cO!XXDW-#nfX >`(s-f8paF_OKg|*CU-ehDf-_bfXFg$$pH% zy?*bApvD76D*%lLjzVZ{h104Dz@yz`()PuA&M7i>%kGXl?|R9P{?zlo?R$Uyw|`A^ zxo){2GS*{{q<3F?+IxVUvghy^{W_B;q(rO71R7rsCc@L4%1I3Yz;fXUsj#@oe?uc= zz>fw4Ig>I1I+T!T9brxnIIY)eFrNO4SoOkyq*|8wIQA$$Ql>dK21$FCI)wsPHrck4 zYDvh5AhP0>bN#2-v&kp6+2~{46J`i0hnOmu)-WDK8s~jl)-C=B$&{?7(-o|tc?bbW zJ3K(1ZWFEdq@1Yg49%^y0<|czHy&2%mOtzJMHW$5PMZj`F?nYAR%pEngk2LTjkald z7M|qHIZbGDE+-zD65Kluuv2&(^IkPe?ncrnNmYOnMP?*2s=i@YYO!PkB^<1lKW{2* zI{VmT(r8_dIDV%5mjUsO_+Ea201FicZm^Jr=5xFCROj80l%FUu8 CxF>;qm3LI$CmX_3)hMZ>aFm*`=opp))k~dP$7Td^xP;!xX0~_7O7lO5 3e!hQ;k(=dVFjOn>S`%~ z1n@MVp82QRKCB-#%9gUo4vhdX$q*`ynM$)&L$0GUeGZ|$Khm&r_Vun#@2mHUYjw74 z1?8q1EI@HXRVBaDaq69!Z=$FW5RoxNp_kW7hdcR*kNDG{{hUwxn2-6W>#jS0`REcL zBg50!+MH-}W-BTJ_5M}>8V?Yy05l#TPDK6$)3=GqEozfY-U5CA9PSP;UOxJdKlQWE z|JHy0(*OR_b?SxPjRMRyCR|@(S1um)F(iogIxcqGNknfe*K=@T^YvKF`sK?3r&@Je ztPFu4SL94lAenbNvH%Gni{}E8EHQ1dK@?cj;QdqG_AJPJ)m9CU``7ef*R@tH!bKHB zhN}>yyd(11=(GYG$&+9btTo(D79`tqU(}s&o8>0{F}@uuc5+;V(xOgc{NB^0!k#tE zD7c12zb@pN7so#zzVMT043O%RiaCwm{7{0PDueR@aC3GL(;3V)CCmK)%32M`VuaQA zd!)i?Asi}x;L+qJa&;=?{VQ>(T&ReJ9Zt8M+uf|>LCb6DOaTzL`1)o?Vcj>q#m=># zxtGi-UG}f|f61bC%UOjyEB4wtqx7o~B9Hi75IhTl1>P&JJDIO+jKv?U9(LmABv5aF z%Aga$8nGOYM@22-NH{>qmwcd2U157RC<6lUSXP&r=oczZoF6Jp ^z4jR%5O02=oXCwPB; zqKJh^>sJb$>s07&S%C51Uiv@3^_#xyKmEs_zPR3X{c_V7LsyTpG{o#oi%YTJ`UOFK zI9)sML)b@^lv?s6Q%EeyNuTnbWLZYHVpbIz{Tbg20h4CXJe)TS>tS6g09c5xk;`b7 zTx>Pm_l1UUd vr0_DQp1y>a>eGfy`W0K5gB|Wgv+%ag zBdi;yr|1ydyRBMVz{`w?drqW!! z6f@FQC7}s*mh9h6%-p`5=6YsF31<#eZ99oj*va_#CCgJ vYccKd9*7SB0>V^ zZzVE>I-7KBe@N_LDeN|LPFM~cK)mDj*niR>d(70;r76$)R@;XYN8kl_quwE<>2e#n zbxPEW;XpwNV`OREip@-oshV*2_cFP*QR@X@WeHf#4HLC~XXxM@t~z;Oi^(lVdu6;n z5BY9a%RfW)P;CzYHOiRZKmzJY0(g4&{@iYl@iDckrvaV1`dGmP?E<=2AZ7EZ$&=Qz z3-+gH-hr><^J+$+X+glg+q8ns9ug*mXiV-3Sp7tE>IEztZ5G1L$nMj+K*i`O(Urxd z1r6nMOc7b<@nyaJ(GPv>|M6*m?OD(M%qKnR2}kSEdUQM%KW&D~8t3j<>0%YDZPaQ& z<9?zQfX4m6N%kKf=HF_PGPX2TL=F!RU;XOWe#bxi7eDxYFMR7=uRlL-JdgwBN;j~? zG#(7zNu+&7+6TAc`Aea}g+!m*wziI1BCJ}HiwPOZfX&*$T7oC|_fl3?aeKJMh=YvE z6o5@L`AVa!eFYIS_T^Gl_#vZc7*|b-Er__%seD2Zu{!lD6%pzqVGom7uW47)sdJoA zkhZPnKN%x`pB=?EEo))qZ&BZ{2 zo*eUE#HQ6?0?`XasWrcq(+15dx&$+@t#A{Lsn7u;?m2W@c+Y~$q3cm1WUW-fSxjp& zDgZ_?;kJJ`=O;@JUDo)PDnxl6tWOAPo2RQflFdbh#ECm^#ddS99IfMh7G5oA*$_-} zr`+3OS|ClrH1*nSCz7&Moe1%)G6qxGv*xG3_+x59)0v8hdhil!IC+2j9F)CDzN(W^ zVWoZ2n5=-Ly?=P;49ody5l^lW DRsb6J6<4?YpK_+}oc8zp`SX`99lhuUKmJW$^XOdCRDCBu}%p>q4Ip>Lsc2l> D~Dx!?IKsz9YVH}$O#4x^fQI1q{X^iUUwSekiXwUwNNH+9|2+KQ=TV6391 zYL$!?S66ky&%RL!+;+ E?6L<<|Po@J{r7FF(DQR6gE-64