diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85381e2..8769a85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,3 +18,9 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
форматтером `Units.formatDistance()`; выбор сохраняется в localStorage
(ключ `distance_unit`), пересчёт всех видимых расстояний выполняется
единым оркестратором по событию `unitchange`
+- ET-006: загрузка и визуализация GPX-треков — новый модуль
+ `src/web/gpx.js` с клиентским парсингом GPX 1.1 (`DOMParser`,
+ чанковая конвертация), отрисовкой треков и waypoints на карте,
+ панелью `#sheet-gpx` со списком треков, статистикой и canvas-профилем
+ высот; GPX-слои восстанавливаются после смены стиля карты через
+ `rebuildMapOverlays()`. Данные треков хранятся только в памяти сессии
diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md
index ea0654c..51da402 100644
--- a/docs/architecture/adr/README.md
+++ b/docs/architecture/adr/README.md
@@ -2,5 +2,8 @@
Индекс ADR проекта enduro-trails.
-| # | Решение | Статус | Дата |
-|---|---------|--------|------|
+| # | Решение | Статус | Дата | Источник |
+|---|---------|--------|------|----------|
+| ADR-001 | Блокировка шлагбаумов через `mode.inaccessible` | accepted | 2026-05-15 | [ET-001](../../work-items/ET-001/06-adr/ADR-001-barrier-blocking.md) |
+| ADR-002 | GPX-фича как отдельный модуль `gpx.js` | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-002-gpx-module-structure.md) |
+| ADR-003 | Парсинг GPX — `DOMParser` в основном потоке с чанковой конвертацией | accepted | 2026-05-22 | [ET-006](../../work-items/ET-006/06-adr/ADR-003-gpx-parsing-strategy.md) |
diff --git a/docs/work-items/ET-006/00-business-request.md b/docs/work-items/ET-006/00-business-request.md
new file mode 100644
index 0000000..9f7f369
--- /dev/null
+++ b/docs/work-items/ET-006/00-business-request.md
@@ -0,0 +1,28 @@
+---
+type: business-request
+work_item_id: ET-006
+title: "Загрузка и визуализация GPX-треков на карте"
+created_at: 2026-05-22
+source: telegram
+requester: Слава
+---
+
+# Бизнес-запрос — ET-006
+
+## Исходная формулировка
+
+> Нужно добавить опцию загрузки GPX-треков. Я хочу нажать на кнопку в интерфейсе, выбрать файл с треком и далее увидеть его визуализацию на карте.
+
+## Уточнения (из диалога)
+
+1. Формат: GPX 1.1 стандартный. Возможны несколько треков в одном файле.
+2. Визуализация: линия трека + waypoints как маркеры + профиль высот + статистика (длина, набор/сброс высоты).
+3. Кнопка: отдельная в интерфейсе.
+4. После загрузки: карта центрируется на треке (fit bounds).
+5. Можно загрузить несколько треков одновременно.
+6. Можно удалить загруженный трек.
+7. Хранение: только на время сессии (в памяти браузера), без сохранения на сервер.
+8. Каждый трек — свой цвет (для различения).
+9. Лимит файла: до 50 МБ, без упрощения (simplify) точек.
+10. Waypoints из GPX: показывать как маркеры на карте с именами.
+11. UX профиля высот и статистики — на усмотрение аналитика.
diff --git a/docs/work-items/ET-006/01-brd.md b/docs/work-items/ET-006/01-brd.md
new file mode 100644
index 0000000..41f43c3
--- /dev/null
+++ b/docs/work-items/ET-006/01-brd.md
@@ -0,0 +1,84 @@
+---
+type: brd
+work_item_id: ET-006
+title: "BRD: Загрузка и визуализация GPX-треков"
+version: 2
+status: approved
+created_at: 2026-05-22
+updated_at: 2026-05-22
+authors:
+ - "agent:analyst"
+---
+
+# BRD — ET-006: Загрузка и визуализация GPX-треков
+
+## 1. Цель
+
+Дать пользователю возможность загрузить GPX-файл с треком и увидеть его на карте: линию маршрута, waypoints, профиль высот и статистику. Это позволяет визуально оценить чужой или ранее записанный трек перед поездкой.
+
+## 2. Контекст
+
+- Приложение уже умеет строить маршруты через OSRM и экспортировать их в GPX (кнопка «Скачать GPX» в sheet-route).
+- Обратная операция — импорт GPX — отсутствует.
+- Фаза PH-3 (Smart Route) в roadmap включает работу с GPX.
+- Фронтенд: MapLibre GL JS + vanilla JS, без фреймворков.
+- Backend-изменения не требуются — парсинг GPX происходит на клиенте.
+
+## 3. Scope
+
+### In scope
+
+| # | Функция |
+|---|---------|
+| F-01 | Кнопка загрузки GPX в тулбаре карты |
+| F-02 | Парсинг GPX 1.1 на клиенте (XML → GeoJSON) |
+| F-03 | Поддержка нескольких треков в одном файле |
+| F-04 | Отрисовка линии трека на карте (каждый трек — свой цвет) |
+| F-05 | Отображение waypoints из GPX как маркеров с именами |
+| F-06 | Fit bounds — карта центрируется на загруженном треке |
+| F-07 | Загрузка нескольких файлов (треки накапливаются) |
+| F-08 | Удаление отдельного трека |
+| F-09 | Панель управления треками (список, цвет, удаление) |
+| F-10 | Профиль высот выбранного трека |
+| F-11 | Статистика трека: длина, набор высоты, сброс высоты, мин/макс высота |
+| F-12 | Лимит размера файла: 50 МБ |
+| F-13 | Сохранение GPX-слоёв при переключении стиля карты (тёмная тема / рельеф) |
+
+### Out of scope
+
+- Сохранение треков на сервер / в БД
+- Редактирование трека (обрезка, склейка)
+- Конвертация из других форматов (KML, FIT, TCX)
+- Упрощение (simplify) точек трека
+- Экспорт загруженного трека обратно в GPX
+- Роутинг по загруженному треку (snap to road)
+
+## 4. Метрики успеха
+
+| Метрика | Критерий |
+|---------|----------|
+| Загрузка файла | Файл до 50 МБ загружается и парсится без ошибок за ≤ 3 сек (на среднем устройстве) |
+| Визуализация | Трек отображается на карте как цветная линия |
+| Waypoints | Маркеры с именами видны на карте |
+| Fit bounds | Карта автоматически подстраивает zoom/center под трек |
+| Множественные треки | 5+ треков отображаются одновременно, различимы по цвету |
+| Удаление | Удалённый трек исчезает с карты и из панели |
+| Профиль высот | Отображается корректный график высот для выбранного трека |
+| Статистика | Длина, набор/сброс высоты отображаются корректно |
+| Сохранение при смене стиля | GPX-треки остаются на карте после переключения тёмной темы / слоёв рельефа |
+| Не ломает существующий функционал | Роутинг, рельеф, POI, линейка работают как прежде |
+
+## 5. Риски
+
+| Риск | Вероятность | Влияние | Митигация |
+|------|-------------|---------|-----------|
+| Большой GPX (50 МБ, 500K+ точек) тормозит рендеринг | Средняя | Среднее | Использовать GeoJSON source + line layer (MapLibre оптимизирует); при необходимости — Web Worker для парсинга |
+| GPX без данных высот → профиль пустой | Средняя | Низкое | Показать сообщение «Данные высот отсутствуют» |
+| Невалидный GPX → ошибка парсинга | Низкая | Низкое | Показать пользователю понятное сообщение об ошибке |
+| Конфликт цветов треков с цветами маршрута OSRM | Низкая | Низкое | Использовать отдельную палитру, отличную от цветов роутинга |
+
+## 6. Зависимости
+
+- Нет внешних зависимостей
+- Только фронтенд (vanilla JS + MapLibre GL JS)
+- Парсинг XML: нативный DOMParser браузера
diff --git a/docs/work-items/ET-006/02-trz.md b/docs/work-items/ET-006/02-trz.md
new file mode 100644
index 0000000..44ba8e1
--- /dev/null
+++ b/docs/work-items/ET-006/02-trz.md
@@ -0,0 +1,289 @@
+---
+type: trz
+work_item_id: ET-006
+title: "ТЗ: Загрузка и визуализация GPX-треков"
+version: 2
+status: approved
+created_at: 2026-05-22
+updated_at: 2026-05-22
+authors:
+ - "agent:analyst"
+---
+
+# ТЗ — ET-006: Загрузка и визуализация GPX-треков
+
+## 1. Функциональные требования
+
+### REQ-F-01: Кнопка загрузки GPX
+
+- В правой панели кнопок карты (`#map-controls-r`) добавляется кнопка «GPX» с иконкой загрузки (стрелка вверх + документ).
+- Позиция: между кнопкой «Компас» и «Моё местоположение» (верхняя часть панели).
+- По нажатию открывается системный диалог выбора файла (``).
+- Допускается множественный выбор (`multiple`).
+
+### REQ-F-02: Парсинг GPX
+
+- Парсинг выполняется на клиенте через `DOMParser` (XML → DOM → GeoJSON).
+- Поддерживается GPX 1.1 (namespace `http://www.topografix.com/GPX/1/1`).
+- Извлекаются:
+ - `` → массив треков, каждый `` → массив точек `[lon, lat, ele?, time?]`
+ - `` → waypoints `{lon, lat, name?, ele?}`
+ - `` → route points (трактуются как трек)
+- Если файл содержит несколько ``, каждый трек — отдельная сущность.
+- При ошибке парсинга — показать toast-уведомление: «Не удалось прочитать GPX-файл».
+
+### REQ-F-03: Валидация
+
+- Максимальный размер файла: 50 МБ. При превышении — toast: «Файл слишком большой (макс. 50 МБ)».
+- Если файл не содержит ни одного трека и ни одного waypoint — toast: «GPX-файл не содержит данных».
+
+### REQ-F-04: Отрисовка трека на карте
+
+- Каждый трек отрисовывается как `line` layer в MapLibre.
+- Source: GeoJSON (`LineString` или `MultiLineString`).
+- Цвет: из палитры 8 цветов, циклически. Палитра отличается от цветов роутинга (синий/зелёный/оранжевый).
+ - Предлагаемая палитра: `#e6194b`, `#3cb44b`, `#ffe119`, `#4363d8`, `#f58231`, `#911eb4`, `#42d4f4`, `#f032e6`.
+- Толщина линии: 4px.
+- Opacity: 0.85.
+- Z-index: выше базовых слоёв, ниже маршрута OSRM (если активен).
+
+### REQ-F-05: Отображение waypoints
+
+- Каждый `` отображается как маркер (circle layer + symbol layer для имени).
+- Цвет маркера: совпадает с цветом трека из того же файла (или нейтральный, если waypoints без трека).
+- Имя waypoint (``) отображается как label рядом с маркером.
+- Если имя отсутствует — маркер без подписи.
+
+### REQ-F-06: Fit bounds
+
+- После загрузки файла карта выполняет `fitBounds` по bbox всех точек загруженного файла.
+- Padding: 50px со всех сторон.
+- Если загружено несколько файлов подряд — fit bounds только по последнему загруженному.
+
+### REQ-F-07: Множественная загрузка
+
+- Треки из разных файлов накапливаются в сессии.
+- Каждый файл получает следующий цвет из палитры.
+- Максимальное количество одновременных треков: не ограничено (разумный предел — производительность браузера).
+
+### REQ-F-08: Удаление трека
+
+- В панели управления треками (REQ-F-09) у каждого трека есть кнопка удаления (иконка ✕).
+- При удалении: убирается line layer, source, маркеры waypoints с карты.
+- Если удалён активный (выбранный) трек — панель профиля высот скрывается.
+
+### REQ-F-09: Панель управления треками (GPX Sheet)
+
+- Реализуется как bottom sheet (`#sheet-gpx`), аналогично существующим sheet-route, sheet-recon.
+- Открывается автоматически при загрузке первого трека.
+- Содержит:
+ - Заголовок «GPX-треки» с иконкой и кнопкой свернуть.
+ - Список загруженных треков: цветной кружок + имя файла (без расширения) + кнопка удаления.
+ - По тапу на трек в списке — он становится «активным» (выделяется), показывается его статистика и профиль высот.
+- Кнопка в тулбаре нижнего toolbar (`#toolbar`): «GPX» — переключает видимость sheet.
+
+### REQ-F-10: Профиль высот
+
+- Отображается в нижней части sheet-gpx (под списком треков) для активного трека.
+- График: canvas-элемент, ширина 100% sheet, высота 120px.
+- Ось X: расстояние от начала трека (км).
+- Ось Y: высота (м).
+- Линия графика: цвет трека.
+- Заливка под линией: цвет трека с opacity 0.2.
+- Если данные высот отсутствуют (`` нет) — показать текст: «Данные высот отсутствуют».
+- При наведении/тапе на график — показать tooltip с высотой и расстоянием, и подсветить соответствующую точку на карте (маркер-курсор).
+
+### REQ-F-11: Статистика трека
+
+- Отображается над профилем высот в sheet-gpx для активного трека.
+- Формат: компактная сетка (аналогично recon-grid).
+- Поля:
+ - Длина (км) — сумма расстояний между точками (Haversine).
+ - Набор высоты (м) — сумма положительных дельт `ele`.
+ - Сброс высоты (м) — сумма отрицательных дельт `ele` (абсолютное значение).
+ - Мин. высота (м).
+ - Макс. высота (м).
+- Если данные высот отсутствуют — показать только длину, остальные поля: «—».
+
+### REQ-F-12: Интерактивность трека на карте
+
+- При клике на линию трека на карте — этот трек становится активным в панели (показывается статистика + профиль).
+- Курсор при наведении на трек: pointer.
+
+### REQ-F-13: Сохранение треков при переключении стиля карты
+
+- При переключении стиля карты (тёмная тема, восстановление слоёв рельефа) вызывается `map.setStyle()`, который удаляет **все** пользовательские source и layer.
+- После смены стиля все загруженные GPX-треки должны быть автоматически восстановлены: линии треков, source, waypoints-маркеры.
+- Восстановление выполняется в функции `rebuildMapOverlays()` (`src/web/app.js`) — по аналогии с уже реализованными там маршрутом OSRM, разведкой и scenic-маршрутами.
+- Данные треков (`window.gpxTracks`) хранятся в памяти и при `setStyle()` не теряются — пересоздаются только объекты карты (source / layer / маркеры).
+- Активный трек, его статистика и профиль высот должны сохраняться после переключения стиля.
+- Z-order GPX-слоёв (см. REQ-F-04) корректно восстанавливается и после смены стиля.
+
+## 2. Нефункциональные требования
+
+### REQ-NF-01: Производительность
+
+- Парсинг файла 50 МБ: ≤ 5 секунд на устройстве с 4 ГБ RAM.
+- Рендеринг трека 500K точек: без видимых фризов при pan/zoom (MapLibre оптимизирует GeoJSON line layers).
+- Во время парсинга показывать индикатор загрузки (spinner или moto-wheel).
+
+### REQ-NF-02: Совместимость
+
+- Работает в Chrome 90+, Firefox 90+, Safari 15+.
+- Работает на мобильных (touch events для профиля высот).
+
+### REQ-NF-03: UX
+
+- Кнопка загрузки доступна всегда, независимо от активного режима (роутинг, разведка и т.д.).
+- GPX-треки не конфликтуют с активным маршрутом OSRM — отображаются одновременно.
+- При ошибках — toast-уведомления (не alert/confirm).
+
+### REQ-NF-04: Хранение
+
+- Данные треков хранятся только в памяти (JS-переменные).
+- При перезагрузке страницы — все треки теряются.
+- Не используется localStorage/sessionStorage для данных треков (слишком большие).
+
+## 3. UI-спецификация
+
+### 3.1 Кнопка в правой панели (#map-controls-r)
+
+```
+┌──────────┐
+│ ↑ GPX │ ← новая кнопка (между Компас и Геолокация)
+└──────────┘
+```
+
+- Класс: `map-btn`
+- ID: `btn-gpx-upload`
+- Иконка: стрелка вверх из документа (upload file)
+- Title: «Загрузить GPX»
+
+### 3.2 Кнопка в нижнем тулбаре (#toolbar)
+
+```
+[ Маршрут | Связка | Красивый | Разведка | Линейка | Поиск | Метка | GPX ]
+```
+
+- Класс: `tb-btn`
+- ID: `tb-gpx`
+- Иконка: файл с линией (track)
+- Label: «GPX»
+- Действие: `toggleGpxSheet()`
+
+### 3.3 Bottom sheet (#sheet-gpx)
+
+```
+┌─────────────────────────────────────┐
+│ ═══ (handle) │
+│ 📄 GPX-треки [свернуть]│
+├─────────────────────────────────────┤
+│ 🔴 track_morning.gpx [✕] │
+│ 🔵 weekend_ride.gpx ✓ [✕] │ ← активный (выделен)
+│ 🟢 test_route.gpx [✕] │
+├─────────────────────────────────────┤
+│ СТАТИСТИКА │
+│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
+│ │47км│ │820м│ │650м│ │120м│ │980м│ │
+│ │длин│ │наб.│ │сбр.│ │мин │ │макс│ │
+│ └────┘ └────┘ └────┘ └────┘ └────┘ │
+├─────────────────────────────────────┤
+│ ПРОФИЛЬ ВЫСОТ │
+│ ┌───────────────────────────────┐ │
+│ │ ╱╲ ╱╲╱╲ │ │
+│ │╱ ╲╱╲╱ ╲╱╲ │ │
+│ └───────────────────────────────┘ │
+│ 0 km 23.5 km 47 km │
+└─────────────────────────────────────┘
+```
+
+### 3.4 Toast-уведомления
+
+- Позиция: верх экрана, по центру.
+- Автоскрытие: 4 секунды.
+- Стиль: аналогично `#ruler-toast`.
+
+## 4. Данные
+
+### Входные данные (GPX 1.1)
+
+```xml
+
+
+
+ Morning Ride
+
+ 150
+ ...
+
+
+
+ Кафе
+ 155
+
+
+```
+
+### Внутренняя модель (JS)
+
+```javascript
+// Массив загруженных GPX-файлов
+window.gpxTracks = [
+ {
+ id: 'gpx-1716336000000', // уникальный ID (timestamp)
+ filename: 'morning_ride', // имя файла без расширения
+ color: '#e6194b', // цвет из палитры
+ tracks: [ // массив треков из файла
+ {
+ name: 'Morning Ride',
+ points: [[lon, lat, ele, time], ...], // массив точек
+ stats: { distanceKm, elevGain, elevLoss, eleMin, eleMax }
+ }
+ ],
+ waypoints: [
+ { lon, lat, name, ele }
+ ],
+ sourceId: 'gpx-source-1716336000000',
+ layerId: 'gpx-layer-1716336000000',
+ waypointLayerId: 'gpx-wpt-1716336000000'
+ }
+];
+```
+
+## 5. Алгоритмы
+
+### 5.1 Расчёт расстояния (Haversine)
+
+Сумма расстояний между последовательными точками трека. Формула Haversine для каждой пары.
+
+### 5.2 Расчёт набора/сброса высоты
+
+```
+elevGain = Σ max(0, ele[i+1] - ele[i]) для всех i
+elevLoss = Σ max(0, ele[i] - ele[i+1]) для всех i
+```
+
+Фильтрация шума: игнорировать дельты < 2 м (GPS-шум).
+
+### 5.3 Палитра цветов
+
+Циклический массив из 8 цветов. Индекс = `gpxTracks.length % 8` на момент добавления.
+
+## 6. Файловая структура изменений
+
+```
+src/web/
+├── index.html # + кнопка в #map-controls-r, + sheet-gpx, + tb-btn
+├── app.js # + gpx-модуль (парсинг, рендеринг, управление)
+├── app.css # + стили sheet-gpx, профиля высот, toast
+```
+
+Альтернативно, GPX-логику можно вынести в отдельный файл `gpx.js` (аналогично `units.js`).
+
+## 7. Взаимодействие с существующими режимами
+
+- GPX-треки отображаются **параллельно** с любым активным режимом (роутинг, разведка, красивый маршрут).
+- Z-order: GPX-треки ниже активного маршрута OSRM, но выше базовых слоёв (trails, terrain).
+- Кнопка загрузки в `#map-controls-r` доступна всегда.
+- Кнопка «GPX» в toolbar переключает sheet, но не деактивирует другие режимы.
+- При смене стиля карты (`setStyle` — тёмная тема, слои рельефа) GPX-слои восстанавливаются через `rebuildMapOverlays()` — см. REQ-F-13.
diff --git a/docs/work-items/ET-006/03-acceptance-criteria.md b/docs/work-items/ET-006/03-acceptance-criteria.md
new file mode 100644
index 0000000..9a729a1
--- /dev/null
+++ b/docs/work-items/ET-006/03-acceptance-criteria.md
@@ -0,0 +1,254 @@
+---
+type: acceptance-criteria
+work_item_id: ET-006
+title: "AC: Загрузка и визуализация GPX-треков"
+version: 2
+status: approved
+created_at: 2026-05-22
+updated_at: 2026-05-22
+authors:
+ - "agent:analyst"
+---
+
+# Acceptance Criteria — ET-006: Загрузка и визуализация GPX-треков
+
+## AC-01: Загрузка файла через кнопку
+
+```gherkin
+Feature: Загрузка GPX-файла
+
+ Scenario: Успешная загрузка одного файла
+ Given пользователь находится на карте
+ When пользователь нажимает кнопку «Загрузить GPX» в правой панели
+ And выбирает валидный GPX-файл размером < 50 МБ
+ Then файл парсится без ошибок
+ And трек отображается на карте цветной линией
+ And карта выполняет fit bounds по треку
+ And в панели GPX-треков появляется запись с именем файла
+
+ Scenario: Файл превышает лимит
+ Given пользователь находится на карте
+ When пользователь выбирает GPX-файл размером > 50 МБ
+ Then показывается toast «Файл слишком большой (макс. 50 МБ)»
+ And трек не загружается
+
+ Scenario: Невалидный файл
+ Given пользователь находится на карте
+ When пользователь выбирает файл с невалидным XML
+ Then показывается toast «Не удалось прочитать GPX-файл»
+ And трек не загружается
+
+ Scenario: Пустой GPX (без треков и waypoints)
+ Given пользователь находится на карте
+ When пользователь выбирает GPX-файл без и без
+ Then показывается toast «GPX-файл не содержит данных»
+```
+
+## AC-02: Визуализация трека
+
+```gherkin
+Feature: Отрисовка трека на карте
+
+ Scenario: Один трек в файле
+ Given загружен GPX-файл с одним треком
+ Then на карте отображается линия трека
+ And линия имеет цвет из палитры
+ And толщина линии 4px
+
+ Scenario: Несколько треков в одном файле
+ Given загружен GPX-файл с 3 треками
+ Then на карте отображаются 3 линии
+ And все линии одного цвета (цвет файла)
+ And в панели — одна запись (имя файла) с 3 треками внутри
+
+ Scenario: Несколько файлов
+ Given загружены 3 GPX-файла
+ Then на карте отображаются треки из всех файлов
+ And каждый файл имеет свой цвет из палитры
+ And в панели — 3 записи
+```
+
+## AC-03: Waypoints
+
+```gherkin
+Feature: Отображение waypoints
+
+ Scenario: Waypoints с именами
+ Given загружен GPX-файл с waypoints, у которых есть
+ Then на карте отображаются маркеры в позициях waypoints
+ And рядом с каждым маркером отображается имя
+
+ Scenario: Waypoints без имён
+ Given загружен GPX-файл с waypoints без
+ Then на карте отображаются маркеры без подписей
+
+ Scenario: Файл без waypoints
+ Given загружен GPX-файл без
+ Then маркеры waypoints не отображаются
+ And трек отображается нормально
+```
+
+## AC-04: Fit bounds
+
+```gherkin
+Feature: Автоцентрирование карты
+
+ Scenario: Карта центрируется на загруженном треке
+ Given карта показывает произвольную область
+ When пользователь загружает GPX-файл
+ Then карта выполняет fit bounds по всем точкам файла
+ And padding составляет 50px со всех сторон
+
+ Scenario: Загрузка второго файла
+ Given на карте уже есть загруженный трек
+ When пользователь загружает второй GPX-файл
+ Then карта выполняет fit bounds только по второму файлу
+```
+
+## AC-05: Удаление трека
+
+```gherkin
+Feature: Удаление загруженного трека
+
+ Scenario: Удаление трека из панели
+ Given загружены 2 GPX-файла
+ When пользователь нажимает кнопку удаления (✕) у первого трека
+ Then первый трек исчезает с карты (линия + waypoints)
+ And первый трек исчезает из панели
+ And второй трек остаётся на карте
+
+ Scenario: Удаление активного трека
+ Given загружены 2 GPX-файла, второй — активный (показана статистика)
+ When пользователь удаляет второй трек
+ Then профиль высот и статистика скрываются
+ And первый трек остаётся на карте
+ And первый трек не становится автоматически активным
+
+ Scenario: Удаление последнего трека
+ Given загружен 1 GPX-файл
+ When пользователь удаляет его
+ Then карта пуста (нет GPX-слоёв)
+ And панель GPX показывает пустое состояние
+```
+
+## AC-06: Панель управления (sheet-gpx)
+
+```gherkin
+Feature: Панель GPX-треков
+
+ Scenario: Открытие панели при загрузке
+ Given панель GPX закрыта
+ When пользователь загружает первый GPX-файл
+ Then панель GPX открывается автоматически
+
+ Scenario: Переключение через toolbar
+ Given панель GPX закрыта
+ When пользователь нажимает кнопку «GPX» в нижнем тулбаре
+ Then панель GPX открывается
+
+ Scenario: Выбор активного трека
+ Given загружены 3 GPX-файла
+ When пользователь тапает на второй трек в списке
+ Then второй трек выделяется визуально
+ And показывается его статистика и профиль высот
+```
+
+## AC-07: Профиль высот
+
+```gherkin
+Feature: Профиль высот
+
+ Scenario: Трек с данными высот
+ Given выбран активный трек с данными
+ Then в панели отображается график профиля высот
+ And ось X — расстояние (км)
+ And ось Y — высота (м)
+ And линия графика — цвет трека
+
+ Scenario: Трек без данных высот
+ Given выбран активный трек без данных
+ Then вместо графика отображается текст «Данные высот отсутствуют»
+
+ Scenario: Интерактивность профиля
+ Given отображается профиль высот
+ When пользователь наводит курсор (или тапает) на точку графика
+ Then показывается tooltip с высотой и расстоянием
+ And на карте подсвечивается соответствующая точка трека
+```
+
+## AC-08: Статистика трека
+
+```gherkin
+Feature: Статистика трека
+
+ Scenario: Полная статистика (с высотами)
+ Given выбран активный трек с данными
+ Then отображаются: длина (км), набор высоты (м), сброс высоты (м), мин. высота (м), макс. высота (м)
+
+ Scenario: Частичная статистика (без высот)
+ Given выбран активный трек без данных
+ Then отображается только длина (км)
+ And остальные поля показывают «—»
+```
+
+## AC-09: Интерактивность на карте
+
+```gherkin
+Feature: Клик по треку на карте
+
+ Scenario: Выбор трека кликом
+ Given на карте отображаются 3 трека из разных файлов
+ When пользователь кликает на линию второго трека
+ Then второй трек становится активным в панели
+ And показывается его статистика и профиль высот
+```
+
+## AC-10: Совместимость с другими режимами
+
+```gherkin
+Feature: Параллельная работа с роутингом
+
+ Scenario: GPX + активный маршрут
+ Given пользователь построил маршрут через OSRM
+ And загрузил GPX-файл
+ Then оба отображаются на карте одновременно
+ And маршрут OSRM визуально выше GPX-трека
+ And оба интерактивны
+
+ Scenario: GPX + режим разведки
+ Given пользователь в режиме разведки
+ When загружает GPX-файл
+ Then трек отображается на карте
+ And режим разведки продолжает работать
+```
+
+## AC-12: Сохранение при переключении стиля карты
+
+```gherkin
+Feature: Сохранение GPX-треков при смене стиля карты
+
+ Scenario: Переключение тёмной темы
+ Given загружен GPX-трек и отображается на карте
+ When пользователь переключает тёмную тему
+ Then трек остаётся на карте после смены стиля
+ And waypoints остаются на карте
+ And активный трек, его статистика и профиль высот сохраняются
+
+ Scenario: Переключение слоёв рельефа
+ Given загружены 2 GPX-трека
+ When пользователь включает или выключает слой рельефа (hillshade / TRI)
+ Then оба трека остаются на карте с прежними цветами
+ And z-order GPX-слоёв сохраняется (ниже маршрута OSRM)
+```
+
+## AC-11: Индикатор загрузки
+
+```gherkin
+Feature: Индикатор при парсинге
+
+ Scenario: Большой файл
+ Given пользователь выбирает GPX-файл > 10 МБ
+ Then показывается индикатор загрузки (spinner)
+ And после завершения парсинга индикатор скрывается
+ And трек отображается на карте
+```
diff --git a/docs/work-items/ET-006/04-test-plan.yaml b/docs/work-items/ET-006/04-test-plan.yaml
new file mode 100644
index 0000000..b2d73c7
--- /dev/null
+++ b/docs/work-items/ET-006/04-test-plan.yaml
@@ -0,0 +1,254 @@
+---
+type: test-plan
+work_item_id: ET-006
+title: "Test Plan: Загрузка и визуализация GPX-треков"
+version: 2
+status: approved
+created_at: 2026-05-22
+updated_at: 2026-05-22
+authors:
+ - "agent:analyst"
+
+test_suites:
+
+ - name: unit-gpx-parser
+ type: unit
+ description: "Парсинг GPX XML → внутренняя модель"
+ cases:
+ - id: U-01
+ name: "Парсинг валидного GPX 1.1 с одним треком"
+ input: "GPX-файл с 1 trk, 1 trkseg, 10 trkpt (lat, lon, ele, time)"
+ expected: "Возвращает объект с 1 треком, 10 точками, корректными координатами и высотами"
+
+ - id: U-02
+ name: "Парсинг GPX с несколькими треками"
+ input: "GPX-файл с 3 trk"
+ expected: "Возвращает массив из 3 треков"
+
+ - id: U-03
+ name: "Парсинг waypoints"
+ input: "GPX-файл с 5 wpt (lat, lon, name, ele)"
+ expected: "Возвращает массив из 5 waypoints с именами и координатами"
+
+ - id: U-04
+ name: "Парсинг route (rte)"
+ input: "GPX-файл с 1 rte, 20 rtept"
+ expected: "Возвращает как трек с 20 точками"
+
+ - id: U-05
+ name: "GPX без данных высот"
+ input: "GPX-файл с trkpt без "
+ expected: "Точки имеют ele=null, stats.elevGain=null"
+
+ - id: U-06
+ name: "Невалидный XML"
+ input: "Файл с битым XML"
+ expected: "Выбрасывает ошибку с сообщением"
+
+ - id: U-07
+ name: "Пустой GPX (нет trk, wpt, rte)"
+ input: "Валидный XML, но без данных"
+ expected: "Выбрасывает ошибку 'no data'"
+
+ - id: U-08
+ name: "GPX с namespace и без"
+ input: "GPX без xmlns атрибута"
+ expected: "Парсится корректно (fallback без namespace)"
+
+ - name: unit-gpx-stats
+ type: unit
+ description: "Расчёт статистики трека"
+ cases:
+ - id: U-10
+ name: "Расчёт длины (Haversine)"
+ input: "Трек из 3 точек: [37.6, 55.7], [37.7, 55.8], [37.8, 55.9]"
+ expected: "Длина ≈ 28.3 км (±0.5 км)"
+
+ - id: U-11
+ name: "Расчёт набора высоты"
+ input: "Точки с ele: [100, 150, 120, 200, 180]"
+ expected: "elevGain = 130 м (50 + 80), elevLoss = 70 м (30 + 20)"
+
+ - id: U-12
+ name: "Фильтрация шума высот (дельта < 2м)"
+ input: "Точки с ele: [100, 101, 100, 101, 150]"
+ expected: "elevGain = 50 м (только 100→150), мелкие колебания игнорируются"
+
+ - id: U-13
+ name: "Мин/макс высота"
+ input: "Точки с ele: [100, 250, 80, 300, 150]"
+ expected: "eleMin=80, eleMax=300"
+
+ - id: U-14
+ name: "Статистика без данных высот"
+ input: "Точки без ele"
+ expected: "distanceKm рассчитан, elevGain/elevLoss/eleMin/eleMax = null"
+
+ - name: unit-gpx-colors
+ type: unit
+ description: "Назначение цветов из палитры"
+ cases:
+ - id: U-20
+ name: "Первый файл получает первый цвет"
+ input: "Загрузка первого файла"
+ expected: "Цвет = #e6194b"
+
+ - id: U-21
+ name: "Девятый файл получает первый цвет (цикл)"
+ input: "Загрузка 9-го файла"
+ expected: "Цвет = #e6194b (индекс 8 % 8 = 0)"
+
+ - name: integration-gpx-map
+ type: integration
+ description: "Интеграция GPX с MapLibre"
+ cases:
+ - id: I-01
+ name: "Добавление source и layer при загрузке"
+ input: "Загрузка валидного GPX"
+ expected: "map.getSource(sourceId) !== null, map.getLayer(layerId) !== null"
+
+ - id: I-02
+ name: "Удаление source и layer при удалении трека"
+ input: "Удаление загруженного трека"
+ expected: "map.getSource(sourceId) === null, map.getLayer(layerId) === null"
+
+ - id: I-03
+ name: "Fit bounds после загрузки"
+ input: "Загрузка GPX с bbox [37.5, 55.6, 37.9, 55.9]"
+ expected: "map.getBounds() содержит указанный bbox"
+
+ - id: I-04
+ name: "Waypoints как маркеры"
+ input: "GPX с 3 waypoints"
+ expected: "На карте 3 маркера с подписями"
+
+ - id: I-05
+ name: "Клик по треку активирует его"
+ input: "Клик на линию трека"
+ expected: "Трек становится активным, показывается статистика"
+
+ - id: I-06
+ name: "GPX-слои ниже маршрута OSRM"
+ input: "Загружен GPX + построен маршрут"
+ expected: "Layer order: gpx-layer before route-layer"
+
+ - id: I-07
+ name: "Треки сохраняются после setStyle (переключение стиля карты)"
+ input: "Загружен GPX-трек (линия + waypoints), вызывается switchMapStyle() / map.setStyle()"
+ expected: "После события idle: map.getLayer(layerId) !== null, map.getSource(sourceId) !== null, waypoint-маркеры присутствуют, активный трек и его статистика/профиль сохранены"
+
+ - name: integration-gpx-elevation
+ type: integration
+ description: "Профиль высот"
+ cases:
+ - id: I-10
+ name: "Рендеринг canvas профиля"
+ input: "Активный трек с 100 точками и ele"
+ expected: "Canvas отрисован, ширина = ширина контейнера, высота = 120px"
+
+ - id: I-11
+ name: "Tooltip при наведении"
+ input: "Mousemove по canvas на позиции 50%"
+ expected: "Tooltip показывает высоту и расстояние средней точки"
+
+ - id: I-12
+ name: "Маркер-курсор на карте при наведении на профиль"
+ input: "Mousemove по canvas"
+ expected: "На карте появляется маркер в соответствующей точке трека"
+
+ - name: e2e-gpx-workflow
+ type: e2e
+ description: "Полный пользовательский сценарий"
+ cases:
+ - id: E-01
+ name: "Загрузка → визуализация → статистика → удаление"
+ steps:
+ - "Открыть приложение"
+ - "Нажать кнопку GPX в правой панели"
+ - "Выбрать файл test-track.gpx (1 трек, 500 точек, с ele)"
+ - "Убедиться: трек на карте, панель открыта, статистика показана"
+ - "Проверить профиль высот"
+ - "Удалить трек"
+ - "Убедиться: карта пуста, панель пуста"
+
+ - id: E-02
+ name: "Множественная загрузка и различение цветов"
+ steps:
+ - "Загрузить 3 GPX-файла последовательно"
+ - "Убедиться: 3 трека на карте разных цветов"
+ - "Убедиться: 3 записи в панели"
+ - "Кликнуть на второй трек в панели"
+ - "Убедиться: показана статистика второго трека"
+
+ - id: E-03
+ name: "Большой файл (50 МБ)"
+ steps:
+ - "Загрузить GPX-файл ~50 МБ"
+ - "Убедиться: показан индикатор загрузки"
+ - "Убедиться: трек отображается после парсинга"
+ - "Убедиться: pan/zoom работают без фризов"
+
+ - id: E-04
+ name: "Файл с waypoints"
+ steps:
+ - "Загрузить GPX с 5 waypoints"
+ - "Убедиться: 5 маркеров на карте с подписями"
+ - "Удалить трек"
+ - "Убедиться: маркеры исчезли"
+
+ - id: E-05
+ name: "GPX параллельно с роутингом"
+ steps:
+ - "Построить маршрут через OSRM"
+ - "Загрузить GPX-файл"
+ - "Убедиться: оба отображаются, маршрут выше GPX"
+ - "Удалить GPX"
+ - "Убедиться: маршрут не затронут"
+
+ - id: E-06
+ name: "Ошибки: невалидный файл, превышение лимита"
+ steps:
+ - "Попытаться загрузить .txt файл переименованный в .gpx"
+ - "Убедиться: toast с ошибкой"
+ - "Попытаться загрузить файл > 50 МБ"
+ - "Убедиться: toast с ошибкой"
+ - "Убедиться: предыдущие треки не затронуты"
+
+ - id: E-07
+ name: "Мобильное устройство (touch)"
+ steps:
+ - "Открыть на мобильном (или эмуляция)"
+ - "Загрузить GPX"
+ - "Тапнуть на трек в панели"
+ - "Тапнуть на профиль высот"
+ - "Убедиться: tooltip и маркер-курсор работают"
+
+ - name: e2e-gpx-toolbar
+ type: e2e
+ description: "Кнопка GPX в toolbar"
+ cases:
+ - id: E-10
+ name: "Переключение панели через toolbar"
+ steps:
+ - "Нажать кнопку GPX в нижнем тулбаре"
+ - "Убедиться: панель GPX открылась"
+ - "Нажать ещё раз"
+ - "Убедиться: панель свернулась"
+
+test_data:
+ - name: "test-track-simple.gpx"
+ description: "1 трек, 10 точек, с ele и time"
+ - name: "test-track-multi.gpx"
+ description: "3 трека в одном файле"
+ - name: "test-track-waypoints.gpx"
+ description: "1 трек + 5 waypoints с именами"
+ - name: "test-track-no-ele.gpx"
+ description: "1 трек без данных высот"
+ - name: "test-track-large.gpx"
+ description: "~50 МБ, 500K+ точек"
+ - name: "test-track-invalid.gpx"
+ description: "Битый XML"
+ - name: "test-track-empty.gpx"
+ description: "Валидный GPX без trk/wpt/rte"
+ - name: "test-track-route.gpx"
+ description: "GPX с вместо "
diff --git a/docs/work-items/ET-006/06-adr/ADR-002-gpx-module-structure.md b/docs/work-items/ET-006/06-adr/ADR-002-gpx-module-structure.md
new file mode 100644
index 0000000..c97371c
--- /dev/null
+++ b/docs/work-items/ET-006/06-adr/ADR-002-gpx-module-structure.md
@@ -0,0 +1,143 @@
+---
+type: adr
+work_item_id: ET-006
+adr_id: ADR-002
+title: "GPX-фича как отдельный модуль gpx.js"
+status: accepted
+date: 2026-05-22
+authors:
+ - "agent:architect"
+supersedes: null
+superseded_by: null
+---
+
+# ADR-002: GPX-фича как отдельный модуль `gpx.js`
+
+## Контекст
+
+ET-006 добавляет самодостаточную фичу: парсинг GPX, внутренняя модель,
+управление source/layer/маркерами карты, bottom sheet `sheet-gpx`,
+canvas-профиль высот, расчёт статистики. Оценка объёма — ~600–900 строк JS.
+
+ТЗ (`02-trz.md` §6) оставляет структуру файлов открытой и предлагает два
+варианта: дописать в `app.js` либо вынести в отдельный `gpx.js`. ТЗ ссылается
+на `units.js` как на прецедент.
+
+Фактическое состояние кодовой базы (проверено):
+
+- `src/web/units.js` **не существует**. `app.js` — единственный JS-файл
+ фронтенда: 113 КБ, ~2900 строк.
+- `app.js` подключён как **классический скрипт** (`` после строки 400
+ (`
+
+