Full Apple-style app with backend API, admin panel, scroll animations

This commit is contained in:
temp 2026-08-17 11:00:06 +00:00
parent d1848333cc
commit 9ae6383d9d
12 changed files with 2191 additions and 633 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.env
node_modules/
.vibe42-run.log
.vibe42-run.pid
*.local

View File

@ -11,7 +11,7 @@ Workspace юзера `temp`. Это **учебная среда**, где обы
1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/temp/<repo>/`, бот/приложение — запущены через `run` с живой ссылкой, 1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/temp/<repo>/`, бот/приложение — запущены через `run` с живой ссылкой,
2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты. 2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты.
Юзер не разработчик. Ему важен **работающий результат**, а не код. Юзер не разработчик. Ему важен **работающий результат**, и оптимизированный и эффективный код.
--- ---
@ -23,7 +23,7 @@ Workspace юзера `temp`. Это **учебная среда**, где обы
| Что хочет юзер | Стек (ЖЁСТКО) | Рецепт | | Что хочет юзер | Стек (ЖЁСТКО) | Рецепт |
|----------------|---------------|--------| |----------------|---------------|--------|
| Веб сайт фронтенд на javascript | **Статика:** `index.html` + `style.css` + `script.js` + еще файлы для фронтенда, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind или bootstrap — только CDN. | старт с `design.md`, публикация в `pages` | | Веб сайт фронтенд с API на javascript | **Статика:** `index.html` + `style.css` + `script.js` + еще файлы для фронтенда, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind или bootstrap — только CDN. | старт с `design.md`, публикация в `pages` |
| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже | | Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже |
| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже | | Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже |
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже | | Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже |
@ -464,7 +464,7 @@ if (req.method === "POST" && req.url === "/api/upload") {
| «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` | | «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` |
| «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp | | «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp |
| «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` | | «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` |
| «сайт-визитка / промо / портфолио» | статичный лендинг + публикация в `pages` — тут это лучший инструмент, а не компромисс | | «сайт-визитка / промо / портфолио» | динамичный лендинг с красивыми и крутыми анимациями + публикация в `pages` — тут это лучший инструмент, а не компромисс |
| «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет | | «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет |
Правила ведения: Правила ведения:
@ -766,11 +766,11 @@ async function inbox() {
5. Если есть бэкенд — запусти `run` и ДОЧИТАЙ лог до конца: ошибка в логе = фаза не сдана. Если только статика — посмотри вкладку Предпросмотр. 5. Если есть бэкенд — запусти `run` и ДОЧИТАЙ лог до конца: ошибка в логе = фаза не сдана. Если только статика — посмотри вкладку Предпросмотр.
6. Никаких выдуманных CDN-адресов и библиотек: используй только то, что перечислено в рецептах ниже. 6. Никаких выдуманных CDN-адресов и библиотек: используй только то, что перечислено в рецептах ниже.
## 📁 ТЫ УЖЕ ВНУТРИ ПАПКИ ПРОЕКТА — собирай сайт ЗДЕСЬ ## 📁 ТЫ УЖЕ ВНУТРИ ПАПКИ ПРОЕКТА — собирай проект ЗДЕСЬ
Твоя рабочая директория (cwd) — это **папка проекта юзера** (`.../users/<username>/<project>/`). Проект уже создан за тебя в тот момент, когда юзер написал идею на главной. Проверь: `pwd` — папка проекта, `ls` — там лежат AGENTS.md/design.md/README.md. Твоя рабочая директория (cwd) — это **папка проекта юзера** (`.../users/<username>/<project>/`). Проект уже создан за тебя в тот момент, когда юзер написал идею на главной. Проверь: `pwd` — папка проекта, `ls` — там лежат AGENTS.md/design.md/README.md.
**Собирай сайт ПРЯМО В ТЕКУЩЕЙ папке:** создавай `index.html`, `style.css`, `script.js` здесь же, в cwd. **Собирай сайт ПРЯМО В ТЕКУЩЕЙ папке:** создавай `index.html`, `style.css`, `script.js` и другие необходимые файлы здесь же, в cwd.
❌ **НЕ запускай `./new-project`.** ❌ **НЕ делай `cd` в другие папки / в корень воркспейса.** Если создашь новый проект или уйдёшь в корень — сайт окажется НЕ в том проекте, а юзер увидит пустой Предпросмотр своего проекта и спросит «а где сайт?». Именно так это ломается. ❌ **НЕ запускай `./new-project`.** ❌ **НЕ делай `cd` в другие папки / в корень воркспейса.** Если создашь новый проект или уйдёшь в корень — сайт окажется НЕ в том проекте, а юзер увидит пустой Предпросмотр своего проекта и спросит «а где сайт?». Именно так это ломается.
@ -808,8 +808,8 @@ git push origin HEAD:pages
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f` - **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
### ⚠️ Проекты с бэкендом (server.js / run) и статика — НЕ путай юзера ### ⚠️ Проекты с бэкендом (server.js / run) — НЕ путай юзера
Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО статические файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому: Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому:
- Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия. - Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия.
- Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть. - Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть.
- run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`. - run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`.

318
API.md Normal file
View File

@ -0,0 +1,318 @@
# API Документация — Apple Style App
## 📚 Обзор
Бэкенд построен на **Node.js http** (без зависимостей). Данные хранятся в `data.json`.
---
## 🔌 PUBLIC API (без авторизации)
### Получить все продукты
```
GET /api/products
```
**Ответ:**
```json
{
"ok": true,
"data": [
{
"id": 1,
"name": "Vision Pro",
"tagline": "Пространственный компьютер здесь",
"description": "...",
"price": 3499,
"image": "https://...",
"features": ["M2", "R1"],
"category": "premium",
"order": 1
}
]
}
```
---
### Получить продукт по ID
```
GET /api/products/:id
```
**Ответ:**
```json
{
"ok": true,
"data": { /* объект продукта */ }
}
```
---
### Получить активные объявления
```
GET /api/announcements
```
**Ответ:**
```json
{
"ok": true,
"data": [
{
"id": 1,
"title": "Apple Event 2026",
"date": "2026-09-15",
"content": "Смотрите презентацию...",
"active": true
}
]
}
```
---
### Получить настройки сайта
```
GET /api/settings
```
**Ответ:**
```json
{
"ok": true,
"data": {
"heroTitle": "Добро пожаловать в будущее",
"heroSubtitle": "Инновации, которые меняют мир",
"theme": "dark"
}
}
```
---
## 🔐 ADMIN API (для редактирования)
### Добавить продукт
```
POST /api/admin/products
Content-Type: application/json
{
"name": "iPhone 18 Pro",
"tagline": "Ещё мощнее",
"description": "Описание...",
"price": 999,
"category": "phones",
"image": "https://...",
"features": ["A18 чип", "Face ID", "5G"]
}
```
**Ответ:**
```json
{
"ok": true,
"data": { /* созданный продукт с id и order */ }
}
```
---
### Обновить продукт
```
PUT /api/admin/products/:id
Content-Type: application/json
{
"name": "Новое название",
"price": 1099
}
```
---
### Удалить продукт
```
DELETE /api/admin/products/:id
```
**Ответ:**
```json
{
"ok": true,
"deleted": 123
}
```
---
### Добавить объявление
```
POST /api/admin/announcements
Content-Type: application/json
{
"title": "Специальное предложение",
"date": "2026-12-01",
"content": "Скидки до 30%",
"active": true
}
```
---
### Обновить объявление
```
PUT /api/admin/announcements/:id
Content-Type: application/json
{
"title": "Обновлённый заголовок",
"active": false
}
```
---
### Удалить объявление
```
DELETE /api/admin/announcements/:id
```
---
### Обновить настройки сайта
```
PUT /api/admin/settings
Content-Type: application/json
{
"heroTitle": "Новый заголовок",
"heroSubtitle": "Новый подзаголовок",
"theme": "light"
}
```
---
## 🗂 Структура данных (data.json)
```json
{
"products": [
{
"id": number,
"name": string,
"tagline": string,
"description": string,
"price": number,
"image": string (URL),
"features": string[],
"category": string,
"order": number
}
],
"announcements": [
{
"id": number,
"title": string,
"date": string (ISO date),
"content": string,
"active": boolean
}
],
"siteSettings": {
"heroTitle": string,
"heroSubtitle": string,
"theme": string ("dark" | "light")
}
}
```
---
## 🚀 Архитектура сервера
```
server.js
├── Routes (API)
│ ├── GET /api/products → список продуктов
│ ├── GET /api/products/:id → продукт по ID
│ ├── GET /api/announcements → активные объявления
│ ├── GET /api/settings → настройки сайта
│ │
│ └── ADMIN (CRUD)
│ ├── POST /api/admin/products
│ ├── PUT /api/admin/products/:id
│ ├── DELETE /api/admin/products/:id
│ ├── POST /api/admin/announcements
│ ├── PUT /api/admin/announcements/:id
│ ├── DELETE /api/admin/announcements/:id
│ └── PUT /api/admin/settings
│
└── Static Files
├── / → index.html
├── /admin.html
├── /style.css
├── /app.js
└── /admin.js
```
---
## 📝 Примеры использования (Frontend)
### Загрузка продуктов
```javascript
async function loadProducts() {
const res = await fetch('/api/products');
const json = await res.json();
if (json.ok) {
console.log(json.data); // массив продуктов
}
}
```
### Добавление продукта (из админки)
```javascript
async function addProduct(data) {
const res = await fetch('/api/admin/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const json = await res.json();
return json.ok;
}
```
---
## 🔒 Безопасность (TODO для продакшена)
Сейчас API открыт. Для продакшена добавить:
- JWT или session аутентификацию для `/api/admin/*`
- Валидацию входных данных
- Rate limiting
- CORS только для доверенных доменов
---
## 🧪 Тестирование
```bash
# Получить все продукты
curl http://localhost:3000/api/products
# Добавить продукт
curl -X POST http://localhost:3000/api/admin/products \
-H "Content-Type: application/json" \
-d '{"name":"Test","tagline":"Test","price":99,"category":"test","features":[]}'
# Обновить настройки
curl -X PUT http://localhost:3000/api/admin/settings \
-H "Content-Type: application/json" \
-d '{"heroTitle":"Hello"}'
```

243
admin.css Normal file
View File

@ -0,0 +1,243 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
background: #0a0a0a;
color: #f5f5f7;
line-height: 1.5;
}
.admin-nav {
position: sticky;
top: 0;
background: rgba(29, 29, 31, 0.95);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255,255,255,0.1);
z-index: 100;
}
.admin-nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-logo {
font-size: 20px;
font-weight: 600;
color: #f5f5f7;
text-decoration: none;
}
.btn-back {
color: #2997ff;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: opacity 0.3s;
}
.btn-back:hover {
opacity: 0.8;
}
.admin-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 24px;
}
.admin-section {
margin-bottom: 60px;
}
.admin-section h2 {
font-size: 28px;
font-weight: 600;
margin-bottom: 24px;
letter-spacing: -0.01em;
}
.admin-card {
background: #1d1d1f;
border-radius: 16px;
padding: 32px;
margin-bottom: 24px;
border: 1px solid rgba(255,255,255,0.08);
}
.admin-card h3 {
font-size: 20px;
margin-bottom: 24px;
font-weight: 600;
}
.admin-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.list-item {
background: #2d2d30;
border-radius: 12px;
padding: 24px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
flex-wrap: wrap;
}
.list-item-info {
flex: 1;
min-width: 200px;
}
.list-item-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 6px;
}
.list-item-meta {
font-size: 14px;
color: #86868b;
}
.list-item-actions {
display: flex;
gap: 12px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #86868b;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px 16px;
background: #0a0a0a;
border: 1px solid rgba(255,255,255,0.15);
border-radius: 10px;
color: #f5f5f7;
font-size: 16px;
font-family: inherit;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #2997ff;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
.btn {
padding: 12px 24px;
border-radius: 980px;
font-size: 15px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #2997ff;
color: white;
}
.btn-primary:hover {
background: #147ce5;
}
.btn-secondary {
background: transparent;
color: #2997ff;
border: 2px solid #2997ff;
}
.btn-secondary:hover {
background: #2997ff;
color: white;
}
.btn-danger {
background: #ff3b30;
color: white;
}
.btn-danger:hover {
background: #d63027;
}
.btn-small {
padding: 8px 16px;
font-size: 13px;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 980px;
font-size: 12px;
font-weight: 500;
background: rgba(41, 151, 255, 0.15);
color: #2997ff;
}
.status-badge.inactive {
background: rgba(134, 134, 139, 0.15);
color: #86868b;
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.list-item {
flex-direction: column;
align-items: flex-start;
}
.list-item-actions {
width: 100%;
justify-content: flex-end;
}
}

138
admin.html Normal file
View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель — Apple Style</title>
<link rel="stylesheet" href="admin.css">
</head>
<body>
<nav class="admin-nav">
<div class="admin-nav-container">
<a href="index.html" class="admin-logo"> Админка</a>
<a href="index.html" class="btn-back">← На сайт</a>
</div>
</nav>
<div class="admin-container">
<!-- Settings Section -->
<section class="admin-section">
<h2>🏠 Настройки сайта</h2>
<div class="admin-card">
<form id="settingsForm">
<div class="form-group">
<label>Заголовок Hero</label>
<input type="text" id="heroTitle" name="heroTitle" placeholder="Добро пожаловать в будущее">
</div>
<div class="form-group">
<label>Подзаголовок Hero</label>
<input type="text" id="heroSubtitle" name="heroSubtitle" placeholder="Инновации, которые меняют мир">
</div>
<div class="form-group">
<label>Тема</label>
<select id="theme" name="theme">
<option value="dark">Тёмная</option>
<option value="light">Светлая</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
</form>
</div>
</section>
<!-- Products Section -->
<section class="admin-section">
<h2>📱 Продукты</h2>
<div class="admin-card">
<button class="btn btn-secondary" onclick="showAddProductForm()">+ Добавить продукт</button>
</div>
<div id="addProductForm" class="admin-card" style="display:none">
<h3>Новый продукт</h3>
<form id="productForm">
<div class="form-group">
<label>Название</label>
<input type="text" id="productName" required placeholder="iPhone 18 Pro">
</div>
<div class="form-group">
<label>Слоган</label>
<input type="text" id="productTagline" required placeholder="Ещё мощнее. Ещё лучше.">
</div>
<div class="form-group">
<label>Описание</label>
<textarea id="productDescription" rows="3" placeholder="Подробное описание..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Цена ($)</label>
<input type="number" id="productPrice" required placeholder="999">
</div>
<div class="form-group">
<label>Категория</label>
<select id="productCategory">
<option value="premium">Premium</option>
<option value="audio">Audio</option>
<option value="wearables">Wearables</option>
<option value="computers">Computers</option>
<option value="phones">Phones</option>
</select>
</div>
</div>
<div class="form-group">
<label>URL изображения</label>
<input type="text" id="productImage" placeholder="https://picsum.photos/800/600?random=1">
</div>
<div class="form-group">
<label>Особенности (через запятую)</label>
<input type="text" id="productFeatures" placeholder="M3 чип, 20ч работы, Face ID">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Добавить</button>
<button type="button" class="btn btn-secondary" onclick="hideAddProductForm()">Отмена</button>
</div>
</form>
</div>
<div id="productsList" class="admin-list">
<!-- Products will be loaded here -->
</div>
</section>
<!-- Announcements Section -->
<section class="admin-section">
<h2>📢 Объявления</h2>
<div class="admin-card">
<button class="btn btn-secondary" onclick="showAddAnnouncementForm()">+ Добавить объявление</button>
</div>
<div id="addAnnouncementForm" class="admin-card" style="display:none">
<h3>Новое объявление</h3>
<form id="announcementForm">
<div class="form-group">
<label>Заголовок</label>
<input type="text" id="announcementTitle" required placeholder="Apple Event 2026">
</div>
<div class="form-group">
<label>Дата</label>
<input type="date" id="announcementDate" required>
</div>
<div class="form-group">
<label>Содержание</label>
<textarea id="announcementContent" rows="3" required placeholder="Смотрите презентацию..."></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Добавить</button>
<button type="button" class="btn btn-secondary" onclick="hideAddAnnouncementForm()">Отмена</button>
</div>
</form>
</div>
<div id="announcementsList" class="admin-list">
<!-- Announcements will be loaded here -->
</div>
</section>
</div>
<script src="admin.js"></script>
</body>
</html>

267
admin.js Normal file
View File

@ -0,0 +1,267 @@
// ===================================
// ADMIN PANEL - JAVASCRIPT
// ===================================
const API = {
products: '/api/products',
adminProducts: '/api/admin/products',
announcements: '/api/announcements',
adminAnnouncements: '/api/admin/announcements',
settings: '/api/settings',
adminSettings: '/api/admin/settings'
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
loadProducts();
loadAnnouncements();
setupForms();
});
// Setup forms
function setupForms() {
document.getElementById('settingsForm').addEventListener('submit', saveSettings);
document.getElementById('productForm').addEventListener('submit', addProduct);
document.getElementById('announcementForm').addEventListener('submit', addAnnouncement);
}
// ============ SETTINGS ============
async function loadSettings() {
try {
const res = await fetch(API.settings);
const json = await res.json();
if (json.ok && json.data) {
document.getElementById('heroTitle').value = json.data.heroTitle || '';
document.getElementById('heroSubtitle').value = json.data.heroSubtitle || '';
document.getElementById('theme').value = json.data.theme || 'dark';
}
} catch (e) {
console.error('Failed to load settings:', e);
}
}
async function saveSettings(e) {
e.preventDefault();
const data = {
heroTitle: document.getElementById('heroTitle').value,
heroSubtitle: document.getElementById('heroSubtitle').value,
theme: document.getElementById('theme').value
};
try {
const res = await fetch(API.adminSettings, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const json = await res.json();
if (json.ok) {
alert('Настройки сохранены! ✅');
} else {
alert('Ошибка: ' + json.error);
}
} catch (e) {
alert('Ошибка сохранения: ' + e.message);
}
}
// ============ PRODUCTS ============
async function loadProducts() {
try {
const res = await fetch(API.products);
const json = await res.json();
const list = document.getElementById('productsList');
if (!json.ok || !json.data || json.data.length === 0) {
list.innerHTML = '<p style="color:#86868b;text-align:center;padding:40px">Нет продуктов</p>';
return;
}
list.innerHTML = json.data.map(p => `
<div class="list-item">
<div class="list-item-info">
<div class="list-item-title">${p.name}</div>
<div class="list-item-meta">
${p.tagline} • $${p.price} •
<span class="status-badge">${p.category}</span>
</div>
</div>
<div class="list-item-actions">
<button class="btn btn-secondary btn-small" onclick="editProduct(${p.id})">Изменить</button>
<button class="btn btn-danger btn-small" onclick="deleteProduct(${p.id})">Удалить</button>
</div>
</div>
`).join('');
} catch (e) {
console.error('Failed to load products:', e);
}
}
function showAddProductForm() {
document.getElementById('addProductForm').style.display = 'block';
document.getElementById('productForm').reset();
}
function hideAddProductForm() {
document.getElementById('addProductForm').style.display = 'none';
}
async function addProduct(e) {
e.preventDefault();
const features = document.getElementById('productFeatures').value
.split(',')
.map(f => f.trim())
.filter(f => f);
const data = {
name: document.getElementById('productName').value,
tagline: document.getElementById('productTagline').value,
description: document.getElementById('productDescription').value,
price: parseFloat(document.getElementById('productPrice').value),
category: document.getElementById('productCategory').value,
image: document.getElementById('productImage').value || 'https://picsum.photos/800/600?random=' + Date.now(),
features: features
};
try {
const res = await fetch(API.adminProducts, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const json = await res.json();
if (json.ok) {
alert('Продукт добавлен! ✅');
hideAddProductForm();
loadProducts();
} else {
alert('Ошибка: ' + json.error);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
async function editProduct(id) {
const res = await fetch(`${API.products}/${id}`);
const json = await res.json();
if (!json.ok) return alert('Продукт не найден');
const p = json.data;
if (confirm(`Редактировать "${p.name}"?\n\n(В полной версии откроется форма редактирования)`)) {
// В полной версии здесь открывается форма с заполненными данными
window.location.href = `edit-product.html?id=${id}`;
}
}
async function deleteProduct(id) {
if (!confirm('Вы уверены? Это действие нельзя отменить.')) return;
try {
const res = await fetch(`${API.adminProducts}/${id}`, { method: 'DELETE' });
const json = await res.json();
if (json.ok) {
alert('Продукт удалён');
loadProducts();
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
// ============ ANNOUNCEMENTS ============
async function loadAnnouncements() {
try {
const res = await fetch(API.announcements);
const json = await res.json();
const list = document.getElementById('announcementsList');
if (!json.ok || !json.data || json.data.length === 0) {
list.innerHTML = '<p style="color:#86868b;text-align:center;padding:40px">Нет объявлений</p>';
return;
}
list.innerHTML = json.data.map(a => `
<div class="list-item">
<div class="list-item-info">
<div class="list-item-title">${a.title}</div>
<div class="list-item-meta">
${a.content} • ${a.date} •
<span class="status-badge">${a.active ? 'Активно' : 'Скрыто'}</span>
</div>
</div>
<div class="list-item-actions">
<button class="btn btn-secondary btn-small" onclick="editAnnouncement(${a.id})">Изменить</button>
<button class="btn btn-danger btn-small" onclick="deleteAnnouncement(${a.id})">Удалить</button>
</div>
</div>
`).join('');
} catch (e) {
console.error('Failed to load announcements:', e);
}
}
function showAddAnnouncementForm() {
document.getElementById('addAnnouncementForm').style.display = 'block';
document.getElementById('announcementForm').reset();
}
function hideAddAnnouncementForm() {
document.getElementById('addAnnouncementForm').style.display = 'none';
}
async function addAnnouncement(e) {
e.preventDefault();
const data = {
title: document.getElementById('announcementTitle').value,
date: document.getElementById('announcementDate').value,
content: document.getElementById('announcementContent').value
};
try {
const res = await fetch(API.adminAnnouncements, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const json = await res.json();
if (json.ok) {
alert('Объявление добавлено! ✅');
hideAddAnnouncementForm();
loadAnnouncements();
} else {
alert('Ошибка: ' + json.error);
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}
async function editAnnouncement(id) {
if (confirm(`Редактировать объявление #${id}?\n\n(В полной версии откроется форма)`)) {
window.location.href = `edit-announcement.html?id=${id}`;
}
}
async function deleteAnnouncement(id) {
if (!confirm('Удалить это объявление?')) return;
try {
const res = await fetch(`${API.adminAnnouncements}/${id}`, { method: 'DELETE' });
const json = await res.json();
if (json.ok) {
alert('Объявление удалено');
loadAnnouncements();
}
} catch (e) {
alert('Ошибка: ' + e.message);
}
}

166
app.js Normal file
View File

@ -0,0 +1,166 @@
// ===================================
// APPLE-STYLE APP - FRONTEND
// ===================================
const API = {
products: '/api/products',
announcements: '/api/announcements',
settings: '/api/settings'
};
// Load settings and content
async function init() {
await loadSettings();
await loadAnnouncements();
await loadProducts();
setupScrollEffects();
setupRevealAnimations();
}
// Load site settings
async function loadSettings() {
try {
const res = await fetch(API.settings);
const json = await res.json();
if (json.ok && json.data) {
document.getElementById('heroTitle').textContent = json.data.heroTitle || 'Добро пожаловать';
document.getElementById('heroSubtitle').textContent = json.data.heroSubtitle || 'Инновации здесь';
}
} catch (e) {
console.error('Failed to load settings:', e);
}
}
// Load announcements
async function loadAnnouncements() {
try {
const res = await fetch(API.announcements);
const json = await res.json();
const bar = document.getElementById('announcementBar');
if (json.ok && json.data.length > 0) {
const ann = json.data[0];
bar.innerHTML = `${ann.title} — ${ann.content} <a href="#">Подробнее →</a>`;
} else {
bar.classList.add('hidden');
}
} catch (e) {
console.error('Failed to load announcements:', e);
document.getElementById('announcementBar').classList.add('hidden');
}
}
// Load products
async function loadProducts() {
try {
const res = await fetch(API.products);
const json = await res.json();
const container = document.getElementById('productsContainer');
if (!json.ok || !json.data) {
container.innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Нет продуктов</p>';
return;
}
container.innerHTML = json.data.map((p, i) => `
<div class="product-card" style="transition-delay: ${i * 100}ms">
<img src="${p.image}" alt="${p.name}" class="product-image">
<div class="product-content">
<h3 class="product-name">${p.name}</h3>
<p class="product-tagline">${p.tagline}</p>
<p class="product-price">от ${p.price} $</p>
<div class="product-features">
${p.features.map(f => `<span class="feature-tag">${f}</span>`).join('')}
</div>
</div>
</div>
`).join('');
// Trigger visibility check after load
setTimeout(checkProductVisibility, 100);
} catch (e) {
console.error('Failed to load products:', e);
document.getElementById('productsContainer').innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Ошибка загрузки</p>';
}
}
// Scroll effects for navigation
function setupScrollEffects() {
const nav = document.getElementById('nav');
const heroBg = document.getElementById('heroBg');
const featuresBg = document.getElementById('featuresBg');
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
// Nav background
if (scrolled > 100) {
nav.classList.add('scrolled');
} else {
nav.classList.remove('scrolled');
}
// Hero parallax
if (scrolled < window.innerHeight) {
heroBg.style.transform = `translateY(${scrolled * 0.5}px) scale(${1 + scrolled * 0.0008})`;
}
// Features parallax
const featuresSection = document.getElementById('features');
if (featuresSection) {
const rect = featuresSection.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
const offset = (window.innerHeight - rect.top) * 0.2;
featuresBg.style.transform = `translateY(${offset}px)`;
}
}
});
}
// Reveal animations on scroll
function setupRevealAnimations() {
const reveals = document.querySelectorAll('.reveal');
const checkReveal = () => {
reveals.forEach(el => {
const top = el.getBoundingClientRect().top;
const trigger = window.innerHeight * 0.85;
if (top < trigger) {
el.classList.add('active');
}
});
};
window.addEventListener('scroll', checkReveal);
checkReveal();
}
// Check product cards visibility
function checkProductVisibility() {
const cards = document.querySelectorAll('.product-card');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1 });
cards.forEach(card => observer.observe(card));
}
// Smooth scroll for anchor links
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
const href = this.getAttribute('href');
if (href !== '#') {
e.preventDefault();
const target = document.querySelector(href);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
});
init();
});

62
data.json Normal file
View File

@ -0,0 +1,62 @@
{
"products": [
{
"id": 1,
"name": "Vision Pro",
"tagline": "Пространственный компьютер здесь",
"description": "Революционный интерфейс между человеком и машиной. Погрузитесь в мир цифровых возможностей.",
"price": 3499,
"image": "https://picsum.photos/800/600?random=1",
"features": ["M2 чип", "R1 сопроцессор", "23 млн пикселей", "visionOS"],
"category": "premium",
"order": 1
},
{
"id": 2,
"name": "AirPods Max",
"tagline": "Звук в новом измерении",
"description": "Высокая точность звука. Активное шумоподавление. Пространственное аудио.",
"price": 549,
"image": "https://picsum.photos/800/600?random=2",
"features": ["ANC", "40ч работы", "Пространственное аудио", "H1 чип"],
"category": "audio",
"order": 2
},
{
"id": 3,
"name": "Apple Watch Ultra",
"tagline": "Приключения ждут",
"description": "Самые прочные и функциональные часы для экстремальных условий.",
"price": 799,
"image": "https://picsum.photos/800/600?random=3",
"features": ["49мм титан", "36ч работы", "GPS", "100м водолазные"],
"category": "wearables",
"order": 3
},
{
"id": 4,
"name": "MacBook Pro 16",
"tagline": "Мощность профессионала",
"description": "Чип M3 Max. Невероятная производительность для самых требовательных задач.",
"price": 2499,
"image": "https://picsum.photos/800/600?random=4",
"features": ["M3 Max", "128GB RAM", "8TB SSD", "22ч работы"],
"category": "computers",
"order": 4
}
],
"announcements": [
{
"id": 1,
"title": "Apple Event 2026",
"date": "2026-09-15",
"content": "Смотрите презентацию новых продуктов в прямом эфире",
"active": true
}
],
"siteSettings": {
"heroTitle": "Добро пожаловать в будущее",
"heroSubtitle": "Инновации, которые меняют мир",
"theme": "dark"
}
}

View File

@ -3,668 +3,171 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Исландия — Земля льда и пламени</title> <title>Apple Style — Будущее здесь</title>
<style> <link rel="stylesheet" href="style.css">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #0B0F19;
--text: #FFFFFF;
--text-muted: #94A3B8;
--accent: #00D4FF;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
background: var(--bg);
color: var(--text);
overflow-x: hidden;
line-height: 1.6;
}
/* Навигация */
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(11, 15, 25, 0.9);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
padding: 16px 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 32px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: 700;
letter-spacing: -1px;
background: linear-gradient(135deg, #00D4FF, #7C3AED);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-links {
display: flex;
gap: 40px;
}
.nav-links a {
color: var(--text-muted);
text-decoration: none;
font-size: 15px;
font-weight: 500;
transition: color 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
}
.nav-links a:hover {
color: var(--accent);
}
/* Hero секция */
.hero {
height: 100vh;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.parallax-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 120%;
background-size: cover;
background-position: center;
will-change: transform;
}
.hero-bg1 {
background-image: url('https://picsum.photos/1920/1200?random=10');
z-index: 1;
}
.hero-bg2 {
background-image: url('https://picsum.photos/1920/1200?random=11');
z-index: 2;
opacity: 0.3;
}
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(11,15,25,0.3) 0%, var(--bg) 100%);
z-index: 3;
}
.hero-content {
position: relative;
z-index: 4;
text-align: center;
max-width: 900px;
padding: 0 24px;
}
.hero h1 {
font-size: clamp(56px, 12vw, 128px);
font-weight: 800;
letter-spacing: -4px;
line-height: 1;
margin-bottom: 24px;
text-shadow: 0 20px 60px rgba(0,0,0,0.5);
opacity: 0;
transform: translateY(40px);
animation: fadeUp 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
animation-delay: 0.3s;
}
.hero p {
font-size: clamp(20px, 3vw, 28px);
color: var(--text-muted);
margin-bottom: 40px;
font-weight: 300;
opacity: 0;
transform: translateY(40px);
animation: fadeUp 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
animation-delay: 0.6s;
}
.btn {
display: inline-block;
padding: 18px 42px;
border-radius: 980px;
font-size: 17px;
font-weight: 600;
text-decoration: none;
transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1);
cursor: pointer;
border: none;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), #7C3AED);
color: white;
box-shadow: 0 10px 40px rgba(0, 212, 255, 0.3);
}
.btn-primary:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 20px 60px rgba(0, 212, 255, 0.5);
}
/* Destinations */
.destinations {
padding: 140px 24px;
max-width: 1400px;
margin: 0 auto;
}
.section-header {
text-align: center;
margin-bottom: 80px;
}
.section-label {
color: var(--accent);
font-size: 14px;
text-transform: uppercase;
letter-spacing: 3px;
margin-bottom: 16px;
}
.section-title {
font-size: clamp(40px, 6vw, 72px);
font-weight: 700;
letter-spacing: -2px;
}
.destinations-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 32px;
}
.destination-card {
position: relative;
height: 500px;
border-radius: 24px;
overflow: hidden;
cursor: pointer;
transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
}
.destination-card:hover {
transform: translateY(-12px);
}
.destination-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
}
.destination-card:hover .destination-img {
transform: scale(1.1);
}
.destination-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 40px;
background: linear-gradient(0deg, rgba(0,0,0,0.9) 0%, transparent 100%);
}
.destination-card h3 {
font-size: 28px;
margin-bottom: 8px;
letter-spacing: -1px;
}
.destination-card p {
color: var(--text-muted);
font-size: 16px;
}
/* Parallax Section */
.parallax-section {
position: relative;
height: 80vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin: 80px 0;
}
.parallax-bg {
position: absolute;
top: -20%;
left: 0;
width: 100%;
height: 140%;
background-image: url('https://picsum.photos/1920/1200?random=12');
background-size: cover;
background-position: center;
z-index: 1;
will-change: transform;
}
.parallax-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(11, 15, 25, 0.7);
z-index: 2;
}
.parallax-content {
position: relative;
z-index: 3;
text-align: center;
max-width: 800px;
padding: 0 24px;
}
.parallax-content h2 {
font-size: clamp(40px, 7vw, 80px);
font-weight: 700;
letter-spacing: -2px;
margin-bottom: 24px;
}
.parallax-content p {
font-size: 20px;
color: var(--text-muted);
margin-bottom: 32px;
}
/* Stats */
.stats {
padding: 120px 24px;
background: #080B14;
}
.stats-container {
max-width: 1200px;
margin: 0 auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 60px;
text-align: center;
}
.stat-item h3 {
font-size: clamp(48px, 6vw, 80px);
font-weight: 800;
background: linear-gradient(135deg, var(--accent), #7C3AED);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 12px;
}
.stat-item p {
color: var(--text-muted);
font-size: 16px;
text-transform: uppercase;
letter-spacing: 2px;
}
/* Gallery */
.gallery {
padding: 140px 24px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 300px);
gap: 16px;
max-width: 1400px;
margin: 0 auto;
}
.gallery-item {
border-radius: 16px;
overflow: hidden;
position: relative;
}
.gallery-item:first-child {
grid-column: 1 / 3;
grid-row: 1 / 3;
}
.gallery-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
}
.gallery-item:hover .gallery-img {
transform: scale(1.1);
}
/* CTA */
.cta {
padding: 140px 24px;
text-align: center;
background: linear-gradient(180deg, var(--bg) 0%, #1a1f2e 100%);
}
.cta h2 {
font-size: clamp(40px, 6vw, 72px);
font-weight: 700;
letter-spacing: -2px;
margin-bottom: 24px;
}
.cta p {
font-size: 20px;
color: var(--text-muted);
max-width: 600px;
margin: 0 auto 48px;
}
/* Footer */
footer {
padding: 60px 24px;
border-top: 1px solid rgba(255,255,255,0.1);
text-align: center;
color: var(--text-muted);
}
.footer-links {
display: flex;
justify-content: center;
gap: 32px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.footer-links a {
color: var(--text-muted);
text-decoration: none;
transition: color 0.3s;
}
.footer-links a:hover {
color: var(--text);
}
/* Animations */
@keyframes fadeUp {
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal {
opacity: 0;
transform: translateY(50px);
transition: all 1s cubic-bezier(0.25, 1, 0.5, 1);
}
.reveal.active {
opacity: 1;
transform: translateY(0);
}
/* Mobile */
@media (max-width: 768px) {
.nav-links {
display: none;
}
.gallery-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.gallery-item:first-child {
grid-column: 1;
grid-row: 1;
height: 300px;
}
.destinations-grid {
grid-template-columns: 1fr;
}
.stats-grid {
gap: 40px;
}
}
</style>
</head> </head>
<body> <body>
<!-- Навигация --> <!-- Announcement Bar -->
<nav> <div id="announcementBar" class="announcement-bar"></div>
<!-- Navigation -->
<nav class="nav" id="nav">
<div class="nav-container"> <div class="nav-container">
<div class="logo">ICELAND</div> <a href="/" class="logo"></a>
<div class="nav-links"> <div class="nav-links">
<a href="#destinations">Места</a> <a href="#products">Продукты</a>
<a href="#about">О стране</a> <a href="#features">Возможности</a>
<a href="#gallery">Галерея</a> <a href="#vision">Vision</a>
<a href="#book">Забронировать</a> <a href="admin.html" class="admin-link">Админка</a>
</div> </div>
</div> </div>
</nav> </nav>
<!-- Hero --> <!-- Hero Section -->
<section class="hero"> <section class="hero" id="hero">
<div class="parallax-layer hero-bg1" id="heroBg1"></div> <div class="hero-bg" id="heroBg"></div>
<div class="parallax-layer hero-bg2" id="heroBg2"></div>
<div class="hero-overlay"></div> <div class="hero-overlay"></div>
<div class="hero-content"> <div class="hero-content">
<h1>Земля льда и пламени</h1> <h1 id="heroTitle" class="hero-title"></h1>
<p>Открой для себя самую загадочную страну мира</p> <p id="heroSubtitle" class="hero-subtitle"></p>
<a href="#book" class="btn btn-primary">Начать путешествие</a> <div class="hero-cta">
<a href="#products" class="btn btn-primary">Смотреть продукты</a>
<a href="#vision" class="btn btn-secondary">Узнать больше</a>
</div>
</div>
<div class="scroll-indicator">
<div class="mouse">
<div class="wheel"></div>
</div>
</div> </div>
</section> </section>
<!-- Destinations --> <!-- Product Showcase (Apple-style scroll) -->
<section class="destinations" id="destinations"> <section class="product-showcase" id="products">
<div class="section-header reveal"> <div class="showcase-intro">
<p class="section-label">Направления</p> <h2 class="showcase-title">Выберите свой путь</h2>
<h2 class="section-title">Места, которые захватывают дух</h2> <p class="showcase-subtitle">Инновации в каждой детали</p>
</div> </div>
<div class="destinations-grid"> <div class="products-container" id="productsContainer">
<div class="destination-card reveal"> <!-- Products will be loaded here -->
<img src="https://picsum.photos/600/800?random=20" alt="Водопад" class="destination-img">
<div class="destination-content">
<h3>Водопад Гюдльфосс</h3>
<p>Один из самых красивых водопадов Исландии</p>
</div> </div>
</section>
<!-- Features Section with Parallax -->
<section class="features" id="features">
<div class="features-bg" id="featuresBg"></div>
<div class="features-overlay"></div>
<div class="features-content">
<h2 class="section-title reveal">Технологии будущего</h2>
<div class="features-grid">
<div class="feature-card reveal">
<div class="feature-icon">🚀</div>
<h3>Производительность</h3>
<p>Чипы M-серии меняют правила игры</p>
</div> </div>
<div class="destination-card reveal"> <div class="feature-card reveal">
<img src="https://picsum.photos/600/800?random=21" alt="Гейзер" class="destination-img"> <div class="feature-icon">🔒</div>
<div class="destination-content"> <h3>Безопасность</h3>
<h3>Долина гейзеров</h3> <p>Защита данных на аппаратном уровне</p>
<p>Гейзер Строккур извергается каждые 5-10 минут</p>
</div> </div>
<div class="feature-card reveal">
<div class="feature-icon">🌱</div>
<h3>Экология</h3>
<p>Углеродная нейтральность к 2030</p>
</div> </div>
<div class="destination-card reveal"> <div class="feature-card reveal">
<img src="https://picsum.photos/600/800?random=22" alt="Лагуна" class="destination-img"> <div class="feature-icon">🤖</div>
<div class="destination-content"> <h3>ИИ</h3>
<h3>Голубая лагуна</h3> <p>Apple Intelligence в каждом устройстве</p>
<p>Геотермальный спа-курорт мирового класса</p>
</div>
</div>
<div class="destination-card reveal">
<img src="https://picsum.photos/600/800?random=23" alt="Северное сияние" class="destination-img">
<div class="destination-content">
<h3>Северное сияние</h3>
<p>Природное явление, которое нужно увидеть хотя бы раз</p>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Parallax Section --> <!-- Vision Section -->
<section class="parallax-section" id="about"> <section class="vision" id="vision">
<div class="parallax-bg" id="parallaxBg"></div> <div class="vision-content">
<div class="parallax-overlay"></div> <h2 class="vision-title reveal">Vision Pro</h2>
<div class="parallax-content reveal"> <p class="vision-subtitle reveal">Эра пространственных вычислений наступила</p>
<h2>Приключение всей жизни</h2> <div class="vision-features reveal">
<p>Вулканы, ледники, водопады, гейзеры и северное сияние — Исландия собрала все чудеса природы в одном месте</p> <div class="vision-feature">
<a href="#book" class="btn btn-primary">Выбрать тур</a> <span class="vision-feature-icon">👁️</span>
<div>
<h4>eyeSight</h4>
<p>Оставайтесь на связи с окружающими</p>
</div>
</div>
<div class="vision-feature">
<span class="vision-feature-icon">🎭</span>
<div>
<h4>visionOS</h4>
<p>Интуитивный пространственный интерфейс</p>
</div>
</div>
<div class="vision-feature">
<span class="vision-feature-icon">🎬</span>
<div>
<h4>Immersion</h4>
<p>Погружение как в кинотеатре</p>
</div>
</div>
</div>
</div>
<div class="vision-image reveal">
<img src="https://picsum.photos/1200/800?random=50" alt="Vision Pro">
</div> </div>
</section> </section>
<!-- Stats --> <!-- Big Image Scroll Section -->
<section class="stats"> <section class="scroll-gallery" id="gallery">
<div class="stats-container"> <div class="gallery-item" id="galleryItem1">
<div class="stats-grid"> <img src="https://picsum.photos/1920/1080?random=60" alt="">
<div class="stat-item reveal"> <div class="gallery-caption">
<h3>10K+</h3> <h3>Дизайн</h3>
<p>Водопадов</p> <p>Каждая линия имеет значение</p>
</div> </div>
<div class="stat-item reveal">
<h3>130</h3>
<p>Вулканов</p>
</div> </div>
<div class="stat-item reveal"> <div class="gallery-item" id="galleryItem2">
<h3>600</h3> <img src="https://picsum.photos/1920/1080?random=61" alt="">
<p>Горячих источников</p> <div class="gallery-caption">
<h3>Инновации</h3>
<p>Технологии, которые вдохновляют</p>
</div> </div>
<div class="stat-item reveal">
<h3>24/7</h3>
<p>Северное сияние зимой</p>
</div> </div>
<div class="gallery-item" id="galleryItem3">
<img src="https://picsum.photos/1920/1080?random=62" alt="">
<div class="gallery-caption">
<h3>Будущее</h3>
<p>Создаём завтрашний день сегодня</p>
</div> </div>
</div> </div>
</section> </section>
<!-- Gallery --> <!-- CTA Section -->
<section class="gallery" id="gallery"> <section class="cta-section">
<div class="section-header reveal"> <h2 class="cta-title reveal">Готовы к будущему?</h2>
<p class="section-label">Галерея</p> <p class="cta-subtitle reveal">Откройте для себя мир инноваций уже сегодня</p>
<h2 class="section-title">Моменты, которые останутся навсегда</h2> <div class="cta-buttons reveal">
<a href="#products" class="btn btn-primary">Выбрать устройство</a>
<a href="#" class="btn btn-secondary">Связаться с нами</a>
</div> </div>
<div class="gallery-grid">
<div class="gallery-item reveal">
<img src="https://picsum.photos/1200/800?random=30" alt="Пейзаж" class="gallery-img">
</div>
<div class="gallery-item reveal">
<img src="https://picsum.photos/600/600?random=31" alt="Горы" class="gallery-img">
</div>
<div class="gallery-item reveal">
<img src="https://picsum.photos/600/600?random=32" alt="Океан" class="gallery-img">
</div>
<div class="gallery-item reveal">
<img src="https://picsum.photos/600/600?random=33" alt="Ледник" class="gallery-img">
</div>
<div class="gallery-item reveal">
<img src="https://picsum.photos/600/600?random=34" alt="Пляж" class="gallery-img">
</div>
<div class="gallery-item reveal">
<img src="https://picsum.photos/600/600?random=35" alt="Фьорд" class="gallery-img">
</div>
</div>
</section>
<!-- CTA -->
<section class="cta" id="book">
<h2 class="reveal">Готовы к приключению?</h2>
<p class="reveal">Забронируйте тур в Исландию прямо сейчас и получите скидку 20% на групповые поездки</p>
<a href="#" class="btn btn-primary reveal">Забронировать тур</a>
</section> </section>
<!-- Footer --> <!-- Footer -->
<footer> <footer>
<div class="footer-content">
<div class="footer-links"> <div class="footer-links">
<a href="#">О нас</a> <a href="#">О компании</a>
<a href="#">Туры</a> <a href="#">Карьера</a>
<a href="#">Блог</a> <a href="#">Инвесторам</a>
<a href="#">Новости</a>
<a href="#">Конфиденциальность</a>
<a href="#">Контакты</a> <a href="#">Контакты</a>
<a href="#">Политика конфиденциальности</a>
</div> </div>
<p>&copy; 2026 Iceland Travel. Все права защищены.</p> <p class="copyright">&copy; 2026 Apple Style Demo. Все права защищены.</p>
</div>
</footer> </footer>
<script> <script src="app.js"></script>
// Многослойный параллакс для Hero
const heroBg1 = document.getElementById('heroBg1');
const heroBg2 = document.getElementById('heroBg2');
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
if (scrolled < window.innerHeight) {
heroBg1.style.transform = `translateY(${scrolled * 0.6}px) scale(${1 + scrolled * 0.0008})`;
heroBg2.style.transform = `translateY(${scrolled * 0.3}px) scale(${1 + scrolled * 0.0005})`;
}
});
// Параллакс для средней секции
const parallaxBg = document.getElementById('parallaxBg');
const parallaxSection = document.getElementById('parallaxSection');
window.addEventListener('scroll', () => {
const rect = document.querySelector('.parallax-section').getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
const offset = (window.innerHeight - rect.top) * 0.25;
parallaxBg.style.transform = `translateY(${offset}px)`;
}
});
// Анимация появления
const reveals = document.querySelectorAll('.reveal');
function checkReveal() {
reveals.forEach(el => {
const top = el.getBoundingClientRect().top;
const trigger = window.innerHeight * 0.85;
if (top < trigger) {
el.classList.add('active');
}
});
}
window.addEventListener('scroll', checkReveal);
checkReveal();
// Плавный скролл
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
</script>
</body> </body>
</html> </html>

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "apple-style-app",
"private": true,
"type": "commonjs",
"scripts": {
"start": "node server.js"
}
}

185
server.js Normal file
View File

@ -0,0 +1,185 @@
const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json");
function readData() {
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { products: [], announcements: [], siteSettings: {} }; }
}
function writeData(data) {
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
}
function parseBody(req) {
return new Promise((resolve) => {
let s = "";
req.on("data", (c) => (s += c));
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
});
}
function json(res, data, status = 200) {
res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
res.end(JSON.stringify(data));
}
function notFound(res) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
}
const routes = {
// PUBLIC API
"GET /api/products": (req, res) => {
const data = readData();
json(res, { ok: true, data: data.products.sort((a, b) => a.order - b.order) });
},
"GET /api/products/:id": (req, res, params) => {
const data = readData();
const product = data.products.find(p => p.id === parseInt(params.id));
if (!product) return json(res, { ok: false, error: "Product not found" }, 404);
json(res, { ok: true, data: product });
},
"GET /api/announcements": (req, res) => {
const data = readData();
json(res, { ok: true, data: data.announcements.filter(a => a.active) });
},
"GET /api/settings": (req, res) => {
const data = readData();
json(res, { ok: true, data: data.siteSettings });
},
// ADMIN API
"POST /api/admin/products": async (req, res) => {
const body = await parseBody(req);
const data = readData();
const newProduct = {
id: Date.now(),
...body,
order: data.products.length + 1
};
data.products.push(newProduct);
writeData(data);
json(res, { ok: true, data: newProduct }, 201);
},
"PUT /api/admin/products/:id": async (req, res, params) => {
const body = await parseBody(req);
const data = readData();
const idx = data.products.findIndex(p => p.id === parseInt(params.id));
if (idx === -1) return json(res, { ok: false, error: "Product not found" }, 404);
data.products[idx] = { ...data.products[idx], ...body, id: parseInt(params.id) };
writeData(data);
json(res, { ok: true, data: data.products[idx] });
},
"DELETE /api/admin/products/:id": (req, res, params) => {
const data = readData();
data.products = data.products.filter(p => p.id !== parseInt(params.id));
writeData(data);
json(res, { ok: true, deleted: parseInt(params.id) });
},
"POST /api/admin/announcements": async (req, res) => {
const body = await parseBody(req);
const data = readData();
const newAnnouncement = { id: Date.now(), ...body, active: true };
data.announcements.push(newAnnouncement);
writeData(data);
json(res, { ok: true, data: newAnnouncement }, 201);
},
"PUT /api/admin/announcements/:id": async (req, res, params) => {
const body = await parseBody(req);
const data = readData();
const idx = data.announcements.findIndex(a => a.id === parseInt(params.id));
if (idx === -1) return json(res, { ok: false, error: "Not found" }, 404);
data.announcements[idx] = { ...data.announcements[idx], ...body, id: parseInt(params.id) };
writeData(data);
json(res, { ok: true, data: data.announcements[idx] });
},
"DELETE /api/admin/announcements/:id": (req, res, params) => {
const data = readData();
data.announcements = data.announcements.filter(a => a.id !== parseInt(params.id));
writeData(data);
json(res, { ok: true, deleted: parseInt(params.id) });
},
"PUT /api/admin/settings": async (req, res) => {
const body = await parseBody(req);
const data = readData();
data.siteSettings = { ...data.siteSettings, ...body };
writeData(data);
json(res, { ok: true, data: data.siteSettings });
}
};
const mimeTypes = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml"
};
http.createServer(async (req, res) => {
const url = req.url.split("?")[0];
const method = req.method;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (method === "OPTIONS") {
res.writeHead(204);
return res.end();
}
// API ROUTING
for (const [route, handler] of Object.entries(routes)) {
const [m, p] = route.split(" ");
if (method !== m) continue;
const routeParts = p.split("/");
const urlParts = url.split("/");
if (routeParts.length !== urlParts.length) continue;
const params = {};
let match = true;
for (let i = 0; i < routeParts.length; i++) {
if (routeParts[i].startsWith(":")) {
params[routeParts[i].slice(1)] = urlParts[i];
} else if (routeParts[i] !== urlParts[i]) {
match = false;
break;
}
}
if (match) {
return handler(req, res, params);
}
}
// STATIC FILES
let file = url === "/" ? "/index.html" : url;
const full = path.join(__dirname, file);
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
const ext = path.extname(full);
const type = mimeTypes[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": type });
return res.end(fs.readFileSync(full));
}
notFound(res);
}).listen(PORT, () => console.log("Server running on port " + PORT));

663
style.css Normal file
View File

@ -0,0 +1,663 @@
/* ===================================
APPLE-STYLE DESIGN SYSTEM
=================================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #000000;
--bg-secondary: #1d1d1f;
--bg-tertiary: #2d2d30;
--text-primary: #f5f5f7;
--text-secondary: #86868b;
--text-tertiary: #6e6e73;
--accent: #2997ff;
--accent-hover: #147ce5;
--gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-2: linear-gradient(180deg, #1a1a2e 0%, #000000 100%);
--nav-height: 48px;
--transition-smooth: cubic-bezier(0.25, 1, 0.5, 1);
--transition-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Announcement Bar */
.announcement-bar {
background: var(--bg-secondary);
padding: 12px 24px;
text-align: center;
font-size: 14px;
color: var(--text-secondary);
border-bottom: 1px solid rgba(255,255,255,0.1);
transition: all 0.4s var(--transition-smooth);
}
.announcement-bar.hidden {
transform: translateY(-100%);
opacity: 0;
}
.announcement-bar a {
color: var(--accent);
text-decoration: none;
}
.announcement-bar a:hover {
text-decoration: underline;
}
/* Navigation */
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
z-index: 1000;
border-bottom: 1px solid rgba(255,255,255,0.08);
transition: all 0.3s var(--transition-smooth);
}
.nav.scrolled {
background: rgba(0, 0, 0, 0.95);
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 22px;
color: var(--text-primary);
text-decoration: none;
transition: opacity 0.3s;
}
.logo:hover {
opacity: 0.8;
}
.nav-links {
display: flex;
gap: 32px;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
font-size: 13px;
font-weight: 400;
transition: color 0.3s;
letter-spacing: -0.01em;
}
.nav-links a:hover {
color: var(--text-primary);
}
.nav-links .admin-link {
color: var(--accent);
}
/* Hero Section */
.hero {
height: 100vh;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.hero-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 120%;
background: radial-gradient(ellipse at center, #2a2a3e 0%, #0a0a0f 50%, #000000 100%);
z-index: 1;
will-change: transform;
}
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(ellipse at center, transparent 0%, rgba(0,0,0,0.6) 100%);
z-index: 2;
}
.hero-content {
position: relative;
z-index: 3;
text-align: center;
max-width: 900px;
padding: 0 24px;
}
.hero-title {
font-size: clamp(56px, 10vw, 112px);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.05;
margin-bottom: 16px;
background: linear-gradient(180deg, #ffffff 0%, #b0b0b5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
opacity: 0;
transform: translateY(40px);
animation: fadeUp 1s var(--transition-smooth) forwards;
animation-delay: 0.3s;
}
.hero-subtitle {
font-size: clamp(24px, 4vw, 32px);
color: var(--text-secondary);
font-weight: 400;
margin-bottom: 40px;
opacity: 0;
transform: translateY(40px);
animation: fadeUp 1s var(--transition-smooth) forwards;
animation-delay: 0.5s;
}
.hero-cta {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
opacity: 0;
transform: translateY(40px);
animation: fadeUp 1s var(--transition-smooth) forwards;
animation-delay: 0.7s;
}
.btn {
display: inline-block;
padding: 14px 28px;
border-radius: 980px;
font-size: 17px;
font-weight: 500;
text-decoration: none;
transition: all 0.3s var(--transition-smooth);
cursor: pointer;
border: none;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
transform: scale(1.02);
}
.btn-secondary {
background: transparent;
color: var(--accent);
border: 2px solid var(--accent);
}
.btn-secondary:hover {
background: var(--accent);
color: white;
}
/* Scroll Indicator */
.scroll-indicator {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
opacity: 0;
animation: fadeIn 1s ease forwards;
animation-delay: 1.2s;
}
.mouse {
width: 26px;
height: 40px;
border: 2px solid var(--text-secondary);
border-radius: 13px;
position: relative;
}
.wheel {
width: 4px;
height: 8px;
background: var(--text-secondary);
border-radius: 2px;
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
animation: scroll 2s infinite;
}
@keyframes scroll {
0%, 100% { opacity: 1; transform: translateX(-50%) translateY(0); }
50% { opacity: 0.5; transform: translateX(-50%) translateY(12px); }
}
@keyframes fadeUp {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
to { opacity: 1; }
}
/* Product Showcase */
.product-showcase {
padding: 140px 24px;
background: var(--bg-primary);
}
.showcase-intro {
text-align: center;
margin-bottom: 80px;
}
.showcase-title {
font-size: clamp(40px, 6vw, 64px);
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 12px;
}
.showcase-subtitle {
font-size: clamp(20px, 3vw, 28px);
color: var(--text-secondary);
}
.products-container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 32px;
}
.product-card {
background: var(--bg-secondary);
border-radius: 24px;
overflow: hidden;
transition: all 0.6s var(--transition-smooth);
opacity: 0;
transform: translateY(40px);
}
.product-card.visible {
opacity: 1;
transform: translateY(0);
}
.product-card:hover {
transform: translateY(-12px);
box-shadow: 0 30px 60px rgba(0,0,0,0.4);
}
.product-image {
width: 100%;
height: 280px;
object-fit: cover;
transition: transform 0.6s var(--transition-smooth);
}
.product-card:hover .product-image {
transform: scale(1.08);
}
.product-content {
padding: 32px;
}
.product-name {
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
letter-spacing: -0.01em;
}
.product-tagline {
color: var(--text-secondary);
font-size: 16px;
margin-bottom: 16px;
}
.product-price {
font-size: 20px;
font-weight: 600;
color: var(--accent);
margin-bottom: 20px;
}
.product-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.feature-tag {
background: var(--bg-tertiary);
color: var(--text-secondary);
padding: 6px 14px;
border-radius: 980px;
font-size: 13px;
font-weight: 500;
}
/* Features Section */
.features {
position: relative;
padding: 140px 24px;
overflow: hidden;
}
.features-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 120%;
background: url('https://picsum.photos/1920/1200?random=100') center/cover;
opacity: 0.15;
will-change: transform;
}
.features-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, var(--bg-primary) 0%, rgba(0,0,0,0.8) 100%);
}
.features-content {
position: relative;
z-index: 2;
max-width: 1400px;
margin: 0 auto;
}
.section-title {
font-size: clamp(40px, 6vw, 64px);
font-weight: 700;
text-align: center;
margin-bottom: 80px;
letter-spacing: -0.02em;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 32px;
}
.feature-card {
background: rgba(29, 29, 31, 0.6);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px 32px;
text-align: center;
border: 1px solid rgba(255,255,255,0.05);
transition: all 0.4s var(--transition-smooth);
}
.feature-card:hover {
background: rgba(29, 29, 31, 0.8);
transform: translateY(-8px);
}
.feature-icon {
font-size: 48px;
margin-bottom: 20px;
}
.feature-card h3 {
font-size: 22px;
margin-bottom: 12px;
font-weight: 600;
}
.feature-card p {
color: var(--text-secondary);
font-size: 16px;
}
/* Vision Section */
.vision {
padding: 140px 24px;
background: var(--bg-secondary);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
max-width: 1400px;
margin: 0 auto;
align-items: center;
}
.vision-content {
padding: 40px;
}
.vision-title {
font-size: clamp(40px, 5vw, 64px);
font-weight: 700;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.vision-subtitle {
font-size: clamp(20px, 3vw, 26px);
color: var(--text-secondary);
margin-bottom: 48px;
}
.vision-features {
display: flex;
flex-direction: column;
gap: 24px;
}
.vision-feature {
display: flex;
gap: 20px;
align-items: flex-start;
}
.vision-feature-icon {
font-size: 32px;
flex-shrink: 0;
}
.vision-feature h4 {
font-size: 20px;
margin-bottom: 6px;
font-weight: 600;
}
.vision-feature p {
color: var(--text-secondary);
font-size: 16px;
}
.vision-image {
border-radius: 24px;
overflow: hidden;
}
.vision-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Scroll Gallery */
.scroll-gallery {
position: relative;
}
.gallery-item {
position: relative;
height: 80vh;
overflow: hidden;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.gallery-caption {
position: absolute;
bottom: 60px;
left: 60px;
z-index: 2;
}
.gallery-caption h3 {
font-size: clamp(32px, 5vw, 56px);
font-weight: 700;
margin-bottom: 8px;
text-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.gallery-caption p {
font-size: 18px;
color: rgba(255,255,255,0.9);
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
/* CTA Section */
.cta-section {
padding: 140px 24px;
text-align: center;
background: var(--gradient-2);
}
.cta-title {
font-size: clamp(40px, 6vw, 72px);
font-weight: 700;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.cta-subtitle {
font-size: clamp(20px, 3vw, 26px);
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto 48px;
}
.cta-buttons {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
/* Footer */
footer {
padding: 60px 24px;
border-top: 1px solid rgba(255,255,255,0.1);
background: var(--bg-secondary);
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
}
.footer-links {
display: flex;
justify-content: center;
gap: 32px;
flex-wrap: wrap;
margin-bottom: 32px;
}
.footer-links a {
color: var(--text-secondary);
text-decoration: none;
font-size: 13px;
transition: color 0.3s;
}
.footer-links a:hover {
color: var(--text-primary);
}
.copyright {
text-align: center;
color: var(--text-tertiary);
font-size: 13px;
}
/* Reveal Animation */
.reveal {
opacity: 0;
transform: translateY(50px);
transition: all 1s var(--transition-smooth);
}
.reveal.active {
opacity: 1;
transform: translateY(0);
}
/* Responsive */
@media (max-width: 768px) {
.nav-links {
gap: 20px;
}
.vision {
grid-template-columns: 1fr;
}
.products-container {
grid-template-columns: 1fr;
}
.gallery-caption {
left: 24px;
bottom: 40px;
}
}