diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..7ceb43c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,108 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+pythonenv*
+
+# Flask
+instance/
+.webassets-cache
+.env.local
+.env.*.local
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+desktop.ini
+
+# Логи и базы данных
+*.log
+*.sql
+*.sqlite
+*.db
+
+# Кэш станций (создается автоматически при запуске)
+backend/stations_cache.json
+backend/__pycache__/
+
+# Виртуальное окружение
+backend/venv/
+backend/env/
+backend/venv*/
+
+# Файлы с секретами
+backend/.env
+.secrets
+
+# Временные файлы
+*.tmp
+*.temp
+*.bak
+*.backup
+
+# Результаты тестов
+.coverage
+htmlcov/
+.pytest_cache/
+.tox/
+
+# Файлы зависимостей (они уже есть в requirements.txt)
+# Не удаляем сам requirements.txt
+
+# Node modules (если появятся)
+node_modules/
+npm-debug.log
+
+# Системные файлы Windows
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+
+# Системные файлы macOS
+.AppleDouble
+.LSOverride
+._*
+
+# Файлы отладки
+*.pid
+*.seed
+*.pid.lock
+
+# Secrets
+backend/.env
+.env
+*.env
+
+# Python
+__pycache__/
+*.py[cod]
+venv/
+env/
+
+# Cache
+electric-train-tracker/backend/stations_cache.json
+
+# IDE
+.vscode/
+.idea/
+
+# Yandex schedule cache
+yaschedule.core.cache
+*.cache
+backend/yaschedule.core.cache
+
+# Temporary files
+*.tmp
+*.temp
+*.cache
\ No newline at end of file
diff --git a/electric-train-tracker/backend/requirements.txt b/electric-train-tracker/backend/requirements.txt
new file mode 100644
index 00000000..051d8e4b
--- /dev/null
+++ b/electric-train-tracker/backend/requirements.txt
@@ -0,0 +1,5 @@
+flask==2.3.3
+flask-cors==4.0.0
+requests==2.31.0
+yaschedule==0.0.4.2
+python-dotenv==1.0.0
\ No newline at end of file
diff --git a/electric-train-tracker/backend/server.py b/electric-train-tracker/backend/server.py
new file mode 100644
index 00000000..6069a82b
--- /dev/null
+++ b/electric-train-tracker/backend/server.py
@@ -0,0 +1,143 @@
+import os
+from flask import Flask, request, jsonify, send_from_directory
+from flask_cors import CORS
+from yaschedule.core import YaSchedule
+from station_cache import load_all_stations, search_stations
+from datetime import datetime
+import requests
+from dotenv import load_dotenv
+
+load_dotenv()
+
+app = Flask(__name__, static_folder='../frontend/static', static_url_path='/static')
+CORS(app)
+
+API_KEY = os.getenv('YANDEX_API_KEY')
+FLASK_DEBUG = os.getenv('FLASK_DEBUG', 'True') == 'True'
+PORT = int(os.getenv('PORT', 5000))
+
+if not API_KEY:
+ print("❌ ОШИБКА: YANDEX_API_KEY не найден в .env файле!")
+ print("Создайте файл .env в папке backend и добавьте:")
+ print("YANDEX_API_KEY=ваш_ключ_здесь")
+ exit(1)
+
+print(f"✅ API ключ загружен из .env")
+
+train_api = YaSchedule(API_KEY)
+
+print("📡 Загрузка базы станций...")
+station_db = load_all_stations(API_KEY)
+print("✅ База готова!")
+
+@app.route('/')
+def serve_index():
+ return send_from_directory('../frontend/pages', 'index.html')
+
+@app.route('/api/search/stations', methods=['GET'])
+def search_stations_by_name():
+ search_term = request.args.get('q', '')
+ if len(search_term) < 2:
+ return jsonify([])
+
+ results = search_stations(station_db, search_term)
+ return jsonify(results)
+
+@app.route('/api/geo/nearby', methods=['GET'])
+def get_nearby_railway_stations():
+ lat = request.args.get('lat')
+ lng = request.args.get('lng')
+ distance = request.args.get('distance', 50)
+
+ if not lat or not lng:
+ return jsonify([])
+
+ try:
+ url = "https://api.rasp.yandex.net/v3.0/nearest_stations/"
+ params = {
+ 'apikey': API_KEY,
+ 'lat': lat,
+ 'lng': lng,
+ 'distance': distance,
+ 'transport_types': 'train,suburban',
+ 'station_types': 'train_station',
+ 'lang': 'ru_RU',
+ 'limit': 20
+ }
+
+ response = requests.get(url, params=params, timeout=10)
+ if response.status_code == 200:
+ data = response.json()
+ stations = []
+ for station in data.get('stations', []):
+ if station.get('transport_type') in ['train', 'suburban']:
+ stations.append({
+ 'title': station.get('title'),
+ 'code': station.get('code'),
+ 'distance': station.get('distance'),
+ 'latitude': station.get('lat'),
+ 'longitude': station.get('lng'),
+ 'city': station.get('city', '')
+ })
+ return jsonify(stations)
+ else:
+ return jsonify([])
+ except Exception as e:
+ print(f"Error: {e}")
+ return jsonify([])
+
+@app.route('/api/recommended/stations', methods=['GET'])
+def get_recommended_stations():
+ popular_stations = [
+ {'name': 'Москва (Киевский вокзал)', 'code': 's9603402'},
+ {'name': 'Санкт-Петербург (Витебский)', 'code': 's9603551'},
+ {'name': 'Москва (Казанский вокзал)', 'code': 's9603404'},
+ {'name': 'Москва (Ярославский вокзал)', 'code': 's9603408'},
+ {'name': 'Москва (Павелецкий вокзал)', 'code': 's9603405'},
+ ]
+ return jsonify(popular_stations)
+
+@app.route('/api/timetable/station', methods=['GET'])
+def get_station_timetable():
+ station_id = request.args.get('station')
+ travel_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
+
+ if not station_id:
+ return jsonify({'error': 'Station required'}), 400
+
+ try:
+ schedule = train_api.get_station_schedule(
+ station=station_id,
+ transport_types='suburban',
+ date=travel_date
+ )
+ return jsonify(schedule)
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/api/timetable/route', methods=['GET'])
+def get_route_timetable():
+ from_station = request.args.get('from')
+ to_station = request.args.get('to')
+ travel_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d'))
+
+ if not from_station or not to_station:
+ return jsonify({'error': 'Both stations required'}), 400
+
+ try:
+ route_data = train_api.get_schedule(
+ from_station=from_station,
+ to_station=to_station,
+ transport_types='suburban',
+ date=travel_date
+ )
+ return jsonify(route_data)
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/api/health', methods=['GET'])
+def health_check():
+ return jsonify({'status': 'ok', 'api_configured': bool(API_KEY)})
+
+if __name__ == '__main__':
+ app.run(debug=FLASK_DEBUG, port=PORT)
\ No newline at end of file
diff --git a/electric-train-tracker/backend/station_cache.py b/electric-train-tracker/backend/station_cache.py
new file mode 100644
index 00000000..8ca5871d
--- /dev/null
+++ b/electric-train-tracker/backend/station_cache.py
@@ -0,0 +1,52 @@
+import json
+import os
+from yaschedule.core import YaSchedule
+
+def init_api(api_key):
+ return YaSchedule(api_key)
+
+def load_all_stations(api_key):
+ cache_file = 'stations_cache.json'
+
+ if os.path.exists(cache_file):
+ print("Загрузка из кэша...")
+ with open(cache_file, 'r', encoding='utf-8') as f:
+ return json.load(f)
+
+ print("Скачивание с API (первый запуск, подождите)...")
+ api = init_api(api_key)
+ stations_data = api.get_all_stations()
+
+ with open(cache_file, 'w', encoding='utf-8') as f:
+ json.dump(stations_data, f, ensure_ascii=False, indent=2)
+
+ print("Кэш сохранен!")
+ return stations_data
+
+def search_stations(stations_data, query):
+ results = []
+ query_lower = query.lower()
+
+ for country in stations_data.get('countries', []):
+ for region in country.get('regions', []):
+ for settlement in region.get('settlements', []):
+ for station in settlement.get('stations', []):
+ title = station.get('title', '')
+ station_type = station.get('station_type', '')
+
+ if station_type == 'train_station':
+ if query_lower in title.lower():
+ results.append({
+ 'title': title,
+ 'code': station.get('codes', {}).get('yandex_code'),
+ 'city': settlement.get('title', '')
+ })
+
+ unique = []
+ seen = set()
+ for r in results:
+ if r['title'] not in seen:
+ seen.add(r['title'])
+ unique.append(r)
+
+ return unique[:20]
\ No newline at end of file
diff --git a/electric-train-tracker/frontend/pages/index.html b/electric-train-tracker/frontend/pages/index.html
new file mode 100644
index 00000000..5949d98f
--- /dev/null
+++ b/electric-train-tracker/frontend/pages/index.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+ Электрички — расписание пригородных поездов
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Выберите станцию на карте
+
Нажмите в любом месте карты, чтобы найти ближайшие ЖД станции
+
+
+
+
+
+
📍 Ближайшие станции:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Любимые станции
+
+
⭐ Нет избранных станций
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/electric-train-tracker/frontend/static/scripts/app.js b/electric-train-tracker/frontend/static/scripts/app.js
new file mode 100644
index 00000000..1c291429
--- /dev/null
+++ b/electric-train-tracker/frontend/static/scripts/app.js
@@ -0,0 +1,381 @@
+const API_URL = 'http://localhost:5000/api';
+
+let selectedStationCode = null;
+let selectedStationName = null;
+let fromStationCode = null;
+let toStationCode = null;
+let favorites = [];
+let map = null;
+let mapMarkers = [];
+
+$(document).ready(function() {
+ const saved = localStorage.getItem('railway_favorites');
+ if (saved) favorites = JSON.parse(saved);
+
+ const today = new Date().toISOString().split('T')[0];
+ $('#station-date').val(today);
+ $('#route-date').val(today);
+
+ initTabs();
+ initSearch();
+ loadRecommendedStations();
+ renderFavorites();
+
+ $('#search-station-btn').click(loadStationTimetable);
+ $('#search-route-btn').click(findRouteTimetable);
+});
+
+function initTabs() {
+ $('.tab-btn').click(function() {
+ $('.tab-btn').removeClass('active');
+ $(this).addClass('active');
+
+ const tab = $(this).data('tab');
+ $('.tab-content').removeClass('active');
+ $(`#${tab}-tab`).addClass('active');
+ $('#results').hide();
+
+ if (tab === 'map') setTimeout(() => initMap(), 100);
+ if (tab === 'favorites') renderFavorites();
+ });
+}
+
+function initSearch() {
+ const fields = [
+ { input: '#station-input', suggestions: '#station-suggestions', setCode: (c) => selectedStationCode = c, setName: (n) => selectedStationName = n },
+ { input: '#from-input', suggestions: '#from-suggestions', setCode: (c) => fromStationCode = c },
+ { input: '#to-input', suggestions: '#to-suggestions', setCode: (c) => toStationCode = c }
+ ];
+
+ fields.forEach(field => {
+ let timeout;
+ $(field.input).on('input', function() {
+ clearTimeout(timeout);
+ const query = $(this).val();
+ if (query.length < 2) {
+ $(field.suggestions).hide();
+ return;
+ }
+ timeout = setTimeout(() => {
+ $.get(`${API_URL}/search/stations?q=${encodeURIComponent(query)}`, function(data) {
+ $(field.suggestions).empty().show();
+ data.forEach(s => {
+ const item = $(`${s.title}
${s.city || ''}
`);
+ item.click(() => {
+ $(field.input).val(s.title);
+ if (field.setCode) field.setCode(s.code);
+ if (field.setName) field.setName(s.title);
+ $(field.suggestions).hide();
+ });
+ $(field.suggestions).append(item);
+ });
+ });
+ }, 300);
+ });
+ $(document).click(e => {
+ if (!$(e.target).closest(field.input).length) $(field.suggestions).hide();
+ });
+ });
+}
+
+function loadRecommendedStations() {
+ $.get(`${API_URL}/recommended/stations`, function(data) {
+ const container = $('#popular-list');
+ container.empty();
+ data.forEach(s => {
+ const item = $(`${s.name}
`);
+ item.click(() => {
+ selectedStationCode = s.code;
+ selectedStationName = s.name;
+ $('#station-input').val(s.name);
+ loadStationTimetable();
+ });
+ container.append(item);
+ });
+ });
+}
+
+function loadStationTimetable() {
+ if (!selectedStationCode) {
+ showMessage('Выберите станцию!');
+ return;
+ }
+ const date = $('#station-date').val();
+ showLoading();
+ $('#results').show();
+ $.get(`${API_URL}/timetable/station?station=${selectedStationCode}&date=${date}`)
+ .done(data => displayTimetable(data, selectedStationName))
+ .fail(() => $('#schedule-list').html('Ошибка загрузки
'))
+ .always(() => hideLoading());
+}
+
+function findRouteTimetable() {
+ if (!fromStationCode || !toStationCode) {
+ showMessage('Выберите обе станции!');
+ return;
+ }
+ const date = $('#route-date').val();
+ showLoading();
+ $('#results').show();
+ $.get(`${API_URL}/timetable/route?from=${fromStationCode}&to=${toStationCode}&date=${date}`)
+ .done(data => displayTimetable(data))
+ .fail(() => $('#schedule-list').html('Ошибка загрузки
'))
+ .always(() => hideLoading());
+}
+
+function displayTimetable(data, stationContextName) {
+ let trains = [];
+
+ if (data && data.schedule && Array.isArray(data.schedule)) {
+ trains = data.schedule;
+ } else if (data && data.segments && Array.isArray(data.segments)) {
+ trains = data.segments;
+ } else if (Array.isArray(data)) {
+ trains = data;
+ }
+
+ if (trains.length === 0) {
+ $('#schedule-list').html('🚂 Поездов не найдено
');
+ $('#results-count').text('');
+ return;
+ }
+
+ $('#results-count').text(`${trains.length} поезд(ов)`);
+ $('#schedule-list').empty();
+
+ const favContextName = stationContextName || $('#station-input').val() || 'Станция';
+
+ trains.forEach(train => {
+ let trainNumber = '---';
+ let depTime = '---';
+ let arrTime = '---';
+ let days = '';
+ let direction = '';
+
+ if (train.thread && train.thread.number) trainNumber = train.thread.number;
+ else if (train.number) trainNumber = train.number;
+
+ if (train.departure) depTime = train.departure;
+ if (train.arrival) arrTime = train.arrival;
+ if (train.days) days = train.days;
+ if (train.direction) direction = train.direction;
+ else if (train.thread && train.thread.title) direction = train.thread.title;
+
+ const fromName = train.from?.title || '';
+ const toName = train.to?.title || '';
+
+ const isFav = favorites.includes(favContextName);
+
+ const card = $(`
+
+
+
+ ${fromName && toName ? `${fromName} → ${toName}` : (direction || favContextName)}
+
+
+
+ 🕐 Отправление:
+ ${depTime}
+
+
+ 🏁 Прибытие:
+ ${arrTime}
+
+
+ ${days ? `
📅 ${days}
` : ''}
+
+ `);
+
+ card.find('.favorite-btn').click(function(e) {
+ e.stopPropagation();
+ const name = $(this).data('name');
+ toggleFavorite(name);
+ updateFavButton($(this), name);
+ });
+
+ $('#schedule-list').append(card);
+ });
+}
+
+function toggleFavorite(name) {
+ const idx = favorites.indexOf(name);
+ if (idx === -1) {
+ favorites.push(name);
+ showMessage(`❤️ ${name} добавлена в избранное`);
+ } else {
+ favorites.splice(idx, 1);
+ showMessage(`💔 ${name} удалена из избранного`);
+ }
+ localStorage.setItem('railway_favorites', JSON.stringify(favorites));
+ renderFavorites();
+}
+
+function isFavorite(name) {
+ return favorites.includes(name);
+}
+
+function updateFavButton(btn, name) {
+ const fav = isFavorite(name);
+ btn.toggleClass('active', fav);
+ btn.find('i').attr('class', `fas ${fav ? 'fa-heart' : 'fa-heart-o'}`);
+ btn.find('span').text(fav ? 'В избранном' : 'Сохранить');
+}
+
+function renderFavorites() {
+ const container = $('#favorites-list');
+ if (favorites.length === 0) {
+ container.html('⭐ Нет избранных станций. Добавьте их из расписания!
');
+ return;
+ }
+ container.empty();
+ favorites.forEach(name => {
+ const card = $(`
+
+ `);
+ card.click(e => {
+ if (!$(e.target).hasClass('remove-fav')) {
+ selectedStationName = name;
+ $('#station-input').val(name);
+ $('.tab-btn[data-tab="station"]').click();
+ findStationCodeByName(name);
+ }
+ });
+ card.find('.remove-fav').click(e => {
+ e.stopPropagation();
+ toggleFavorite(name);
+ });
+ container.append(card);
+ });
+}
+
+function findStationCodeByName(stationName) {
+ $.get(`${API_URL}/search/stations?q=${encodeURIComponent(stationName)}`, function(data) {
+ if (data && data.length > 0) {
+ selectedStationCode = data[0].code;
+ selectedStationName = data[0].title;
+ loadStationTimetable();
+ } else {
+ showMessage(`❌ Станция "${stationName}" не найдена`);
+ }
+ });
+}
+
+function initMap() {
+ if (map) return;
+ if (typeof ymaps === 'undefined') { setTimeout(initMap, 200); return; }
+
+ ymaps.ready(() => {
+ map = new ymaps.Map('map', {
+ center: [55.751574, 37.573856],
+ zoom: 10,
+ controls: ['zoomControl', 'fullscreenControl', 'geolocationControl']
+ });
+
+ const searchControl = new ymaps.control.SearchControl({ options: { provider: 'yandex#search', size: 'large', noSelect: true } });
+ map.controls.add(searchControl);
+
+ map.events.add('click', function(e) {
+ const coords = e.get('coords');
+ findNearbyStations(coords[0], coords[1]);
+ });
+
+ $('#find-my-location').click(function() {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(function(position) {
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+ map.setCenter([lat, lng], 14);
+ findNearbyStations(lat, lng);
+ showMessage(`📍 Определено ваше местоположение`);
+ }, function() { showMessage(`❌ Не удалось определить местоположение`); });
+ } else { showMessage(`❌ Геолокация не поддерживается`); }
+ });
+ });
+}
+
+function findNearbyStations(lat, lng) {
+ showMessage(`🔍 Поиск станций рядом...`);
+ $('#nearby-stations-list').html('Загрузка...
');
+ $('#selected-station-info').show();
+
+ $.get(`${API_URL}/geo/nearby?lat=${lat}&lng=${lng}&distance=50`)
+ .done(data => displayNearbyStations(data))
+ .fail(() => $('#nearby-stations-list').html('Ошибка поиска станций
'));
+}
+
+function displayNearbyStations(stations) {
+ if (!stations || stations.length === 0) {
+ $('#nearby-stations-list').html('🚉 ЖД станции не найдены
');
+ return;
+ }
+
+ $('#nearby-stations-list').empty();
+ mapMarkers.forEach(marker => map.geoObjects.remove(marker));
+ mapMarkers = [];
+
+ stations.forEach(station => {
+ const stationLat = station.latitude;
+ const stationLng = station.longitude;
+ const stationName = station.title;
+ const stationCode = station.code;
+ const distance = station.distance ? station.distance.toFixed(1) : '?';
+ const city = station.city || '';
+ const buttonId = 'btn_' + Math.random().toString(36).substr(2, 9);
+
+ if (stationLat && stationLng) {
+ const marker = new ymaps.Placemark([stationLat, stationLng], {
+ hintContent: stationName,
+ balloonContent: `${stationName}
Расстояние: ${distance} км
`
+ }, { preset: 'islands#blueRailwayIcon' });
+
+ marker.events.add('balloonopen', function() {
+ const button = document.getElementById(buttonId);
+ if (button) button.onclick = () => { selectStationFromMap(stationCode, stationName); marker.balloon.close(); };
+ });
+
+ map.geoObjects.add(marker);
+ mapMarkers.push(marker);
+ }
+
+ const stationItem = $(`${stationName}${distance} км${city ? `${city}` : ''}
`);
+ stationItem.find('.select-station-btn').click(() => selectStationFromMap(stationCode, stationName));
+ stationItem.click(() => selectStationFromMap(stationCode, stationName));
+ $('#nearby-stations-list').append(stationItem);
+ });
+}
+
+function selectStationFromMap(code, name) {
+ if (code) selectedStationCode = code;
+ selectedStationName = name;
+ $('#station-input').val(name);
+ showMessage(`✅ Выбрана станция: ${name}`);
+ $('.tab-btn[data-tab="station"]').click();
+ if (code) setTimeout(() => loadStationTimetable(), 100);
+}
+
+window.selectStationFromMap = selectStationFromMap;
+
+function showMessage(text) {
+ let toast = $('#toast');
+ if (!toast.length) {
+ $('body').append('');
+ toast = $('#toast');
+ }
+ toast.text(text).fadeIn(200);
+ setTimeout(() => toast.fadeOut(500), 2000);
+}
+
+function showLoading() {
+ $('#schedule-list').html(' Загрузка расписания...
');
+}
+
+function hideLoading() {}
\ No newline at end of file
diff --git a/electric-train-tracker/frontend/static/styles/main.css b/electric-train-tracker/frontend/static/styles/main.css
new file mode 100644
index 00000000..6f3c6916
--- /dev/null
+++ b/electric-train-tracker/frontend/static/styles/main.css
@@ -0,0 +1,692 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', 'Poppins', -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ color: #2d3436;
+}
+
+.app-container {
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 16px;
+ min-height: 100vh;
+}
+
+/* Новый стиль хедера - градиент розово-оранжевый */
+.header {
+ text-align: center;
+ margin-bottom: 20px;
+ padding: 24px 20px;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ border-radius: 32px;
+ color: white;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
+ position: relative;
+ overflow: hidden;
+}
+
+.header::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
+ animation: rotate 20s linear infinite;
+}
+
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.header h1 {
+ font-size: 28px;
+ font-weight: 700;
+ letter-spacing: -0.5px;
+ position: relative;
+ z-index: 1;
+}
+
+.header h1 i {
+ margin-right: 10px;
+ animation: bounce 2s ease-in-out infinite;
+}
+
+@keyframes bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-5px); }
+}
+
+.subtitle {
+ font-size: 13px;
+ opacity: 0.9;
+ margin-top: 6px;
+ position: relative;
+ z-index: 1;
+}
+
+/* Новые вкладки - стеклянный эффект */
+.tabs {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 20px;
+ background: rgba(255,255,255,0.2);
+ backdrop-filter: blur(10px);
+ padding: 6px;
+ border-radius: 60px;
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
+}
+
+.tab-btn {
+ flex: 1;
+ padding: 12px;
+ border: none;
+ background: transparent;
+ border-radius: 50px;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ color: rgba(255,255,255,0.8);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ backdrop-filter: blur(5px);
+}
+
+.tab-btn.active {
+ background: white;
+ color: #f5576c;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ transform: scale(1.02);
+}
+
+.tab-btn:hover:not(.active) {
+ background: rgba(255,255,255,0.3);
+ color: white;
+}
+
+.tab-content {
+ display: none;
+ animation: slideUp 0.4s ease-out;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Карточки - новый дизайн */
+.card {
+ background: rgba(255,255,255,0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 28px;
+ padding: 24px;
+ margin-bottom: 20px;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.1);
+ border: 1px solid rgba(255,255,255,0.3);
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 40px rgba(0,0,0,0.15);
+}
+
+.card h3 {
+ font-size: 18px;
+ margin-bottom: 16px;
+ color: #f5576c;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.input-group {
+ margin-bottom: 18px;
+ position: relative;
+}
+
+.input-group label {
+ display: block;
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 8px;
+ color: #555;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.input-group label i {
+ margin-right: 6px;
+ color: #f5576c;
+}
+
+.input-group input {
+ width: 100%;
+ padding: 14px 16px;
+ border: 2px solid #e0e0e0;
+ border-radius: 20px;
+ font-size: 16px;
+ transition: all 0.3s;
+ background: white;
+}
+
+.input-group input:focus {
+ outline: none;
+ border-color: #f5576c;
+ box-shadow: 0 0 0 3px rgba(245,87,108,0.2);
+}
+
+/* Кнопки - градиентные */
+.btn-primary {
+ width: 100%;
+ padding: 14px;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ color: white;
+ border: none;
+ border-radius: 30px;
+ font-size: 16px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.3s;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px rgba(245,87,108,0.4);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+}
+
+/* Кнопки карты */
+.map-buttons {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 15px;
+}
+
+.map-btn {
+ flex: 1;
+ padding: 12px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ border-radius: 30px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ transition: all 0.3s;
+}
+
+.map-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 15px rgba(102,126,234,0.4);
+}
+
+/* Карта */
+.map-container {
+ width: 100%;
+ height: 400px;
+ border-radius: 24px;
+ overflow: hidden;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
+ border: 3px solid white;
+}
+
+/* Популярные станции - чипсы */
+.popular-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 10px;
+}
+
+.popular-item {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ padding: 8px 18px;
+ border-radius: 40px;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s;
+ color: white;
+ box-shadow: 0 2px 8px rgba(245,87,108,0.3);
+}
+
+.popular-item:hover {
+ transform: scale(1.05);
+ box-shadow: 0 4px 15px rgba(245,87,108,0.5);
+}
+
+/* Подсказки при поиске */
+.suggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: white;
+ border-radius: 20px;
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
+ z-index: 100;
+ max-height: 220px;
+ overflow-y: auto;
+ margin-top: 5px;
+}
+
+.suggestion-item {
+ padding: 14px 16px;
+ cursor: pointer;
+ border-bottom: 1px solid #f0f0f0;
+ transition: background 0.2s;
+}
+
+.suggestion-item:hover {
+ background: linear-gradient(135deg, #f093fb20 0%, #f5576c20 100%);
+}
+
+.suggestion-item .station-name {
+ font-weight: 600;
+ color: #333;
+}
+
+.suggestion-item .station-loc {
+ font-size: 11px;
+ color: #f5576c;
+ margin-top: 3px;
+}
+
+/* Результаты */
+.results-container {
+ background: rgba(255,255,255,0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 28px;
+ padding: 20px;
+ margin-top: 20px;
+ border: 1px solid rgba(255,255,255,0.3);
+}
+
+.results-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 2px solid #f0f0f0;
+}
+
+.results-header h3 {
+ color: #f5576c;
+ font-size: 16px;
+}
+
+#results-count {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ padding: 4px 12px;
+ border-radius: 30px;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+/* Карточки поездов */
+.train-card {
+ background: white;
+ border-radius: 20px;
+ padding: 16px;
+ margin-bottom: 12px;
+ transition: all 0.3s;
+ border: 1px solid #f0f0f0;
+ position: relative;
+ overflow: hidden;
+}
+
+.train-card::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.train-card:hover {
+ transform: translateX(5px);
+ box-shadow: 0 8px 25px rgba(0,0,0,0.1);
+}
+
+.train-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.train-number {
+ font-weight: 800;
+ font-size: 16px;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.train-route {
+ font-size: 13px;
+ color: #666;
+ margin-bottom: 12px;
+ font-weight: 500;
+}
+
+.train-time {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px dashed #e0e0e0;
+}
+
+.departure-info, .arrival-info {
+ flex: 1;
+}
+
+.time-label {
+ font-size: 11px;
+ color: #999;
+ display: block;
+}
+
+.time-value {
+ font-size: 20px;
+ font-weight: 800;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.train-days {
+ margin-top: 10px;
+ font-size: 11px;
+ color: #f5576c;
+ text-align: center;
+ font-weight: 500;
+}
+
+/* Кнопка избранного */
+.favorite-btn {
+ background: rgba(245,87,108,0.1);
+ border: none;
+ cursor: pointer;
+ color: #f5576c;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 14px;
+ border-radius: 30px;
+ font-size: 12px;
+ font-weight: 600;
+ transition: all 0.3s;
+}
+
+.favorite-btn.active {
+ background: #f5576c;
+ color: white;
+}
+
+.favorite-btn.active i {
+ color: white;
+}
+
+.favorite-btn:hover {
+ transform: scale(1.05);
+}
+
+/* Список станций рядом */
+.nearby-station-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px;
+ margin-bottom: 10px;
+ background: white;
+ border-radius: 20px;
+ cursor: pointer;
+ transition: all 0.3s;
+ border: 1px solid #f0f0f0;
+}
+
+.nearby-station-item:hover {
+ background: linear-gradient(135deg, #f093fb10 0%, #f5576c10 100%);
+ transform: translateX(5px);
+}
+
+.nearby-station-item i {
+ color: #f5576c;
+ font-size: 20px;
+ margin-right: 12px;
+}
+
+.nearby-station-item .station-info {
+ flex: 1;
+}
+
+.nearby-station-item .station-info strong {
+ display: block;
+ font-size: 14px;
+ color: #333;
+}
+
+.nearby-station-item .distance {
+ font-size: 11px;
+ color: #f5576c;
+ font-weight: 500;
+}
+
+.nearby-station-item .city {
+ font-size: 11px;
+ color: #999;
+}
+
+.select-station-btn {
+ padding: 6px 16px;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ color: white;
+ border: none;
+ border-radius: 30px;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 600;
+ transition: all 0.3s;
+}
+
+.select-station-btn:hover {
+ transform: scale(1.05);
+}
+
+/* Избранное */
+.favorite-card {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 14px 18px;
+ background: white;
+ border-radius: 20px;
+ margin-bottom: 10px;
+ cursor: pointer;
+ transition: all 0.3s;
+ border: 1px solid #f0f0f0;
+}
+
+.favorite-card:hover {
+ background: linear-gradient(135deg, #f093fb10 0%, #f5576c10 100%);
+ transform: translateX(5px);
+}
+
+.favorite-card i {
+ color: #f5576c;
+}
+
+.remove-fav {
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
+ color: white;
+ border: none;
+ padding: 6px 14px;
+ border-radius: 30px;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 600;
+ transition: all 0.3s;
+}
+
+.remove-fav:hover {
+ transform: scale(1.05);
+}
+
+/* Уведомления */
+#toast {
+ background: linear-gradient(135deg, #2d3436 0%, #1a1a2e 100%);
+ color: white;
+ padding: 14px 24px;
+ border-radius: 50px;
+ font-size: 14px;
+ font-weight: 500;
+ box-shadow: 0 8px 25px rgba(0,0,0,0.2);
+ border: 1px solid rgba(255,255,255,0.2);
+}
+
+/* Загрузчик */
+.loader i {
+ font-size: 32px;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* Пустые состояния */
+.empty-message {
+ text-align: center;
+ color: #999;
+ padding: 40px;
+ font-size: 14px;
+}
+
+/* Дата инпут */
+.date-input {
+ width: 100%;
+ padding: 14px 16px;
+ border: 2px solid #e0e0e0;
+ border-radius: 20px;
+ font-size: 16px;
+ transition: all 0.3s;
+ background: white;
+}
+
+.date-input:focus {
+ outline: none;
+ border-color: #f5576c;
+ box-shadow: 0 0 0 3px rgba(245,87,108,0.2);
+}
+
+/* Адаптив для мобильных */
+@media (max-width: 480px) {
+ .app-container {
+ padding: 12px;
+ }
+
+ .header h1 {
+ font-size: 22px;
+ }
+
+ .tab-btn {
+ padding: 10px;
+ font-size: 11px;
+ }
+
+ .card {
+ padding: 18px;
+ }
+
+ .train-time {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .arrival-info {
+ text-align: left;
+ }
+
+ .time-value {
+ font-size: 18px;
+ }
+
+ .nearby-station-item {
+ flex-wrap: wrap;
+ }
+
+ .select-station-btn {
+ margin-top: 8px;
+ width: 100%;
+ }
+}
+
+/* Скроллбар */
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ border-radius: 10px;
+}
+
+/* Анимация для появления */
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.loading-small {
+ text-align: center;
+ padding: 20px;
+ color: #f5576c;
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+.error-small {
+ text-align: center;
+ padding: 20px;
+ color: #ff6b6b;
+}
\ No newline at end of file