diff --git a/modules/adminBookings/index.js b/modules/adminBookings/index.js
index 294d673..f136b67 100644
--- a/modules/adminBookings/index.js
+++ b/modules/adminBookings/index.js
@@ -74,7 +74,7 @@ function getBookingsForAdmin(req, res) {
const total = countRow.total;
const totalPages = Math.ceil(total / limit);
- db.all(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type
+ db.all(`SELECT b.*, p.code as promocode_code, r.name as room_name
FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
LEFT JOIN rooms r ON b.room_id = r.id
@@ -112,7 +112,7 @@ function updateBookingStatus(req, res) {
db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status);
- db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type
+ db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name
FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
LEFT JOIN rooms r ON b.room_id = r.id
@@ -155,7 +155,7 @@ function updateBookingRoom(req, res) {
[room.id, room.type, basePrice, discountAmount, totalPrice, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'room', oldValue, newValue);
- db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type
+ db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name
FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
LEFT JOIN rooms r ON b.room_id = r.id
@@ -194,7 +194,7 @@ function updateBookingComment(req, res) {
db.run(`UPDATE bookings SET comment = ? WHERE id = ?`, [comment || null, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'comment', oldValue, comment || 'Нет');
- db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type
+ db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name
FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
LEFT JOIN rooms r ON b.room_id = r.id
@@ -223,7 +223,7 @@ function updateBookingDiscount(req, res) {
[discount_percent, discountAmount, totalPrice, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'discount_percent', oldValue.toString(), discount_percent.toString());
- db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type
+ db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name
FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
LEFT JOIN rooms r ON b.room_id = r.id
@@ -320,7 +320,7 @@ function updateBookingDetails(req, res) {
});
});
function finishUpdate() {
- db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name, r.type as room_type
+ db.get(`SELECT b.*, p.code as promocode_code, r.name as room_name
FROM bookings b
LEFT JOIN promocodes p ON b.promocode_id = p.id
LEFT JOIN rooms r ON b.room_id = r.id
diff --git a/modules/auth/index.js b/modules/auth/index.js
index 3b32622..899f697 100644
--- a/modules/auth/index.js
+++ b/modules/auth/index.js
@@ -49,7 +49,7 @@ function init(database, jwtSecret) {
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
- const token = authHeader && authHeader.split(' ')[1];
+ const token = (authHeader && authHeader.split(' ')[1]) || (req.cookies && req.cookies.token);
if (!token) return res.status(401).json({ error: 'Unauthorized' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
@@ -81,6 +81,7 @@ function login(req, res) {
clearRateLimit(clientIp);
const token = jwt.sign({ id: user.id, login: user.login, role: user.role }, JWT_SECRET, { expiresIn: '24h' });
+ res.cookie('token', token, { httpOnly: true, sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 });
res.json({
token,
user: { id: user.id, login: user.login, full_name: user.full_name, email: user.email, role: user.role }
diff --git a/package.json b/package.json
index 330f400..1207226 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,11 @@
{
"dependencies": {
"bcryptjs": "^3.0.3",
+ "cookie-parser": "^1.4.7",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"geoip-lite": "^2.0.3",
+ "helmet": "^8.3.0",
"jsonwebtoken": "^9.0.3",
"multer": "^1.4.5-lts.1",
"node-cron": "^4.2.1",
diff --git a/public/admin.html b/public/admin.html
index 08e8416..e7aa6f3 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -960,7 +960,7 @@ function showToast(msg, type = 'success') {
const c = document.getElementById('toastContainer');
const t = document.createElement('div');
t.className = 'toast ' + type;
- t.innerHTML = '' + msg + '';
+ t.innerHTML = '' + esc(msg) + '';
c.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
@@ -1092,12 +1092,10 @@ async function loadDashboard() {
try {
const reviewData = await api('/api/admin/reviews');
document.getElementById('statPendingReviews')?.textContent != null && (document.getElementById('statPendingReviews').textContent = reviewData.stats.pending);
- } catch(e) {}
+ } catch(e) { console.error('loadDashboard reviews failed:', e); }
loadRevenueChart('monthly');
}
-let currentChartPeriod = 'monthly';
-
async function loadRevenueChart(period) {
currentChartPeriod = period;
document.querySelectorAll('#chartWeekBtn, #chartDayBtn, [onclick*="loadRevenueChart"]').forEach(b => {
@@ -1706,26 +1704,6 @@ async function deleteReview(id) {
}
// Settings Tab Functions
-async function loadSettings() {
- try {
- const data = await api('/api/admin/settings');
- const display = document.getElementById('currentCodeDisplay');
- if (display) {
- display.value = data.review_code || 'Не установлен';
- }
- } catch(err) {
- const display = document.getElementById('currentCodeDisplay');
- if (display) display.value = 'Ошибка загрузки';
- }
-
- try {
- await loadBackupSettings();
- await loadBackupsList();
- } catch(err) {
- console.error('Failed to load backup module:', err);
- }
-}
-
document.getElementById('settingsForm').addEventListener('submit', async e => {
e.preventDefault();
const newCode = document.getElementById('newReviewCode').value.trim();
@@ -1754,17 +1732,6 @@ function toggleCodeShow() {
}
// Backup Functions
-async function loadBackupSettings() {
- try {
- const settings = await api('/api/admin/backup/settings');
- document.getElementById('backupAutoEnabled').checked = settings.backup_auto_enabled === 'true';
- document.getElementById('backupAutoTime').value = settings.backup_auto_time || '03:00';
- document.getElementById('backupRetentionDays').value = settings.backup_retention_days || '30';
- } catch(err) {
- console.error('Failed to load backup settings:', err);
- }
-}
-
async function saveBackupSettings() {
const enabled = document.getElementById('backupAutoEnabled').checked;
const time = document.getElementById('backupAutoTime').value;
@@ -2037,62 +2004,6 @@ function renderRooms(rooms) {
let editingRoomId = null;
let currentRooms = [];
-function showRoomModal(id) {
- editingRoomId = id;
- const modal = document.getElementById('roomModal');
- const form = document.getElementById('roomForm');
- const title = document.getElementById('roomModalTitle');
-
- form.reset();
- document.getElementById('roomImagePreview').src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 120"%3E%3Crect fill="%23274151" width="200" height="120"/%3E%3Ctext fill="%2364748b" font-family="sans-serif" font-size="12" x="50%25" y="50%25" text-anchor="middle" dy=".3em"%3EВыберите фото%3C/text%3E%3C/svg%3E';
- document.getElementById('roomImagePreview').removeAttribute('data-path');
-
- document.querySelectorAll('#roomForm input[type="checkbox"]').forEach(cb => cb.checked = false);
-
- if (id) {
- title.textContent = 'Редактировать номер';
- const room = currentRooms.find(r => r.id === id);
- if (room) fillRoomForm(room);
- } else {
- title.textContent = 'Добавить номер';
- }
-
- modal.classList.add('show');
- document.body.style.overflow = 'hidden';
-}
-
-function fillRoomForm(r) {
- document.getElementById('roomName').value = r.name || '';
- document.getElementById('roomType').value = r.type || '2x-местный';
- document.getElementById('roomDescription').value = r.description || '';
- document.getElementById('roomPrice').value = r.price_per_night || 0;
- document.getElementById('roomArea').value = r.area_sqm || 20;
- document.getElementById('roomMaxGuests').value = r.max_guests || 2;
- document.getElementById('roomCount').value = r.rooms_count || 1;
- document.getElementById('roomFloors').value = (Array.isArray(r.floors) ? r.floors.join(', ') : '');
- document.getElementById('roomExtraBeds').value = r.extra_beds || 0;
- document.getElementById('roomExtraBedPrice').value = r.extra_bed_price || 0;
- document.getElementById('roomIsActive').checked = r.is_active !== 0;
-
- if (r.image_path) {
- const src = r.image_path.startsWith('uploads') ? '/' + r.image_path : r.image_path;
- document.getElementById('roomImagePreview').src = src;
- document.getElementById('roomImagePreview').dataset.path = r.image_path;
- }
-
- const furniture = Array.isArray(r.furniture) ? r.furniture : [];
- furniture.forEach(f => {
- const cb = document.querySelector('#roomForm input[name="furniture"][value="' + f + '"]');
- if (cb) cb.checked = true;
- });
-
- const amenities = Array.isArray(r.amenities) ? r.amenities : [];
- amenities.forEach(a => {
- const cb = document.querySelector('#roomForm input[name="amenities"][value="' + a + '"]');
- if (cb) cb.checked = true;
- });
-}
-
function closeRoomModal() {
document.getElementById('roomModal').classList.remove('show');
document.body.style.overflow = '';
@@ -2199,7 +2110,7 @@ async function loadRoomImages(roomId) {
const room = rooms.find(r => r.id === roomId);
if (!room || !room.images) return;
renderRoomImages(room.images);
- } catch(e) {}
+ } catch(e) { console.error('loadRoomImages failed:', e); }
}
function renderRoomImages(images) {
@@ -2300,36 +2211,43 @@ function renderCalendar(data) {
const days = data.days || {};
const month = data.month;
const [y, m] = month.split('-').map(Number);
- const firstDay = new Date(y, m-1, 1);
const lastDay = new Date(y, m, 0);
- const startDayOfWeek = firstDay.getDay() || 7;
+ const dayNames = ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'];
+
+ let html = '
';
- let html = '
';
- html += '| Месяц | ';
- const dayNames = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
for (let d = 1; d <= lastDay.getDate(); d++) {
- html += `${d} ${dayNames[(new Date(y,m-1,d).getDay()||7)-1]} | `;
- }
- html += '
';
+ const dateStr = `${month}-${String(d).padStart(2,'0')}`;
+ const dow = new Date(y, m-1, d).getDay();
+ const dayName = dayNames[dow];
+ let hasData = false;
- rooms.forEach(room => {
- html += '| ' + esc(room.type) + ' | ';
- for (let d = 1; d <= lastDay.getDate(); d++) {
- const dateStr = `${month}-${String(d).padStart(2,'0')}`;
+ let roomsHtml = '';
+ rooms.forEach(room => {
const dayData = days[dateStr] ? days[dateStr][room.type] : null;
const booked = dayData ? dayData.booked : 0;
- const total = dayData ? dayData.total : room.rooms_count;
+ const total = room.rooms_count || 1;
const ratio = total > 0 ? booked / total : 0;
let bg = '#dcfce7';
if (ratio >= 1) bg = '#fee2e2';
else if (ratio >= 0.7) bg = '#fef3c7';
+ const pct = Math.round(ratio * 100);
- const title = room.type + ': ' + booked + '/' + total;
- html += `${booked > 0 ? ''+booked+'' : ''} | `;
- }
- html += '
';
- });
- html += '
';
+ if (booked > 0) hasData = true;
+ roomsHtml += `
+ ${esc(room.type)}
+
+ ${booked}/${total}
+
`;
+ });
+
+ html += `
`;
+ }
+
+ html += '
';
grid.innerHTML = html;
}
@@ -2602,7 +2520,7 @@ async function loadSecurityKeys() {
`;
- } catch(e) {}
+ } catch(e) { console.error('loadSecurityKeys failed:', e); }
}
async function regenerateKey(type) {
@@ -2631,7 +2549,7 @@ async function loadEmailSettings() {
document.getElementById('smtpFrom').value = data.smtp_from || '';
document.getElementById('adminEmail').value = data.admin_email || '';
document.getElementById('emailEnabled').checked = data.email_notifications_enabled === 'true';
- } catch(e) {}
+ } catch(e) { console.error('loadEmailSettings failed:', e); }
}
async function saveEmailSettings() {
@@ -2672,7 +2590,7 @@ async function loadPhoneSettings() {
document.getElementById('phoneMaxActive').checked = data.maxActive === 'true';
document.getElementById('phoneInstagramId').value = data.instagramId || '';
document.getElementById('phoneInstagramActive').checked = data.instagramActive === 'true';
- } catch(e) {}
+ } catch(e) { console.error('loadPhoneSettings failed:', e); }
}
async function savePhoneSettings() {
@@ -2707,7 +2625,7 @@ async function loadTelegramSettings() {
document.getElementById('telegramEnabled').checked = data.telegram_notify_enabled === 'true';
document.getElementById('telegramApiUrl').value = data.telegram_api_url || 'https://api.telegram.org';
updateTelegramStatus(data.telegram_proxy_status || 'unknown');
- } catch(e) {}
+ } catch(e) { console.error('loadTelegramSettings failed:', e); }
}
function updateTelegramStatus(status) {
@@ -2790,7 +2708,7 @@ async function loadSettings() {
try {
const data = await api('/api/admin/settings');
document.getElementById('currentCodeDisplay').value = data.review_code || '';
- } catch(e) {}
+ } catch(e) { console.error('loadSettings review_code failed:', e); }
try {
const backupSettings = await api('/api/admin/backup/settings');
if (backupSettings.backup_auto_enabled !== undefined) {
@@ -2802,8 +2720,8 @@ async function loadSettings() {
if (backupSettings.backup_retention_days) {
document.getElementById('backupRetentionDays').value = backupSettings.backup_retention_days;
}
- loadBackups();
- } catch(e) {}
+ loadBackupsList();
+ } catch(e) { console.error('loadSettings backup failed:', e); }
loadSecurityKeys();
loadEmailSettings();
loadPhoneSettings();
@@ -3161,11 +3079,11 @@ async function loadIPs(offset) {
'';
data.entries.forEach(e => {
html += `` +
- `| ${e.ip} | ` +
- `${e.country || '-'} ${e.country_code || ''} | ` +
- `${e.accept_language || '-'} | ` +
+ `${esc(e.ip)} | ` +
+ `${esc(e.country || '-')} ${esc(e.country_code || '')} | ` +
+ `${esc(e.accept_language || '-')} | ` +
`${e.created_at ? e.created_at.substring(0, 16) : e.visit_date} | ` +
- `${e.page_path} | ` +
+ `${esc(e.page_path)} | ` +
`
`;
});
html += '';
@@ -3185,6 +3103,13 @@ async function loadIPs(offset) {
document.getElementById('visIPsTable').innerHTML = 'Ошибка загрузки
';
}
}
+document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') {
+ var modals = document.querySelectorAll('.modal-backdrop-custom.show');
+ modals.forEach(function(m) { m.classList.remove('show'); });
+ if (modals.length > 0) document.body.style.overflow = '';
+ }
+});
@@ -3317,6 +3242,17 @@ async function loadIPs(offset) {
.room-admin-actions { display: flex; gap: 8px; margin-top: 12px; border-top: 1px solid #f1f5f9; padding-top: 12px; }
.room-admin-actions .btn-primary-custom { flex: 1; }
.room-admin-actions .btn-danger-custom { padding: 8px 12px; }
+.calendar-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
+@media (min-width: 768px) { .calendar-cards { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } }
+.cal-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
+.cal-card-header { padding: 8px 12px; background: #f8fafc; font-weight: 600; font-size: 0.85rem; color: #0f172a; border-bottom: 1px solid #e2e8f0; }
+.cal-card-rooms { padding: 4px 0; }
+.cal-room-row { display: flex; align-items: center; gap: 6px; padding: 4px 10px; font-size: 0.75rem; border-bottom: 1px solid #f1f5f9; }
+.cal-room-row:last-child { border-bottom: none; }
+.cal-room-type { width: 80px; flex-shrink: 0; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.cal-room-bar { flex: 1; height: 6px; background: rgba(0,0,0,0.1); border-radius: 3px; overflow: hidden; }
+.cal-room-fill { display: block; height: 100%; border-radius: 3px; transition: width 0.3s ease; }
+.cal-room-count { width: 36px; flex-shrink: 0; text-align: right; font-weight: 600; font-size: 0.7rem; }
diff --git a/public/css/style.css b/public/css/style.css
index f4a34cc..dfce029 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -57,7 +57,6 @@ html, body {
gap: 5px;
}
.nav-link {
- padding: 10px 15px !important;
border-radius: 8px;
}
.nav-link:hover {
@@ -115,12 +114,12 @@ h1, h2, h3, h4 {
/* Navigation */
.navbar {
- transition: all 0.4s ease;
+ transition: transform 0.4s ease, box-shadow 0.4s ease, background 0.4s ease, color 0.4s ease, opacity 0.4s ease;
padding: 20px 0;
background: transparent;
}
-.navbar.scrolled {
- background: rgba(10, 77, 104, 0.95) !important;
+#mainNavbar.navbar.scrolled {
+ background: rgba(10, 77, 104, 0.95);
backdrop-filter: blur(20px);
padding: 10px 0;
box-shadow: 0 4px 30px rgba(0,0,0,0.15);
@@ -128,17 +127,17 @@ h1, h2, h3, h4 {
.navbar-brand {
font-family: 'Playfair Display', serif;
font-size: 1.8rem;
- color: #fff !important;
+ color: #fff;
font-weight: 700;
}
.navbar-brand span { color: var(--gold); }
.nav-link {
- color: rgba(255,255,255,0.9) !important;
+ color: rgba(255,255,255,0.9);
font-weight: 500;
font-size: 0.95rem;
letter-spacing: 0.5px;
- padding: 8px 16px !important;
- transition: all 0.3s ease;
+ padding: 8px 16px;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
position: relative;
}
.nav-link::after {
@@ -147,11 +146,11 @@ h1, h2, h3, h4 {
bottom: 0; left: 50%;
width: 0; height: 2px;
background: var(--gold);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
transform: translateX(-50%);
}
.nav-link:hover::after { width: 60%; }
-.nav-link:hover { color: var(--gold) !important; }
+#mainNavbar .nav-link:hover { color: var(--gold); }
/* Hero Section */
.hero {
@@ -266,7 +265,7 @@ h1, h2, h3, h4 {
font-weight: 700;
font-size: clamp(0.85rem, 1.2vw, 1rem);
letter-spacing: 0.5px;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
@@ -286,7 +285,7 @@ h1, h2, h3, h4 {
border-radius: 50px;
font-weight: 600;
font-size: clamp(0.85rem, 1.2vw, 1rem);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
@@ -298,8 +297,7 @@ h1, h2, h3, h4 {
border-color: #fff;
transform: translateY(-3px);
}
-.btn-whatsapp {
- background: #25D366;
+.btn-social {
color: #fff;
border: none;
width: 44px;
@@ -307,89 +305,27 @@ h1, h2, h3, h4 {
border-radius: 50%;
font-weight: 700;
font-size: 0.85rem;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 0;
+ margin: 0 4px;
}
-.btn-whatsapp:hover {
- background: #1ebe57;
+.btn-social:hover {
transform: translateY(-3px);
- box-shadow: 0 8px 20px rgba(37,211,102,0.4);
- color: #fff;
-}
-.btn-telegram {
- background: #0088cc;
- color: #fff;
- border: none;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- font-weight: 700;
- font-size: 0.85rem;
- transition: all 0.3s ease;
- text-decoration: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 0;
-}
-.btn-telegram:hover {
- background: #0077b5;
- transform: translateY(-3px);
- box-shadow: 0 8px 20px rgba(0,136,204,0.4);
- color: #fff;
-}
-.btn-max {
- background: #471AFF;
- color: #fff;
- border: none;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- font-weight: 700;
- font-size: 0.85rem;
- transition: all 0.3s ease;
- text-decoration: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 0;
-}
-.btn-max:hover {
- background: #3b15d9;
- transform: translateY(-3px);
- box-shadow: 0 8px 20px rgba(71,26,255,0.4);
- color: #fff;
-}
-.btn-instagram {
- background: #E4405F;
- color: #fff;
- border: none;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- font-weight: 700;
- font-size: 0.85rem;
- transition: all 0.3s ease;
- text-decoration: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 0;
-}
-.btn-instagram:hover {
- background: #d12e4e;
- transform: translateY(-3px);
- box-shadow: 0 8px 20px rgba(228,64,95,0.4);
color: #fff;
}
+.btn-social--wa { background: #25D366; }
+.btn-social--wa:hover { background: #1ebe57; box-shadow: 0 8px 20px rgba(37,211,102,0.4); }
+.btn-social--tg { background: #0088cc; }
+.btn-social--tg:hover { background: #0077b5; box-shadow: 0 8px 20px rgba(0,136,204,0.4); }
+.btn-social--max { background: #471AFF; }
+.btn-social--max:hover { background: #3b15d9; box-shadow: 0 8px 20px rgba(71,26,255,0.4); }
+.btn-social--ig { background: #E4405F; }
+.btn-social--ig:hover { background: #d12e4e; box-shadow: 0 8px 20px rgba(228,64,95,0.4); }
.max-icon-circle {
display: inline-flex;
align-items: center;
@@ -417,7 +353,7 @@ h1, h2, h3, h4 {
font-size: 0.7em;
line-height: 1;
flex-shrink: 0;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
}
.footer-social a:hover .max-icon-circle-footer {
background: #471AFF;
@@ -582,7 +518,7 @@ h1, h2, h3, h4 {
gap: clamp(10px, 2vw, 15px);
padding: clamp(10px, 2vw, 15px);
border-radius: 12px;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
}
.feature-item:hover {
background: rgba(212,168,67,0.08);
@@ -617,7 +553,7 @@ h1, h2, h3, h4 {
border-radius: 24px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
- transition: all 0.4s ease;
+ transition: transform 0.4s ease, box-shadow 0.4s ease, background 0.4s ease, color 0.4s ease, opacity 0.4s ease;
height: 100%;
display: flex;
flex-direction: column;
@@ -773,7 +709,7 @@ h1, h2, h3, h4 {
border-radius: 50px;
font-weight: 600;
font-size: clamp(0.8rem, 1.2vw, 0.9rem);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
cursor: pointer;
}
.btn-book:hover {
@@ -898,7 +834,7 @@ h1, h2, h3, h4 {
background: rgba(255,255,255,0.08);
border-radius: 50%;
cursor: pointer;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
z-index: 2;
line-height: 1;
}
@@ -939,7 +875,7 @@ h1, h2, h3, h4 {
overflow: hidden;
height: clamp(250px, 40vw, 350px);
cursor: pointer;
- transition: all 0.4s ease;
+ transition: transform 0.4s ease, box-shadow 0.4s ease, background 0.4s ease, color 0.4s ease, opacity 0.4s ease;
}
.activity-card:hover {
transform: translateY(-8px);
@@ -994,7 +930,7 @@ h1, h2, h3, h4 {
border: 1px solid rgba(255,255,255,0.1);
border-radius: 20px;
padding: clamp(20px, 4vw, 35px);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
height: 100%;
}
.review-card:hover {
@@ -1101,7 +1037,7 @@ h1, h2, h3, h4 {
font-weight: 700;
font-size: clamp(0.9rem, 1.5vw, 1.1rem);
text-decoration: none;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
}
.host-contact:hover {
background: var(--primary-light);
@@ -1214,7 +1150,7 @@ h1, h2, h3, h4 {
color: rgba(255,255,255,0.6);
text-decoration: none;
font-size: clamp(0.8rem, 1.2vw, 0.9rem);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
}
.footer-links a:hover {
color: var(--gold);
@@ -1235,7 +1171,7 @@ h1, h2, h3, h4 {
justify-content: center;
color: rgba(255,255,255,0.6);
text-decoration: none;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
}
.footer-social a:hover {
background: var(--gold);
@@ -1319,7 +1255,7 @@ h1, h2, h3, h4 {
font-weight: 700;
font-size: clamp(0.8rem, 1vw, 0.9rem);
cursor: pointer;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
white-space: nowrap;
}
.cookie-accept-btn:hover {
@@ -1335,7 +1271,7 @@ h1, h2, h3, h4 {
.animate-on-scroll {
opacity: 0;
transform: translateY(40px);
- transition: all 0.8s ease;
+ transition: opacity 0.8s ease, transform 0.8s ease;
}
.animate-on-scroll.visible {
opacity: 1;
@@ -1395,7 +1331,7 @@ h1, h2, h3, h4 {
border-radius: 50px;
font-weight: 700;
font-size: clamp(0.9rem, 1.4vw, 1rem);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
position: relative;
cursor: pointer;
text-decoration: none;
@@ -1431,7 +1367,7 @@ h1, h2, h3, h4 {
border: 2px solid #e0e0e0;
padding: 12px 18px;
font-size: clamp(0.85rem, 1.3vw, 0.95rem);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-light);
@@ -1452,7 +1388,7 @@ h1, h2, h3, h4 {
font-weight: 700;
font-size: clamp(0.9rem, 1.4vw, 1rem);
width: 100%;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
}
.btn-submit-booking:hover {
transform: translateY(-2px);
@@ -1475,7 +1411,7 @@ h1, h2, h3, h4 {
font-size: clamp(1.2rem, 3vw, 1.5rem);
z-index: 1000;
box-shadow: 0 5px 25px rgba(37,211,102,0.4);
- transition: all 0.3s ease;
+ transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease, color 0.3s ease, opacity 0.3s ease;
text-decoration: none;
animation: phonePulse 2s ease infinite;
}
@@ -1602,7 +1538,7 @@ h1, h2, h3, h4 {
.reveal-img {
opacity: 0;
transform: scale(0.95);
- transition: all 0.6s ease;
+ transition: transform 0.6s ease, box-shadow 0.6s ease, background 0.6s ease, opacity 0.6s ease;
}
.reveal-img.visible {
opacity: 1;
@@ -1688,7 +1624,7 @@ h1, h2, h3, h4 {
border: 1px solid rgba(255,255,255,0.1);
border-radius: 20px;
padding: clamp(18px, 3.5vw, 28px);
- transition: all 0.35s ease;
+ transition: transform 0.35s ease, box-shadow 0.35s ease, background 0.35s ease, color 0.35s ease, opacity 0.35s ease;
height: 100%;
}
.tips-card:hover {
@@ -1799,7 +1735,7 @@ h1, h2, h3, h4 {
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px;
padding: clamp(14px, 2.5vw, 20px);
- transition: all 0.35s ease;
+ transition: transform 0.35s ease, box-shadow 0.35s ease, background 0.35s ease, color 0.35s ease, opacity 0.35s ease;
position: relative;
overflow: hidden;
}
diff --git a/public/index.html b/public/index.html
index eeea914..77e1ef9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -20,10 +20,11 @@
-