diff --git a/docs/architecture/README.md b/docs/architecture/README.md
index aabaab8..893e5dd 100644
--- a/docs/architecture/README.md
+++ b/docs/architecture/README.md
@@ -11,12 +11,25 @@
- **Database** — SQLite + Spatialite (точки интереса, маршруты)
## Слои карты
-- Base map (OpenStreetMap)
+- Base map: **Схема** (OpenStreetMap raster) либо **Спутник** (Esri World Imagery raster) — переключается в UI (ET-007 / ADR-004)
- Hillshade (рельеф с тенями)
- TRI (Terrain Ruggedness Index — сложность рельефа)
- Hypsometric (высотная раскраска)
- Trails (маршруты из OSM)
+## Внешние тайл-провайдеры
+
+Клиент (браузер) обращается напрямую к двум внешним raster-tile сервисам.
+Сервер mva154 эти тайлы не проксирует и не кэширует.
+
+| Провайдер | Назначение | URL | Активация | API-ключ |
+|-----------|-----------|-----|-----------|----------|
+| OpenStreetMap | Базовый слой «Схема» | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` | всегда (default подложка) | нет |
+| Esri World Imagery | Базовый слой «Спутник» | `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}` | лениво — только при включении «Спутник» пользователем (ET-007) | нет |
+
+Атрибуция обоих провайдеров выводится MapLibre автоматически при
+активном source.
+
## Деплой
Один Docker Compose на mva154. Nginx проксирует /enduro/ на контейнер.
diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md
index 51da402..c91bad5 100644
--- a/docs/architecture/adr/README.md
+++ b/docs/architecture/adr/README.md
@@ -7,3 +7,4 @@
| 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) |
+| ADR-004 | Спутниковая подложка: Esri World Imagery, ленивый raster-source, гибридное halo | accepted | 2026-05-31 | [ET-007](../../work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md) |
diff --git a/docs/work-items/ET-007/00-business-request.md b/docs/work-items/ET-007/00-business-request.md
new file mode 100644
index 0000000..545e408
--- /dev/null
+++ b/docs/work-items/ET-007/00-business-request.md
@@ -0,0 +1,7 @@
+# Business Request: ET-005: Спутниковая карта (Схема/Спутник)
+
+Work Item ID: ET-007
+
+## Description
+
+TBD
diff --git a/docs/work-items/ET-007/01-brd.md b/docs/work-items/ET-007/01-brd.md
new file mode 100644
index 0000000..aea4b29
--- /dev/null
+++ b/docs/work-items/ET-007/01-brd.md
@@ -0,0 +1,100 @@
+---
+type: brd
+work_item_id: ET-007
+title: "BRD: Спутниковая карта (Схема / Спутник)"
+version: 2
+status: draft
+created_at: 2026-05-31
+updated_at: 2026-05-31
+changelog:
+ - "v2 (2026-05-31): code-review fix (12-review.md P1-3) — митигация риска hillshade приведена в соответствие с TRZ/ADR/AC: авто-выключение не вводится."
+authors:
+ - "agent:analyst"
+---
+
+# BRD — ET-007: Спутниковая карта (Схема / Спутник)
+
+## 1. Цель
+
+Дать пользователю возможность одним кликом переключать подложку карты
+между «Схемой» (текущая OSM-схема) и «Спутник» (растровые снимки
+поверхности Земли). Спутниковая подложка помогает увидеть реальный
+рельеф и поверхность маршрута — лес/поле/брод/каменистый участок — до
+выезда.
+
+## 2. Контекст
+
+- Сейчас в приложении используется единственная подложка — OSM-растр,
+ стилизованный для «Схемы» в двух темах (`style.json`,
+ `style-dark.json`). Спутникового слоя нет.
+- В фазе PH-5 Redesign уже была введена тёмная/светлая тема — но
+ «тема» относится к стилизации (контрасты, насыщенность), а не к
+ природе подложки.
+- Эндуро-маршруты часто проходят вне дорог OSM (бездорожье, броды,
+ лесные участки). Спутник критичен для разведки.
+- Все клиентские модули (`app.js`, `units.js`, `gpx.js`) уже умеют
+ переживать `map.setStyle()` через `rebuildMapOverlays()` — это
+ опорная точка для будущей реализации.
+
+## 3. Scope
+
+### In scope
+
+| # | Функция |
+| ----- | ------------------------------------------------------------------------------------ |
+| F-01 | Переключатель «Схема / Спутник» в UI (segmented control) |
+| F-02 | Спутниковая подложка как новый raster-источник (бесплатный, без API-ключа) |
+| F-03 | В режиме «Спутник» — скрыта OSM-схема, показаны спутниковые тайлы |
+| F-04 | Все надстройки (грунтовки, тропы, POI, hillshade, TRI, маршрут, GPX) поверх спутника |
+| F-05 | Сохранение выбора в `localStorage` (ключ `map-base-layer`) |
+| F-06 | Восстановление выбора при загрузке страницы и при смене темы |
+| F-07 | Корректное отображение атрибуции спутниковых тайлов |
+| F-08 | Сохранение всех пользовательских слоёв (роутинг, GPX, recon) при переключении |
+
+### Out of scope
+
+- Кэширование спутниковых тайлов (offline / PWA — это PH-9).
+- Динамический выбор провайдера спутниковых тайлов в UI.
+- Гибридный режим «Спутник + подписи дорог OSM поверх».
+- Самостоятельный хостинг спутниковых тайлов (юридические/трафик-риски).
+- Изменение базовой карты для расчёта маршрутов (роутинг по-прежнему OSRM).
+- Авто-переключение Схема/Спутник в зависимости от зума.
+
+## 4. Метрики успеха
+
+| Метрика | Критерий |
+| ------------------------ | ------------------------------------------------------------------------------------- |
+| Время переключения | ≤ 500 мс от клика до первой видимой спутниковой плитки |
+| Сохранение состояния | Выбор подложки сохраняется после reload, смены темы, смены слоёв terrain |
+| Совместимость со слоями | Грунтовки, тропы, POI, маршрут OSRM, GPX-треки, hillshade, TRI видны и поверх спутника |
+| Совместимость с темой | Переключение тёмной/светлой темы не сбрасывает режим «Спутник» |
+| Атрибуция | На карте видна корректная атрибуция провайдера спутника |
+| Не ломает существующее | Все режимы (роутинг, разведка, красивый маршрут, GPX, линейка) работают как прежде |
+
+## 5. Риски
+
+| Риск | Вероятность | Влияние | Митигация |
+| ------------------------------------------------------------------------------------------------- | ----------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Провайдер спутниковых тайлов закроет доступ / введёт лимит / потребует API-ключ | Средняя | Высокое | Зафиксировать конкретного провайдера в ADR; предусмотреть точку расширения для альтернативного провайдера (несколько URL) |
+| Спутниковая подложка медленно грузится → пользователь видит «дыры» | Высокая | Среднее | Использовать background-цвет (тёмно-серый) под спутником; OSM-схема остаётся как fallback в случае ошибки загрузки тайлов |
+| Цвет грунтовок и троп плохо виден на спутниковой подложке | Высокая | Среднее | TRZ: на режиме «Спутник» включается обводка (halo) у линий грунтовок и троп — по аналогии с подписями POI |
+| Hillshade поверх спутника даёт некрасивое наложение (двойное затенение рельефа) | Средняя | Низкое | Hillshade продолжает работать поверх спутника как и поверх схемы — авто-выключение не вводится (TRZ §1 REQ-F-04, ADR-004 §«Контекст 1.5»); визуальная проверка — UI-тест AC-04 «Hillshade поверх спутника» |
+| Юридические ограничения на использование стороннего провайдера спутниковых тайлов | Низкая | Высокое | В ADR указать выбранного провайдера с лицензией, разрешающей использование без API-ключа (Esri World Imagery, ArcGIS) |
+| Регресс UI на мобильных устройствах из-за нового переключателя | Низкая | Среднее | UI-тест-кейсы (04b) для desktop и mobile viewport |
+| Конфликт с уже сохранёнными localStorage-значениями старых версий | Низкая | Низкое | Использовать новый ключ `map-base-layer`, default = `schematic` |
+
+## 6. Зависимости
+
+- Только фронтенд — backend изменений не требуется.
+- MapLibre GL JS 4.7.0 (уже подключен).
+- Внешний провайдер спутниковых тайлов (выбор и фиксация — в ADR).
+- Сетевое подключение клиента к серверу провайдера.
+
+## 7. Связь с roadmap
+
+- Фаза PH-5 Redesign — тёмная тема и mobile UI уже сделаны; ET-007
+ встраивается в эту же панель «Рельеф / Слои» (одна точка управления
+ визуальными слоями карты).
+- Фаза PH-9 PWA — кэширование спутниковых тайлов оффлайн — будет
+ планироваться отдельно, ET-007 закладывает архитектурную основу
+ (источник тайлов, точка переключения).
diff --git a/docs/work-items/ET-007/02-trz.md b/docs/work-items/ET-007/02-trz.md
new file mode 100644
index 0000000..63d7125
--- /dev/null
+++ b/docs/work-items/ET-007/02-trz.md
@@ -0,0 +1,498 @@
+---
+type: trz
+work_item_id: ET-007
+title: "ТЗ: Спутниковая карта (Схема / Спутник)"
+version: 2
+status: draft
+created_at: 2026-05-31
+updated_at: 2026-05-31
+changelog:
+ - "v2 (2026-05-31): code-review fixes (12-review.md, attempt 2/3) — P1-1..P1-6: реальные id halo-слоёв, контраст POI labels, единый satellite-bg, контракт с layerState.basemap, синхронизация halo с чекбоксами."
+authors:
+ - "agent:analyst"
+---
+
+# ТЗ — ET-007: Спутниковая карта (Схема / Спутник)
+
+## 1. Функциональные требования
+
+### REQ-F-01: Переключатель «Схема / Спутник»
+
+- В попап-панели слоёв (`#terrain-popup`, открывается кнопкой
+ `#terrain-toggle`) добавляется новая секция в самом верху панели —
+ «Подложка».
+- Реализация — segmented-control (`.seg-control` / `.seg-btn`) с двумя
+ кнопками:
+ - «Схема» (`data-base="schematic"`, ID `base-btn-schematic`) —
+ активна по умолчанию.
+ - «Спутник» (`data-base="satellite"`, ID `base-btn-satellite`).
+- Активная кнопка визуально выделяется (`.active` — оранжевый фон, по
+ аналогии с переключателем единиц измерения, ET-005).
+- Обработчик: `onBaseLayerToggle(base)` в `src/web/app.js`.
+- Под переключателем — горизонтальная линия-разделитель (`
`),
+ как уже сделано между секциями попапа.
+
+### REQ-F-02: Спутниковый растровый источник
+
+- Используется растровый тайл-сервер Esri World Imagery (см. ADR в
+ `docs/work-items/ET-007/06-adr/`):
+ - URL-шаблон: `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}`.
+ - `tileSize: 256`, `minzoom: 0`, `maxzoom: 19`.
+ - Атрибуция: «Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community».
+- Источник добавляется на карту лениво: при первом включении режима
+ «Спутник», а не на старте приложения.
+- ID источника: `satellite-raster`.
+- ID слоя: `satellite-base`.
+
+### REQ-F-03: Поведение в режиме «Спутник»
+
+- При включении «Спутник»:
+ - Если ещё не добавлен — добавить source `satellite-raster` и layer
+ `satellite-base` сразу после слоя `background` (т.е. ниже всех
+ остальных слоёв).
+ - Слой `osm-base` (существующий) скрывается (`visibility: none`).
+ - Слой `background` остаётся (показывает «дыры» если тайлы ещё не
+ загрузились) — цвет фона на спутнике — единая константа `#2a2a2a`
+ для обеих тем (тёмно-серый, чтобы не «бликовал» под медленно
+ подгружающимися спутниковыми плитками; решение зафиксировано в
+ ADR-004 §6). Baseline `background-color` для возврата на «Схему»:
+ `#f0ede6` (light), `#1a1a2e` (dark) — см. Data §5.
+- При возврате на «Схема»:
+ - `osm-base` снова видим (`visibility: visible`).
+ - `satellite-base` скрывается (`visibility: none`), но не удаляется
+ из стиля (быстрое повторное переключение).
+
+### REQ-F-04: Совместимость со слоями приложения
+
+Все клиентские слои должны корректно отображаться поверх спутника:
+
+| Слой | Z-order над спутником | Доп. правила в режиме «Спутник» |
+| ----------------------------- | --------------------- | ------------------------------------------------------------------------------ |
+| Hillshade (`terrain-hillshade`) | поверх спутника | Включается/выключается чекбоксом как раньше; по умолчанию НЕ авто-выключается |
+| TRI (`terrain-tri`) | поверх спутника | Аналогично hillshade |
+| Trails — грунтовки (`trails-track`) | поверх terrain | Halo через парный underlay-слой `trails-track-halo-satellite` (единый halo на весь слой, без разбиения по grade) |
+| Paths / bridleway (`trails-path-bridleway`) | поверх trails | Halo через парный underlay-слой `trails-path-bridleway-halo-satellite` |
+| Asphalt-дороги (`trails-asphalt`) | поверх trails | Halo не вводится — слой по умолчанию скрыт (`visibility: none`, `line-opacity: 0`); если будет включён в будущем, halo добавляется тем же паттерном |
+| POI circles (`poi-circles`) | поверх trails | Обводка `circle-stroke-color: #ffffff`, толщина 2 px |
+| POI labels (`poi-labels`) | поверх POI | `text-color: #ffffff`, `text-halo-color: #000000`, `text-halo-width: 2` для читаемости на спутнике (см. REQ-F-04-POI ниже) |
+| Route / Scenic / Link / Ruler | поверх POI | Без изменений |
+| GPX-треки и waypoints | поверх Route | Без изменений (ET-006 уже совместим) |
+
+**REQ-F-04-POI (контраст подписей POI на спутнике).** На спутнике
+менять обе пары свойств `text-color` и `text-halo-*`, иначе тёмный
+текст `#333333` (light-theme) останется нечитаем поверх тёмного halo.
+Конкретные значения и baseline-возврат — в Data §5.
+
+**Halo-слои в `style*.json` (подтверждено фактическим кодом
+`src/web/style.json` и `style-dark.json`):** реальные id — это
+`trails-track-halo-satellite` и `trails-path-bridleway-halo-satellite`.
+Слоёв `trails-grade1..5-halo-satellite` или
+`paths-bridleway-halo-satellite` **нет** и заводить их не нужно:
+`trails-track` хранит дифференциацию по grade внутри одного `match`-
+выражения по `tracktype`. На спутнике halo единого цвета/ширины
+накладывается на весь `trails-track` целиком; разделять halo по grade
+не требуется (визуально не различимо под линией grade-цвета).
+
+Реализация:
+- Halo для грунтовок и троп — пара underlay-слоёв
+ (`trails-track-halo-satellite`, `trails-path-bridleway-halo-satellite`),
+ уже присутствующих в обоих `style*.json` с `visibility: none`.
+ Включаются через `setLayoutProperty(..., 'visibility', 'visible')`
+ только в режиме «Спутник».
+- Стили POI (circles и labels) на спутнике задаются динамически через
+ `setPaintProperty` при переключении режима; baseline-значения
+ возврата на «Схему» зафиксированы в `08-data-requirements.md` §5
+ и в `applyBaseLayer()` (см. §5.2 ниже).
+
+### REQ-F-05: Сохранение состояния (localStorage)
+
+- Ключ: `map-base-layer`.
+- Значения: `"schematic"` (default) | `"satellite"`.
+- При `onBaseLayerToggle()` — запись.
+- При старте приложения — чтение и применение через
+ `restoreBaseLayerState()` (по аналогии с `restoreTerrainState()`).
+
+### REQ-F-06: Восстановление после смены стиля карты
+
+- При вызове `map.setStyle()` (переключение тёмной/светлой темы, см.
+ `switchMapStyle()` в `app.js`) спутниковый source/layer удаляются
+ вместе со стилем.
+- В функции `rebuildMapOverlays()` добавляется вызов
+ `restoreBaseLayerState()` — это пересоздаёт source/layer спутника и
+ выставляет видимость по сохранённому состоянию.
+- Порядок вызовов в `rebuildMapOverlays()`: `restoreBaseLayerState()`
+ вызывается **до** `restoreTerrainState()` — чтобы hillshade/TRI
+ оказались выше спутника, но ниже trails (тот же подход, что и для
+ schematic-режима).
+
+### REQ-F-07: Атрибуция
+
+- При создании source `satellite-raster` передаётся свойство
+ `attribution: "Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community"`.
+- MapLibre автоматически отображает атрибуцию в правом нижнем углу
+ карты, когда соответствующий source активен.
+- Атрибуция OSM остаётся видимой в обоих режимах (vector-источник
+ `trails-tiles` всегда активен).
+
+### REQ-F-08: Fallback при ошибке загрузки тайлов
+
+- Если спутниковые тайлы не загружаются (network error / 4xx / 5xx),
+ MapLibre сам показывает прозрачную плитку — под ней видим `background`.
+- Логика fallback на схему не предусмотрена (пользователь сам
+ переключит, если нужно).
+
+## 2. Нефункциональные требования
+
+### REQ-NF-01: Производительность
+
+- Время переключения «Схема → Спутник» (до первой видимой спутниковой
+ плитки): ≤ 500 мс при скорости сети ≥ 5 Мбит/с.
+- Переключение обратно «Спутник → Схема» — мгновенное (источник
+ остаётся в стиле, меняется только visibility).
+- В момент переключения не должно быть «прыжков» камеры — `center`,
+ `zoom`, `bearing`, `pitch` сохраняются.
+
+### REQ-NF-02: Совместимость
+
+- Браузеры: Chrome 90+, Firefox 90+, Safari 15+.
+- Мобильные: iOS Safari 15+, Chrome для Android.
+- MapLibre GL JS 4.7.0 (уже подключен).
+
+### REQ-NF-03: UX
+
+- Текущая активная подложка визуально видна в UI всегда (в попапе
+ слоёв).
+- Переключение происходит без перезагрузки страницы и без потери
+ пользовательского состояния (маршрута, GPX, точек разведки).
+
+### REQ-NF-04: Хранение
+
+- localStorage ключ `map-base-layer`, размер ≤ 16 байт.
+- Никаких других данных приложение для этой фичи не хранит.
+
+### REQ-NF-05: Безопасность
+
+- Запросы к Esri World Imagery идут по HTTPS.
+- Никаких персональных данных пользователя в URL запросов не
+ передаётся.
+- Атрибуция выводится в соответствии с лицензией провайдера (см. ADR).
+
+## 3. UI-спецификация
+
+### 3.1 Изменения в `#terrain-popup`
+
+Сейчас:
+```
+┌────────────────────────────┐
+│ Эндуро │
+│ ☐ Тени рельефа │
+│ ☐ Перепады │
+│ ─────── │
+│ ☑ Грунтовки │
+│ ☑ Тропы │
+│ ─────── │
+│ ☑ POI │
+│ ─────── │
+│ Единицы [км][мили] │
+└────────────────────────────┘
+```
+
+После:
+```
+┌────────────────────────────┐
+│ Подложка [Схема][Спутник] │ ← новая секция
+│ ─────── │
+│ Эндуро │
+│ ☐ Тени рельефа │
+│ ☐ Перепады │
+│ ─────── │
+│ ☑ Грунтовки │
+│ ☑ Тропы │
+│ ─────── │
+│ ☑ POI │
+│ ─────── │
+│ Единицы [км][мили] │
+└────────────────────────────┘
+```
+
+### 3.2 Разметка HTML
+
+В `src/web/index.html`, в начале `#terrain-popup` (сразу после
+`
Эндуро
` ИЛИ выше него — по
+выбору разработчика; рекомендуется в самом верху для большей
+заметности):
+
+```html
+
+
+ Подложка
+
+
+
+
+
+
+```
+
+### 3.3 CSS
+
+В `src/web/app.css` — добавить стили (по аналогии с `.terrain-unit-row`):
+
+```css
+.terrain-base-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 0;
+}
+.terrain-base-label {
+ font-size: 12px;
+ color: var(--text2);
+ flex-shrink: 0;
+}
+.terrain-base-row .seg-control {
+ flex: 1;
+ margin-bottom: 0;
+}
+.base-seg .seg-btn {
+ font-size: 12px;
+}
+```
+
+### 3.4 Поведение на мобильных устройствах
+
+- Попап `#terrain-popup` уже адаптирован под мобильные (ET-005). Новая
+ строка не должна нарушать ширину попапа.
+- Высота кнопок `.seg-btn` остаётся 34px (как у переключателя единиц).
+
+## 4. Данные
+
+### 4.1 Спутниковый источник (MapLibre source spec)
+
+```js
+{
+ type: 'raster',
+ tiles: [
+ 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
+ ],
+ tileSize: 256,
+ minzoom: 0,
+ maxzoom: 19,
+ attribution: 'Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community'
+}
+```
+
+### 4.2 Спутниковый слой (MapLibre layer spec)
+
+```js
+{
+ id: 'satellite-base',
+ type: 'raster',
+ source: 'satellite-raster',
+ paint: {
+ 'raster-opacity': 1.0,
+ 'raster-resampling': 'linear'
+ },
+ layout: { visibility: 'none' } // включается при переключении
+}
+```
+
+Вставляется в стиль сразу после слоя `background`.
+
+### 4.3 localStorage
+
+| Ключ | Значения | Default |
+| ----------------- | ------------------------------ | ------------- |
+| `map-base-layer` | `"schematic"` \| `"satellite"` | `"schematic"` |
+
+## 5. Алгоритмы
+
+### 5.1 `onBaseLayerToggle(base)`
+
+```
+1. Если base === текущий — return.
+2. Сохранить в localStorage('map-base-layer', base).
+3. Применить applyBaseLayer(base).
+4. syncBaseLayerUI(base).
+```
+
+### 5.2 `applyBaseLayer(base)`
+
+```
+1. map = window._map; если нет — return.
+2. Если base === 'satellite':
+ 2.1. Если source 'satellite-raster' отсутствует — addSource (см. 4.1).
+ 2.2. Если layer 'satellite-base' отсутствует — addLayer (см. 4.2)
+ без beforeId. Корректный z-order гарантируется тем, что
+ restoreBaseLayerState вызывается ПЕРВЫМ в rebuildMapOverlays
+ (см. ADR-004 §«Вариант O», O-A; см. также R-7 в Tech-Risks).
+ 2.3. setLayoutProperty('satellite-base', 'visibility', 'visible').
+ 2.4. Запомнить layerState.basemap в _savedBasemapState (см. §5.6).
+ Принудительно скрыть osm-base:
+ setLayoutProperty('osm-base', 'visibility', 'none').
+ 2.5. Включить halo-слои (см. §5.7 — синхронизация с чекбоксами):
+ для каждой пары (base, halo) ∈
+ [('trails-track', 'trails-track-halo-satellite'),
+ ('trails-path-bridleway', 'trails-path-bridleway-halo-satellite')]
+ выставить halo.visibility = base.visibility текущего слоя.
+ 2.6. Применить динамические правки POI:
+ - poi-circles: circle-stroke-color = '#ffffff',
+ circle-stroke-width = 2;
+ - poi-labels: text-color = '#ffffff',
+ text-halo-color = '#000000',
+ text-halo-width = 2.
+ 2.7. Сменить background-color на единую satellite-константу
+ '#2a2a2a' (для обеих тем, см. ADR-004 §6).
+3. Иначе (base === 'schematic'):
+ 3.1. setLayoutProperty('osm-base', 'visibility',
+ _savedBasemapState === false ? 'none' : 'visible') —
+ восстановить выбор пользователя по «Базовая карта»
+ (см. §5.6); по умолчанию (если не сохранено) — 'visible'.
+ 3.2. setLayoutProperty('satellite-base', 'visibility', 'none')
+ (если слой существует).
+ 3.3. Скрыть halo-underlay-слои:
+ для обеих пар выставить halo.visibility = 'none'.
+ 3.4. Вернуть POI к baseline текущей темы (см. Data §5):
+ - poi-circles: circle-stroke-color / circle-stroke-width
+ читаются из Data §5 baseline (поэтапно: light → dark);
+ - poi-labels: text-color, text-halo-color, text-halo-width — то же.
+ Источник истины baseline'ов — Data §5; код держит две константы
+ per-theme и выбирает по текущей теме.
+ 3.5. Background-color — установить baseline текущей темы из Data §5
+ ('#f0ede6' light / '#1a1a2e' dark). Прямая запись через
+ setPaintProperty (не полагаемся на setStyle, потому что
+ applyBaseLayer вызывается и без смены стиля).
+```
+
+### 5.3 `restoreBaseLayerState()`
+
+```
+1. base = localStorage.getItem('map-base-layer') || 'schematic'.
+2. syncBaseLayerUI(base).
+3. applyBaseLayer(base).
+```
+
+### 5.4 `syncBaseLayerUI(base)`
+
+```
+1. schematicBtn.classList.toggle('active', base === 'schematic').
+2. satelliteBtn.classList.toggle('active', base === 'satellite').
+```
+
+### 5.5 Интеграция с `rebuildMapOverlays()` (`app.js`)
+
+В существующей функции (см. `app.js`, ~строка 127) добавить вызов
+**первым**:
+
+```js
+function rebuildMapOverlays() {
+ // ET-007/ADR-004 O-A: восстановить подложку ПЕРВОЙ — terrain/trails/POI
+ // ложатся поверх неё (z-order через порядок вставки, без beforeId).
+ // Функция определена в этом же файле (ADR-004 §2), глобально доступна.
+ restoreBaseLayerState();
+ // ── далее без изменений ──
+ restoreTerrainState();
+ restoreTrailsState();
+ // ...
+}
+```
+
+### 5.6 Взаимодействие с существующим `toggleLayer('basemap')`
+
+В `app.js:384–391` уже определены:
+
+```js
+const layerState = { tracks: true, paths: true, poi: true, basemap: true };
+const layerGroups = { …, basemap: ['osm-base'] };
+function toggleLayer(group) { …setLayoutProperty('osm-base', 'visibility', …) }
+```
+
+— это существующий механизм «Базовая карта (схема)» как
+самостоятельного выключателя. ET-007 уважает этот механизм по
+следующему контракту:
+
+1. **При входе в «Спутник»** (`applyBaseLayer('satellite')`, §5.2 шаг
+ 2.4): запомнить `layerState.basemap` в локальной переменной
+ `_savedBasemapState` (init: `null`). Затем **принудительно** скрыть
+ `osm-base`. `layerState.basemap` **не меняется** — UI-кнопка
+ `#btn-basemap` остаётся в прежнем визуальном состоянии.
+2. **Пока активен «Спутник»**, кнопка «Базовая карта» скрыта из UI
+ (CSS-класс `.satellite-active` на корне приложения скрывает
+ `#btn-basemap`) — пользователь не должен пытаться включить схему
+ поверх спутника (гибридный режим out of scope BRD §3). Альтернатива
+ реализации — disabled, на усмотрение разработчика; визуальный
+ эффект и AC-02/AC-03 идентичны.
+3. **При возврате на «Схему»** (§5.2 шаг 3.1): `osm-base.visibility`
+ восстанавливается из `_savedBasemapState` (по умолчанию `true` →
+ `'visible'`, если ранее пользователь сам выключал — `false` →
+ `'none'`). После восстановления `_savedBasemapState = null`.
+4. **На «Схеме» (default-режим)**: `toggleLayer('basemap')` работает
+ ровно как раньше — пишет в `layerState.basemap` и переключает
+ `osm-base.visibility`. ET-007 этот код не трогает.
+
+### 5.7 Синхронизация halo с чекбоксами «Грунтовки» / «Тропы» / «POI»
+
+В `app.js:2783–2826` существуют `onTrailsCheckbox()` и
+`restoreTrailsState()`, которые управляют `visibility` только
+`trails-track` и `trails-path-bridleway`. Halo-underlay-слои
+(`*-halo-satellite`) сейчас они не трогают — в режиме «Спутник» это
+дало бы «фантом» halo без основной линии.
+
+Правило (источник истины): **halo-слой видим ⇔ (текущая база ===
+'satellite') AND (соответствующий пользовательский чекбокс ON)**.
+
+Реализация:
+
+1. Ввести хелпер `applyTrailHaloVisibility(trackOn, pathOn)`:
+ - для пары `('trails-track-halo-satellite', trackOn)` и
+ `('trails-path-bridleway-halo-satellite', pathOn)`:
+ `visibility = (currentBaseLayer === 'satellite' && checked) ? 'visible' : 'none'`.
+2. В `onTrailsCheckbox()` после установки `visibility` основным слоям —
+ вызвать `applyTrailHaloVisibility(trackChecked, pathChecked)`.
+3. В `restoreTrailsState()` после установки `visibility` основным слоям —
+ вызвать `applyTrailHaloVisibility(trackOn, pathOn)`.
+4. В `applyBaseLayer('satellite')` (§5.2 шаг 2.5) и
+ `applyBaseLayer('schematic')` (§5.2 шаг 3.3) — читать текущее
+ состояние чекбоксов из DOM (`#trails-track-cb`, `#trails-path-cb`)
+ и вызвать тот же хелпер.
+
+**POI:** для группы `poi-circles` / `poi-labels` отдельных
+halo-underlay-слоёв нет — динамические правки `setPaintProperty`
+(см. §5.2) уже привязаны к видимости самих слоёв. При выключении
+чекбокса «POI» оба слоя становятся `visibility: none` через
+существующий механизм `layerState.poi`/`restorePoiState()` — текстовые
+halo-свойства просто не видны, поэтому отдельная синхронизация не
+требуется.
+
+## 6. Файловая структура изменений
+
+```
+src/web/
+├── index.html # + блок переключателя в #terrain-popup
+├── app.css # + стили .terrain-base-row, .base-seg
+├── app.js # + onBaseLayerToggle, applyBaseLayer,
+ # restoreBaseLayerState, syncBaseLayerUI,
+ # правка rebuildMapOverlays
+```
+
+Backend изменений нет.
+
+## 7. Взаимодействие с существующими режимами
+
+- Все режимы тулбара (Маршрут, Связка, Красивый, Разведка, Линейка,
+ Поиск, Метка, GPX) работают независимо от выбранной подложки.
+- Переключение подложки **не сбрасывает** состояние режимов: маршруты,
+ GPX-треки, точки разведки, линейка, метки — остаются.
+- Переключение темы (тёмная/светлая) **не сбрасывает** выбор подложки.
+- При вызове `map.setStyle()` (тема, восстановление стиля)
+ спутниковый слой пересоздаётся в `rebuildMapOverlays()`.
+
+## 8. Открытые вопросы для ADR
+
+- Выбор провайдера спутниковых тайлов (Esri / Mapbox / Bing / OpenAerialMap).
+- Решение по halo для POI/trails на спутнике: статические правки в
+ `style.json` через `visibility` или динамические `setPaintProperty`.
+- Поведение hillshade при включении спутника: оставить как есть (по
+ выбору пользователя) — зафиксировано в REQ-F-04 как «оставить».
diff --git a/docs/work-items/ET-007/03-acceptance-criteria.md b/docs/work-items/ET-007/03-acceptance-criteria.md
new file mode 100644
index 0000000..f156df8
--- /dev/null
+++ b/docs/work-items/ET-007/03-acceptance-criteria.md
@@ -0,0 +1,263 @@
+---
+type: acceptance-criteria
+work_item_id: ET-007
+title: "AC: Спутниковая карта (Схема / Спутник)"
+version: 2
+status: draft
+created_at: 2026-05-31
+updated_at: 2026-05-31
+changelog:
+ - "v2 (2026-05-31): code-review fixes (12-review.md P1-2, P1-5, P1-6) — добавлены сценарии: видимость #btn-basemap при входе/выходе из «Спутник», save&restore _savedBasemapState, синхронизация halo с чекбоксами Грунтовки/Тропы, явные значения POI text-color/halo на спутнике и baseline при возврате."
+authors:
+ - "agent:analyst"
+---
+
+# Acceptance Criteria — ET-007: Спутниковая карта (Схема / Спутник)
+
+## AC-01: UI переключателя
+
+```gherkin
+Feature: Переключатель подложки в попапе слоёв
+
+ Scenario: Открытие попапа показывает переключатель
+ Given пользователь находится на карте
+ When пользователь нажимает кнопку «Рельеф» (#terrain-toggle)
+ Then открывается попап #terrain-popup
+ And в попапе виден segmented-control «Подложка» с кнопками «Схема» и «Спутник»
+ And одна из кнопок имеет класс .active
+
+ Scenario: Default — Схема
+ Given localStorage пуст (или ключ 'map-base-layer' не задан)
+ When пользователь открывает попап слоёв
+ Then активна кнопка «Схема» (#base-btn-schematic)
+ And не активна кнопка «Спутник» (#base-btn-satellite)
+```
+
+## AC-02: Переключение на «Спутник»
+
+```gherkin
+Feature: Переключение Схема → Спутник
+
+ Scenario: Базовое переключение
+ Given активна подложка «Схема»
+ When пользователь нажимает «Спутник» в попапе слоёв
+ Then кнопка «Спутник» получает класс .active
+ And кнопка «Схема» теряет класс .active
+ And на карте слой osm-base скрыт (visibility=none)
+ And на карте появляется слой satellite-base (visibility=visible)
+ And положение карты (center, zoom, bearing, pitch) не изменилось
+
+ Scenario: Атрибуция Esri отображается
+ Given пользователь включил режим «Спутник»
+ Then в нижнем правом углу карты видна атрибуция «Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community»
+
+ Scenario: Кнопка «Базовая карта» скрывается на спутнике (P1-5)
+ Given активна подложка «Спутник»
+ Then UI-кнопка #btn-basemap не видна пользователю
+ And пользователь не может из UI включить osm-base поверх спутника (out of scope, BRD §3 — гибридный режим)
+
+ Scenario: Запоминание выбора «Базовая карта» при входе в Спутник (P1-5)
+ Given активна подложка «Схема»
+ And пользователь явно выключил «Базовую карту» (layerState.basemap === false, osm-base.visibility === 'none')
+ When пользователь переключается на «Спутник»
+ Then значение layerState.basemap сохраняется во внутреннем _savedBasemapState === false
+ And osm-base.visibility остаётся 'none' (принудительно)
+```
+
+## AC-03: Переключение на «Схема»
+
+```gherkin
+Feature: Переключение Спутник → Схема
+
+ Scenario: Возврат на схему (layerState.basemap по умолчанию true)
+ Given активна подложка «Спутник»
+ And до входа в «Спутник» layerState.basemap === true (default)
+ When пользователь нажимает «Схема» в попапе слоёв
+ Then кнопка «Схема» получает класс .active
+ And слой osm-base снова виден (visibility=visible)
+ And слой satellite-base скрыт (visibility=none), но source остаётся в стиле
+ And положение карты не изменилось
+ And UI-кнопка #btn-basemap снова видна
+
+ Scenario: Возврат на схему с восстановлением выбора пользователя (P1-5)
+ Given активна подложка «Спутник»
+ And до входа в «Спутник» пользователь выключил «Базовую карту» (_savedBasemapState === false)
+ When пользователь нажимает «Схема»
+ Then слой osm-base остаётся скрытым (visibility=none) — выбор пользователя восстановлен
+ And layerState.basemap === false
+ And _savedBasemapState сбрасывается в null
+```
+
+## AC-04: Совместимость со слоями приложения
+
+```gherkin
+Feature: Слои поверх спутника
+
+ Scenario: Грунтовки и тропы видны на спутнике
+ Given активна подложка «Спутник»
+ And в попапе включены «Грунтовки» и «Тропы»
+ Then на карте видны линии грунтовок (trails-track) и троп (trails-path-bridleway) поверх спутника
+ And halo-слой trails-track-halo-satellite visibility=visible
+ And halo-слой trails-path-bridleway-halo-satellite visibility=visible
+
+ Scenario: Выключение «Грунтовки» скрывает и halo (P1-6)
+ Given активна подложка «Спутник»
+ And чекбокс «Грунтовки» был ON
+ When пользователь снимает чекбокс «Грунтовки»
+ Then trails-track visibility=none
+ And trails-track-halo-satellite visibility=none (halo не остаётся «фантомом»)
+
+ Scenario: Выключение «Тропы» скрывает и halo (P1-6)
+ Given активна подложка «Спутник»
+ And чекбокс «Тропы» был ON
+ When пользователь снимает чекбокс «Тропы»
+ Then trails-path-bridleway visibility=none
+ And trails-path-bridleway-halo-satellite visibility=none
+
+ Scenario: На «Схеме» halo-слои всегда скрыты (P1-6)
+ Given активна подложка «Схема»
+ And чекбокс «Грунтовки» ON
+ Then trails-track visibility=visible
+ And trails-track-halo-satellite visibility=none
+
+ Scenario: POI видны и читаемы на спутнике (P1-2)
+ Given активна подложка «Спутник»
+ And в попапе включён «POI»
+ Then на карте видны маркеры POI поверх спутника
+ And poi-labels paint: text-color === '#ffffff'
+ And poi-labels paint: text-halo-color === '#000000'
+ And poi-labels paint: text-halo-width === 2
+ And poi-circles paint: circle-stroke-color === '#ffffff'
+ And poi-circles paint: circle-stroke-width === 2
+
+ Scenario: POI baseline восстанавливается на «Схеме» (P1-2)
+ Given был активен «Спутник», POI labels в режиме спутника
+ When пользователь возвращается на «Схему» (light-тема)
+ Then poi-labels paint: text-color === '#333333' (baseline light, Data §5)
+ And poi-labels paint: text-halo-color === '#ffffff' (baseline light)
+ And poi-labels paint: text-halo-width === 1.5 (baseline light)
+
+ Scenario: Hillshade поверх спутника
+ Given активна подложка «Спутник»
+ When пользователь включает «Тени рельефа»
+ Then на карте видны и спутник, и hillshade (hillshade поверх спутника)
+
+ Scenario: Маршрут OSRM поверх спутника
+ Given пользователь построил маршрут через OSRM
+ When пользователь переключает подложку на «Спутник»
+ Then маршрут остаётся виден поверх спутника
+ And статистика маршрута сохранена
+
+ Scenario: GPX-треки поверх спутника
+ Given пользователь загрузил GPX-трек
+ When пользователь переключает подложку на «Спутник»
+ Then GPX-линии и waypoints остаются видны поверх спутника
+```
+
+## AC-05: Сохранение в localStorage
+
+```gherkin
+Feature: Persistence выбора подложки
+
+ Scenario: Сохранение при переключении
+ Given активна подложка «Схема»
+ When пользователь нажимает «Спутник»
+ Then localStorage['map-base-layer'] === 'satellite'
+
+ Scenario: Восстановление после reload
+ Given localStorage['map-base-layer'] === 'satellite'
+ When пользователь перезагружает страницу
+ Then после загрузки карты активна подложка «Спутник»
+ And кнопка «Спутник» имеет класс .active
+```
+
+## AC-06: Восстановление при смене темы
+
+```gherkin
+Feature: Подложка переживает смену темы
+
+ Scenario: Переключение тёмной/светлой темы в режиме «Спутник»
+ Given активна подложка «Спутник»
+ When пользователь переключает тему (тёмная ↔ светлая)
+ Then после завершения map.setStyle() спутниковый слой восстановлен
+ And подложка «Спутник» остаётся активной
+ And все слои поверх (trails, POI, маршрут, GPX) восстановлены
+
+ Scenario: Переключение слоёв terrain в режиме «Спутник»
+ Given активна подложка «Спутник»
+ When пользователь включает или выключает «Тени рельефа» / «Перепады»
+ Then подложка «Спутник» остаётся активной
+```
+
+## AC-07: Совместимость с режимами тулбара
+
+```gherkin
+Feature: Подложка не мешает другим режимам
+
+ Scenario: Режим «Маршрут» на спутнике
+ Given активна подложка «Спутник»
+ When пользователь активирует режим «Маршрут»
+ And тапает 2 точки на карте
+ Then маршрут строится корректно
+ And линия маршрута видна на спутнике
+
+ Scenario: Режим «Разведка» на спутнике
+ Given активна подложка «Спутник»
+ When пользователь активирует режим «Разведка» и тапает на карту
+ Then круг радиуса разведки видим
+ And статистика разведки отображается
+
+ Scenario: Линейка на спутнике
+ Given активна подложка «Спутник»
+ When пользователь активирует «Линейка» и расставляет точки
+ Then линия линейки видна
+ And расстояние отображается
+
+ Scenario: Поиск на спутнике
+ Given активна подложка «Спутник»
+ When пользователь нажимает «Поиск» и вводит запрос
+ Then результаты поиска отображаются
+ And карта корректно центрируется на найденной точке
+```
+
+## AC-08: Производительность
+
+```gherkin
+Feature: Скорость переключения
+
+ Scenario: Переключение Схема → Спутник
+ Given активна подложка «Схема» и сеть ≥ 5 Мбит/с
+ When пользователь нажимает «Спутник»
+ Then первая спутниковая плитка отображается в течение ≤ 500 мс
+
+ Scenario: Переключение Спутник → Схема
+ Given активна подложка «Спутник» (тайлы уже подгружены)
+ When пользователь нажимает «Схема»
+ Then смена визуально мгновенная (≤ 100 мс)
+```
+
+## AC-09: Mobile UI
+
+```gherkin
+Feature: Переключатель на мобильных устройствах
+
+ Scenario: Попап слоёв на мобильном
+ Given пользователь открыл приложение на мобильном устройстве (виртуальный viewport 375×812)
+ When пользователь открывает попап слоёв
+ Then переключатель «Подложка» виден полностью
+ And обе кнопки нажимаемы (touch target ≥ 34px)
+ And не перекрывает другие элементы попапа
+```
+
+## AC-10: Не ломает существующий функционал
+
+```gherkin
+Feature: Регресс-проверка
+
+ Scenario: Все режимы работают как в режиме «Схема», так и в «Спутник»
+ Given пользователь использует приложение
+ Then режимы Маршрут, Связка, Красивый, Разведка, Линейка, Поиск, Метка, GPX
+ работают одинаково в обеих подложках
+ And переключение единиц измерения (км/мили) работает в обеих подложках
+ And переключение темы работает в обеих подложках
+```
diff --git a/docs/work-items/ET-007/04-test-plan.yaml b/docs/work-items/ET-007/04-test-plan.yaml
new file mode 100644
index 0000000..a5ef729
--- /dev/null
+++ b/docs/work-items/ET-007/04-test-plan.yaml
@@ -0,0 +1,231 @@
+---
+type: test-plan
+work_item_id: ET-007
+title: "Test Plan: Спутниковая карта (Схема / Спутник)"
+version: 1
+status: draft
+created_at: 2026-05-31
+updated_at: 2026-05-31
+authors:
+ - "agent:analyst"
+
+test_suites:
+
+ - name: unit-base-layer-state
+ type: unit
+ description: "Чтение/запись/восстановление выбора подложки"
+ cases:
+ - id: U-01
+ name: "Default — Схема, если localStorage пуст"
+ input: "localStorage без ключа 'map-base-layer'"
+ expected: "restoreBaseLayerState() выставляет base='schematic'"
+
+ - id: U-02
+ name: "Чтение значения 'satellite' из localStorage"
+ input: "localStorage['map-base-layer'] = 'satellite'"
+ expected: "restoreBaseLayerState() выставляет base='satellite'"
+
+ - id: U-03
+ name: "Запись значения при переключении"
+ input: "onBaseLayerToggle('satellite')"
+ expected: "localStorage['map-base-layer'] === 'satellite'"
+
+ - id: U-04
+ name: "Игнор некорректного значения в localStorage"
+ input: "localStorage['map-base-layer'] = 'unknown'"
+ expected: "restoreBaseLayerState() fallback на 'schematic'"
+
+ - id: U-05
+ name: "Toggle на уже активный режим — no-op"
+ input: "active=schematic; onBaseLayerToggle('schematic')"
+ expected: "Никаких изменений в стиле, localStorage не записывается повторно"
+
+ - name: unit-ui-sync
+ type: unit
+ description: "Синхронизация .active у кнопок переключателя"
+ cases:
+ - id: U-10
+ name: "syncBaseLayerUI('satellite')"
+ input: "DOM с #base-btn-schematic.active и #base-btn-satellite без класса"
+ expected: "После: #base-btn-satellite.active=true, #base-btn-schematic.active=false"
+
+ - id: U-11
+ name: "syncBaseLayerUI('schematic')"
+ input: "DOM с #base-btn-satellite.active"
+ expected: "После: #base-btn-schematic.active=true, #base-btn-satellite.active=false"
+
+ - name: integration-maplibre-layers
+ type: integration
+ description: "Взаимодействие с MapLibre source/layer"
+ cases:
+ - id: I-01
+ name: "Добавление спутникового source при первом включении"
+ input: "applyBaseLayer('satellite') впервые"
+ expected: "map.getSource('satellite-raster') !== undefined; URL содержит arcgisonline.com"
+
+ - id: I-02
+ name: "Добавление спутникового layer при первом включении"
+ input: "applyBaseLayer('satellite') впервые"
+ expected: "map.getLayer('satellite-base') !== undefined; type='raster'"
+
+ - id: I-03
+ name: "Visibility OSM-base после переключения на спутник"
+ input: "applyBaseLayer('satellite')"
+ expected: "map.getLayoutProperty('osm-base', 'visibility') === 'none'"
+
+ - id: I-04
+ name: "Visibility satellite-base после переключения на схему"
+ input: "applyBaseLayer('satellite') → applyBaseLayer('schematic')"
+ expected: "satellite-base.visibility==='none', osm-base.visibility==='visible'"
+
+ - id: I-05
+ name: "Z-order: satellite ниже terrain и trails"
+ input: "applyBaseLayer('satellite'); включены hillshade и trails"
+ expected: "Layer index(satellite-base) < index(terrain-hillshade) < index(trails-track)"
+
+ - id: I-06
+ name: "Position карты сохраняется при переключении"
+ input: "center=[37.6,55.75], zoom=10; applyBaseLayer('satellite')"
+ expected: "После: getCenter() == [37.6,55.75], getZoom() == 10"
+
+ - id: I-07
+ name: "Атрибуция Esri зарегистрирована"
+ input: "applyBaseLayer('satellite')"
+ expected: "source 'satellite-raster' содержит attribution с упоминанием Esri"
+
+ - name: integration-style-switch
+ type: integration
+ description: "Поведение при map.setStyle (смена темы)"
+ cases:
+ - id: I-10
+ name: "Спутник восстанавливается после setStyle (тёмная → светлая)"
+ input: "active='satellite'; вызывается switchMapStyle()"
+ expected: "После idle: layer 'satellite-base' существует; visibility='visible'; osm-base.visibility='none'"
+
+ - id: I-11
+ name: "Сохранённое состояние читается из localStorage в rebuildMapOverlays"
+ input: "localStorage='satellite'; rebuildMapOverlays() вручную"
+ expected: "applyBaseLayer вызван с 'satellite'"
+
+ - id: I-12
+ name: "Восстановление выполняется до restoreTerrainState"
+ input: "rebuildMapOverlays() с заглушками-shpions"
+ expected: "Порядок вызовов: restoreBaseLayerState → restoreTerrainState"
+
+ - name: integration-other-layers
+ type: integration
+ description: "Совместимость со всеми клиентскими слоями"
+ cases:
+ - id: I-20
+ name: "Маршрут OSRM не теряется при переключении"
+ input: "Построен маршрут; applyBaseLayer('satellite')"
+ expected: "Layer маршрута существует, координаты не изменились"
+
+ - id: I-21
+ name: "GPX-трек не теряется при переключении"
+ input: "Загружен GPX; applyBaseLayer('satellite')"
+ expected: "Layer gpx-* существует, source.data не изменён"
+
+ - id: I-22
+ name: "Recon-круг не теряется при переключении"
+ input: "Активен recon; applyBaseLayer('satellite')"
+ expected: "Recon-круг отображается на карте"
+
+ - id: I-23
+ name: "Hillshade поверх спутника"
+ input: "applyBaseLayer('satellite'); включить hillshade"
+ expected: "Оба слоя видимы; hillshade выше satellite-base в стиле"
+
+ - id: I-24
+ name: "POI halo чёрный на спутнике"
+ input: "applyBaseLayer('satellite')"
+ expected: "map.getPaintProperty('poi-labels','text-halo-color') === '#000000' (или эквивалент)"
+
+ - id: I-25
+ name: "POI halo дефолтный на схеме"
+ input: "applyBaseLayer('schematic') после спутника"
+ expected: "POI labels вернули halo цвет схемы (#ffffff)"
+
+ - name: e2e-base-layer-workflow
+ type: e2e
+ description: "Полный пользовательский сценарий"
+ cases:
+ - id: E-01
+ name: "Открыть попап → включить спутник → сохранилось"
+ steps:
+ - "Открыть приложение (default — Схема)"
+ - "Нажать кнопку «Рельеф» в правой панели"
+ - "Убедиться: переключатель «Подложка» виден"
+ - "Нажать «Спутник»"
+ - "Убедиться: спутниковые тайлы загрузились"
+ - "Убедиться: атрибуция Esri видна"
+ - "Перезагрузить страницу"
+ - "Убедиться: после загрузки активен «Спутник»"
+
+ - id: E-02
+ name: "Переключение туда-обратно без потери маршрута"
+ steps:
+ - "Построить маршрут через OSRM (2 точки)"
+ - "Переключить на «Спутник»"
+ - "Убедиться: маршрут виден на спутнике, статистика сохранена"
+ - "Переключить на «Схема»"
+ - "Убедиться: маршрут виден на схеме, статистика та же"
+
+ - id: E-03
+ name: "Спутник + загрузка GPX"
+ steps:
+ - "Переключить на «Спутник»"
+ - "Загрузить GPX-файл"
+ - "Убедиться: трек отрисован поверх спутника"
+ - "Убедиться: цвет трека различим"
+
+ - id: E-04
+ name: "Спутник + смена темы"
+ steps:
+ - "Переключить на «Спутник»"
+ - "Переключить тёмную тему на светлую"
+ - "Дождаться idle"
+ - "Убедиться: подложка осталась «Спутник»"
+ - "Убедиться: все остальные слои восстановились"
+
+ - id: E-05
+ name: "Спутник + переключение единиц измерения"
+ steps:
+ - "Переключить на «Спутник»"
+ - "Открыть попап слоёв и переключить «мили»"
+ - "Убедиться: единицы переключились, подложка не сбросилась"
+
+ - id: E-06
+ name: "Спутник + hillshade"
+ steps:
+ - "Переключить на «Спутник»"
+ - "Включить «Тени рельефа»"
+ - "Убедиться: видны спутник и тени одновременно"
+
+ - id: E-07
+ name: "Линейка на спутнике"
+ steps:
+ - "Переключить на «Спутник»"
+ - "Активировать линейку"
+ - "Поставить 3 точки на карте"
+ - "Убедиться: линия линейки видна на спутнике"
+ - "Убедиться: расстояния отображаются"
+
+ - name: e2e-error-handling
+ type: e2e
+ description: "Поведение при сетевых ошибках"
+ cases:
+ - id: E-10
+ name: "Спутниковые тайлы недоступны (offline)"
+ steps:
+ - "Включить «Спутник»"
+ - "Симулировать offline (DevTools throttling: Offline)"
+ - "Сдвинуть карту в новую область"
+ - "Убедиться: приложение не падает; видим фон background"
+ - "Восстановить сеть → тайлы догружаются"
+
+test_data:
+ - name: "test-track-simple.gpx"
+ description: "1 трек, 10 точек — для проверки совместимости с GPX"
+ - name: "Тестовый OSRM-маршрут"
+ description: "2 waypoint в районе [37.6,55.75] → [37.7,55.8]"
diff --git a/docs/work-items/ET-007/04b-ui-test-cases.md b/docs/work-items/ET-007/04b-ui-test-cases.md
new file mode 100644
index 0000000..497f857
--- /dev/null
+++ b/docs/work-items/ET-007/04b-ui-test-cases.md
@@ -0,0 +1,274 @@
+---
+type: ui-test-cases
+work_item_id: ET-007
+title: "UI Test Cases: Спутниковая карта (Схема / Спутник)"
+version: 1
+status: draft
+created_at: 2026-05-31
+updated_at: 2026-05-31
+authors:
+ - "agent:analyst"
+---
+
+# UI Test Cases — ET-007: Спутниковая карта (Схема / Спутник)
+
+Базовый URL: `https://openclaw.mva154.duckdns.org/enduro/`
+
+Все тесты проверяют появление и поведение переключателя «Подложка» в
+попапе слоёв, а также корректное отображение спутниковой подложки
+поверх существующих UI-элементов.
+
+---
+
+### TC-UI-01 — Переключатель «Подложка» виден в попапе
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. screenshot: "01-popup-with-base-toggle"
+6. check-visual: "В открывшемся попапе #terrain-popup видна строка «Подложка» с двумя кнопками: «Схема» (активна, оранжевый фон) и «Спутник» (неактивна)"
+
+---
+
+### TC-UI-02 — Активация «Спутник» меняет подложку
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. screenshot: "02-satellite-active"
+8. check-visual: "Карта показывает спутниковые снимки (зелёные/коричневые поля, реальный рельеф). В попапе кнопка «Спутник» подсвечена оранжевым, «Схема» — нет"
+
+---
+
+### TC-UI-03 — Атрибуция Esri видна
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. click: "#terrain-toggle"
+8. wait: 500
+9. screenshot: "03-attribution-esri"
+10. check-visual: "В правом нижнем углу карты видна атрибуция со словом «Esri» (или иконка info, при клике на которую разворачивается полный текст)"
+
+---
+
+### TC-UI-04 — Возврат на «Схема»
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. click: "#base-btn-schematic"
+8. wait: 2000
+9. screenshot: "04-schematic-restored"
+10. check-visual: "Карта снова показывает схему OSM (бежевый/серый фон, дороги). В попапе кнопка «Схема» подсвечена оранжевым"
+
+---
+
+### TC-UI-05 — Грунтовки и тропы видны на спутнике
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. screenshot: "05-trails-on-satellite"
+8. check-visual: "На спутниковой подложке отчётливо видны линии грунтовок (золотые/красные) и троп (красные пунктирные). Линии имеют светлую обводку (halo) для контраста с тёмным спутником"
+
+---
+
+### TC-UI-06 — POI и подписи на спутнике читаемы
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. screenshot: "06-poi-on-satellite"
+8. check-visual: "POI-маркеры (цветные кружки) видны на спутнике. Подписи POI имеют тёмный halo, читаемы на любом фоне"
+
+---
+
+### TC-UI-07 — Спутник переживает смену темы
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. click: "#btn-theme"
+8. wait: 3000
+9. screenshot: "07-satellite-after-theme-switch"
+10. check-visual: "После переключения темы карта по-прежнему показывает спутниковую подложку (а не схему)"
+
+---
+
+### TC-UI-08 — Hillshade поверх спутника
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. click: "#terrain-hillshade-cb"
+8. wait: 3000
+9. screenshot: "08-hillshade-on-satellite"
+10. check-visual: "Виден спутник + затенение рельефа поверх (тёмные тени по склонам, рельеф «выпуклый»). Слои не перекрывают друг друга полностью"
+
+---
+
+### TC-UI-09 — Маршрут OSRM на спутнике
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#tb-route"
+4. wait: 1000
+5. click: "#map"
+6. wait: 2000
+7. scroll: 100
+8. click: "#map"
+9. wait: 5000
+10. click: "#terrain-toggle"
+11. wait: 500
+12. click: "#base-btn-satellite"
+13. wait: 5000
+14. screenshot: "09-route-on-satellite"
+15. check-visual: "Маршрут (синяя/оранжевая линия) виден поверх спутниковой подложки, конечные точки маршрута отмечены маркерами"
+
+---
+
+### TC-UI-10 — Переключатель на мобильном
+
+- тип: ui
+- viewport: mobile
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. screenshot: "10-popup-mobile"
+6. check-visual: "На мобильном viewport попап #terrain-popup помещается на экране целиком. Переключатель «Подложка» виден, обе кнопки нажимаемы, не перекрывают другие элементы попапа"
+
+---
+
+### TC-UI-11 — Активация «Спутник» на мобильном
+
+- тип: ui
+- viewport: mobile
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. screenshot: "11-satellite-mobile"
+8. check-visual: "Спутниковая подложка отображается на мобильном устройстве. Тулбар внизу и попап работают корректно, переключатель «Спутник» подсвечен"
+
+---
+
+### TC-UI-12 — Persistence: спутник после перезагрузки
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. navigate: https://openclaw.mva154.duckdns.org/enduro/
+8. wait: 5000
+9. screenshot: "12-satellite-after-reload"
+10. check-visual: "После перезагрузки карта сразу открывается со спутниковой подложкой (не со схемой). Активный режим — «Спутник»"
+
+---
+
+### TC-UI-13 — GPX-панель + Спутник
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. click: "#tb-gpx"
+8. wait: 1000
+9. screenshot: "13-gpx-sheet-on-satellite"
+10. check-visual: "Открылась панель #sheet-gpx с пустым состоянием поверх спутниковой карты. Панель и подложка визуально не конфликтуют"
+
+---
+
+### TC-UI-14 — Совместимость с переключателем единиц
+
+- тип: ui
+- viewport: desktop
+
+шаги:
+1. navigate: https://openclaw.mva154.duckdns.org/enduro/
+2. wait: 5000
+3. click: "#terrain-toggle"
+4. wait: 500
+5. click: "#base-btn-satellite"
+6. wait: 5000
+7. click: "#unit-btn-mi"
+8. wait: 1000
+9. screenshot: "14-satellite-with-miles"
+10. check-visual: "Подложка остаётся «Спутник», единицы переключены на мили (кнопка «мили» подсвечена). Оба переключателя видны и работают независимо"
diff --git a/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md b/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md
new file mode 100644
index 0000000..e40afad
--- /dev/null
+++ b/docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md
@@ -0,0 +1,370 @@
+---
+type: adr
+work_item_id: ET-007
+adr_id: ADR-004
+title: "ADR-004: Спутниковая подложка — провайдер Esri World Imagery, лениво-добавляемый raster-source, гибридная стратегия halo"
+status: accepted
+created_at: 2026-05-31
+authors:
+ - "agent:architect"
+supersedes: []
+superseded_by: []
+labels: []
+---
+
+# ADR-004 — Спутниковая подложка: провайдер, размещение кода, схема восстановления
+
+## Статус
+
+Accepted
+
+## Контекст
+
+ET-007 вводит вторую базовую подложку карты — спутниковые растровые
+снимки — с переключателем «Схема / Спутник» в попапе слоёв
+(см. `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`).
+
+Существующее состояние, проверенное в коде:
+
+- В обоих стилях карты (`src/web/style.json` стр. 16–41,
+ `src/web/style-dark.json`) уже определён единственный raster-source
+ `osm-raster` и слой `osm-base`, лежащий поверх слоя `background`.
+ Тайлы OSM раздаются `https://tile.openstreetmap.org/{z}/{x}/{y}.png`
+ — то есть прецедент **внешней (кросс-оригинальной) тайл-зависимости с
+ атрибуцией без API-ключа уже существует**.
+- `src/web/app.js` (3 132 строки) содержит функцию `rebuildMapOverlays()`
+ (стр. 127), которая последовательно вызывает `restoreTerrainState()`,
+ `restoreTrailsState()`, `restorePoiState()`, перерисовку маршрутов /
+ GPX / линейки. Эта функция — единственная точка восстановления
+ визуальных слоёв после `map.setStyle()` (переключение тёмной/светлой
+ темы, `switchMapStyle()` стр. 100–117).
+- Фронтенд плоский, без сборщика: `index.html`, `app.js`, `units.js`
+ (190 строк, ADR-0001), `gpx.js` (1 242 строки, ADR-002). Сложившийся
+ паттерн — «одна крупная фича = один классический скрипт + глобали»
+ (ADR-002). Все JS-функции глобальные, обработчики навешаны через
+ инлайновые `onclick`.
+- Динамические мутации слоёв через `setPaintProperty` /
+ `setLayoutProperty` / `addSource` / `addLayer` в `app.js` уже широко
+ используются (~30 вхождений).
+- В `app.js` уже есть зрелые «restore*State()»-функции для каждой
+ группы слоёв; ET-007 встраивается в этот же паттерн ещё одной такой
+ функцией `restoreBaseLayerState()`.
+
+Решения, которые предстоит зафиксировать архитектурно:
+
+1. Какого провайдера спутниковых тайлов выбрать.
+2. Где разместить код переключателя — в `app.js` или в новом модуле.
+3. Как именно добавлять спутниковый source/layer (заранее в `style.json`
+ или лениво из JS), и как переживать `map.setStyle()`.
+4. Каким способом обеспечивать читаемость линий грунтовок/троп и POI
+ на тёмной спутниковой подложке (halo).
+5. Классификацию изменения и нужна ли эскалация `arch:major-change`.
+
+## Рассмотренные варианты
+
+### Вариант P (провайдер) — выбор провайдера спутниковых тайлов
+
+| Провайдер | API-ключ | Лицензия / условия | Покрытие | Решение |
+|---|---|---|---|---|
+| **Esri World Imagery** (`server.arcgisonline.com/.../World_Imagery/MapServer/tile/{z}/{y}/{x}`) | нет | Условия Esri ArcGIS Online: бесплатное использование с атрибуцией для некоммерческой и demo-разработки; широко применяется open-source-проектами (Leaflet, OpenLayers, QGIS) | глобальное, до z19 | **выбран** |
+| Mapbox Satellite | требуется | бесплатный квот-лимит, далее платно | глобальное | отклонён — BRD F-02 явно требует «без API-ключа» |
+| Bing Maps | требуется | сложная лицензия, обязательная регистрация | глобальное | отклонён — то же |
+| Google Maps Tiles | требуется | прямо запрещён ToS для нативного встраивания не через Google Maps JS API | глобальное | отклонён |
+| OpenAerialMap | нет | open-source, CC-BY | **фрагментарное**, нет глобального бесшовного слоя | отклонён — не покрывает РФ-эндуро-сценарии |
+| MapTiler Satellite | требуется | бесплатный квот-лимит | глобальное | отклонён — API-ключ |
+
+Esri World Imagery — единственный вариант, удовлетворяющий
+**одновременно** трём ограничениям BRD: без API-ключа, с глобальным
+покрытием, с лицензионно допустимой формой использования через
+атрибуцию.
+
+### Вариант M (модуль) — где разместить код
+
+- **M-A — добавить в `app.js`** (выбран). +~150 строк
+ (`onBaseLayerToggle`, `applyBaseLayer`, `restoreBaseLayerState`,
+ `syncBaseLayerUI`, плюс хук в `rebuildMapOverlays()` и handler
+ `onclick` в `index.html`). Минимальный blast radius, никаких новых
+ файлов, никаких изменений в подключении скриптов.
+- **M-B — выделить `src/web/basemap.js`** (по аналогии с ADR-002 для
+ GPX). Отклонён: ADR-002 разделил фичу, потому что её объём был
+ 600–900 строк и она имела собственную модель данных (`gpxTracks`),
+ собственный bottom sheet и собственный canvas. Здесь фича плоская и
+ объём в 5–7 раз меньше; разделение даёт чистоту, но не покрывает
+ стоимости новой связки `app.js ↔ basemap.js` ради ~150 строк.
+ Контракт интеграции с `rebuildMapOverlays()` и так глобальный —
+ никакой инкапсуляции отдельный файл не добавит.
+
+### Вариант S (source) — как добавить спутниковый source/layer
+
+- **S-A — задекларировать source `satellite-raster` и слой
+ `satellite-base` (`visibility: none`) в обоих `style.json` /
+ `style-dark.json`**. Source активен всегда, тайлы не запрашиваются
+ до показа слоя. Плюс: восстановление после `setStyle()`
+ тривиально (`setLayoutProperty('satellite-base', 'visibility', ...)`).
+ Минус: `style.json` обоих тем нужно править симметрично; дрейф
+ значений между двумя стилями.
+- **S-B — лениво создавать source и layer из JS при первом включении
+ «Спутник»** (выбран, совпадает с TRZ §1 REQ-F-02). Плюс: `style.json`
+ не трогаем; ноль внешних запросов у пользователей, которые не
+ включают спутник; единая точка определения source — в `app.js`. После
+ `map.setStyle()` source и layer исчезают и переcоздаются вызовом
+ `restoreBaseLayerState()` из `rebuildMapOverlays()` — это та же
+ логика, что уже используется для terrain/trails/POI/GPX. Минус:
+ холодное переключение «Схема → Спутник» включает в себя `addSource`
+ + `addLayer` + сетевой запрос — но укладывается в НФТ 500 мс.
+
+### Вариант O (order) — порядок восстановления в `rebuildMapOverlays()`
+
+- **O-A — `restoreBaseLayerState()` вызывается ПЕРВЫМ**, до
+ `restoreTerrainState()` (выбран, совпадает с TRZ §5.5). Гарантирует
+ z-order: `background` → `satellite-base` → `osm-base` → terrain →
+ trails → POI → routes → GPX. terrain/trails/POI оказываются выше
+ спутника, маршрут/GPX — выше terrain.
+- **O-B — добавлять `satellite-base` с явным `beforeId` первого
+ trails-слоя**. Идемпотентно к порядку, но в `rebuildMapOverlays()`
+ моменты создания слоёв не атомарны (terrain/trails добавляются
+ асинхронно); использовать `beforeId` слоёв, которых ещё нет, нельзя.
+ Поэтому простой «вызвать первым» надёжнее.
+
+### Вариант H (halo) — обеспечение читаемости поверх спутника
+
+- **H-A — динамический `setPaintProperty` по всем затрагиваемым слоям**.
+ Все правки делаем из `applyBaseLayer()`; на «Схема» возвращаем
+ исходные значения. Минус: нужно где-то хранить «исходные» paint-
+ значения; при `map.setStyle()` они сбрасываются, что повышает риск
+ drift между двумя темами.
+- **H-B — отдельные «underlay»-слои с halo, `visibility: none` по
+ умолчанию, включаются на спутнике** + **`setPaintProperty` только
+ для POI text-halo** (выбран, совпадает с TRZ §1 REQ-F-04). Halo-линии
+ декларативны в `style.json` обеих тем — никакого «запомнить
+ исходное» не нужно, восстановление по `visibility`. Для POI label
+ правок одна (`text-halo-color`/`text-halo-width`) — её проще менять
+ динамически, чем заводить параллельные label-слои.
+- **H-C — толстая полупрозрачная белая обводка прямо в существующих
+ trails-слоях через `line-gap-width`**. Отклонён: ломает «Схему»
+ (там halo не нужен и портит вид светлой подложки).
+
+## Решение
+
+Принимается комбинация: **P-Esri + M-A + S-B + O-A + H-B**.
+
+1. **Провайдер — Esri World Imagery.** URL-шаблон, атрибуция и параметры
+ source — как в TRZ §4.1. HTTPS обязателен. Атрибуция строки —
+ `"Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community"`.
+ Альтернативные провайдеры не закладываются в код фичи; точка
+ расширения — единственный объект source-spec в `applyBaseLayer()`,
+ при будущей смене провайдера правка локализуется одним местом
+ (см. R-2 в `10-tech-risks.md`).
+
+2. **Код фичи живёт в `app.js`.** Никакой новый JS-файл не вводится.
+ Новые глобальные функции — `onBaseLayerToggle()`, `applyBaseLayer()`,
+ `restoreBaseLayerState()`, `syncBaseLayerUI()` — добавляются по
+ соседству с уже существующими `restoreTerrainState()` /
+ `restoreTrailsState()`. Если в будущей фазе появится потребность
+ (например, второй провайдер, гибридный режим, оффлайн-кэш) — фича
+ мигрирует в `src/web/basemap.js` без изменения публичного контракта
+ (имена функций глобальные и стабильные).
+
+3. **Source и layer добавляются лениво** при первом включении
+ «Спутник» через `addSource('satellite-raster', {...})` +
+ `addLayer({ id: 'satellite-base', ... })`. До этого момента
+ запросов к `server.arcgisonline.com` не происходит. Это важно с
+ точки зрения приватности: пользователи, которые никогда не
+ используют спутник, не светят свой IP на серверы Esri (см.
+ `10-tech-risks.md`, R-3).
+
+4. **Восстановление после `map.setStyle()` — через `rebuildMapOverlays()`.**
+ В функцию добавляется **первым** вызов
+ `if (typeof restoreBaseLayerState === 'function') restoreBaseLayerState();`
+ до `restoreTerrainState()`. Это гарантирует, что terrain и trails
+ окажутся выше спутника, без необходимости вычислять `beforeId`.
+ `restoreBaseLayerState()` идемпотентен: читает `localStorage` ключа
+ `map-base-layer` и применяет `applyBaseLayer()`.
+
+5. **Halo — гибридный подход:**
+
+ - Для **линий грунтовок и троп** в обоих `style.json` /
+ `style-dark.json` присутствуют парные «underlay»-слои
+ `trails-track-halo-satellite` и
+ `trails-path-bridleway-halo-satellite` (более широкая
+ полупрозрачная белая обводка, `layout.visibility = "none"`).
+ При входе в «Спутник» эти слои становятся видимыми; при возврате
+ на «Схему» — скрываются. Никаких runtime-правок paint не
+ требуется. Слоёв на каждую grade (`trails-grade1..5-halo-satellite`)
+ **не заводится**: дифференциация grade хранится внутри одного
+ `match`-выражения по `tracktype` в `trails-track`, halo единого
+ цвета/ширины накладывается на весь слой целиком — этого
+ достаточно для читаемости (под halo всё равно ляжет цветная
+ линия `trails-track`). Аналогично для троп — единый halo на весь
+ `trails-path-bridleway` (фильтр `highway in path/bridleway/footway`).
+ `trails-asphalt` halo не получает: он по умолчанию скрыт
+ (`visibility: none`, `line-opacity: 0`); если в будущей фазе
+ включится — добавится halo тем же паттерном.
+ - Для **POI labels** меняются динамически три свойства:
+ `text-color` (`#ffffff` на спутнике / baseline текущей темы на схеме —
+ `#333333` для light, `#e0e0e0` для dark), `text-halo-color`
+ (`#000000` на спутнике / baseline `#ffffff` для light,
+ `#1a1a2e` для dark на схеме), `text-halo-width` (`2` на спутнике
+ / baseline `1.5` для light, `2` для dark на схеме). Менять
+ **обе** пары (color + halo) необходимо: иначе тёмный baseline-
+ текст светлой темы (`#333333`) поверх чёрного halo не читается.
+ Baseline-значения известны и зафиксированы в Data §5; всегда
+ выставляем явные значения для обоих режимов.
+ - **POI circles** — обводка `circle-stroke-color: #ffffff` /
+ `circle-stroke-width: 2` динамически на спутнике, возврат к
+ baseline текущей темы из Data §5 на схеме (`#ffffff`/`1.5` light,
+ `#333333`/`1.5` dark).
+
+6. **Цвет `background`** в режиме «Спутник» меняется через
+ `setPaintProperty('background', 'background-color', '#2a2a2a')` —
+ **единая константа `#2a2a2a` для обеих тем** (тёмно-серый, чтобы
+ не «бликовал» под медленно подгружающимися спутниковыми плитками).
+ На обеих темах используется одно и то же значение; per-theme-
+ развилки нет (упрощает код и исключает рассинхрон). При возврате
+ на «Схему» восстанавливаются baseline-значения текущей темы —
+ `#f0ede6` (light, из `style.json`) и `#1a1a2e` (dark, из
+ `style-dark.json`; **не** `#1a1a1a` — это была ошибка в более
+ раннем черновике). Эти baseline-константы зафиксированы в
+ `applyBaseLayer()` и в `08-data-requirements.md` §5.
+
+7. **localStorage — ключ `map-base-layer`** (см. TRZ §4.3), значения
+ `"schematic"` / `"satellite"`, default `"schematic"`. Ключ
+ полностью обособлен от существующих UI-настроек
+ (`enduro-theme-mode`, `distance_unit`, `terrain-*`, `trails-*`,
+ `poi-visible`) — никаких миграций старых значений не требуется.
+
+8. **Контракт с существующим `toggleLayer('basemap')`
+ (`app.js:384–391`).** В коде уже есть отдельный пользовательский
+ выключатель «Базовая карта» (управляет `osm-base.visibility` и
+ `layerState.basemap`). ET-007 принимает паттерн **save & restore**
+ (см. TRZ §5.6): при входе в «Спутник» сохраняем `layerState.basemap`
+ в `_savedBasemapState` и принудительно скрываем `osm-base`; UI-кнопка
+ `#btn-basemap` скрывается через CSS-класс `.satellite-active` (чтобы
+ пользователь не пытался включить «гибрид»: out of scope BRD §3).
+ При возврате на «Схему» восстанавливаем `osm-base.visibility` из
+ сохранённого значения. На «Схеме» `toggleLayer('basemap')` работает
+ как раньше — ET-007 этот код не трогает.
+
+9. **Синхронизация halo-слоёв с пользовательскими чекбоксами
+ «Грунтовки» / «Тропы» (`app.js:2783–2826`).** В существующих
+ `onTrailsCheckbox()` / `restoreTrailsState()` управляется
+ видимость только `trails-track` и `trails-path-bridleway`. Halo-
+ underlay-слои сами по себе не отслеживаются; на спутнике это даёт
+ «фантом» halo при выключенной грунтовке/тропе. Решение (TRZ §5.7):
+ ввести единый хелпер `applyTrailHaloVisibility(trackOn, pathOn)`
+ и вызывать его из (а) `onTrailsCheckbox`, (б) `restoreTrailsState`,
+ (в) `applyBaseLayer('satellite' | 'schematic')`. Правило: halo
+ видим ⇔ `currentBaseLayer === 'satellite' AND checkbox === ON`.
+ POI отдельной синхронизации не требуют — paint-правки текста
+ привязаны к самим `poi-circles`/`poi-labels`, которые управляются
+ `layerState.poi` / `restorePoiState()`.
+
+8. **C4 / архитектурная диаграмма.** В репозитории нет файлов
+ `c4-*.mmd`; описание архитектуры — текстовое в
+ `docs/architecture/README.md`. Туда добавляется отдельный раздел
+ «Внешние тайл-провайдеры» с двумя строками: OSM (существующий)
+ и Esri World Imagery (новый, для подложки «Спутник»). Дополнительно
+ `docs/architecture/adr/README.md` пополняется записью ADR-004.
+
+## Последствия
+
+### Положительные
+
+- Изменения — **только в коде фронтенда** (`src/web/index.html`,
+ `src/web/app.js`, `src/web/app.css`, оба `style*.json`). Backend,
+ БД, OSRM, nginx, Docker-конфигурация — без изменений (см.
+ `07-infra-requirements.md`).
+- Лазерная локальность точки расширения: для смены провайдера
+ достаточно отредактировать один объект source-spec в `app.js`.
+- НФТ 500 мс выполнима: при холодном переключении расходы — это
+ единичные вызовы `addSource` + `addLayer` + первая сетевая загрузка
+ плитки z=текущий; последующие переключения мгновенные (только
+ `visibility`).
+- Пользователи, никогда не использующие «Спутник», не отправляют ни
+ одного запроса на серверы Esri — минимизация утечки данных по
+ умолчанию (см. R-3).
+- Существующая инфраструктура восстановления после `map.setStyle()`
+ переиспользуется без изменения её формы — единый паттерн для
+ terrain/trails/POI/GPX/base-layer.
+
+### Отрицательные / ограничения
+
+- **Зависимость от третьей стороны.** Сервис Esri может ввести
+ лимит / потребовать API-ключ / изменить URL. Митигация: точка
+ расширения в `applyBaseLayer()`; риск зафиксирован
+ (`10-tech-risks.md`, R-2).
+- **Утечка IP при использовании спутника.** При активном «Спутник»
+ IP пользователя становится виден Esri (так же, как сейчас он виден
+ tile.openstreetmap.org). Это **не регрессия приватности относительно
+ OSM**, но — расширение перечня третьих сторон, к которым клиент
+ обращается. Зафиксировано в `08-data-requirements.md` §5 и
+ `10-tech-risks.md` R-3.
+- **Корпоративные / анти-трекинг блокировки.** Часть пользователей
+ (корпсети, NextDNS-фильтры) могут блокировать `arcgisonline.com`.
+ Поведение в этом случае — MapLibre показывает прозрачные плитки
+ поверх `#2a2a2a` фона; пользователь сам переключится на «Схему».
+ Это нормальное поведение (TRZ §1 REQ-F-08); fallback на схему
+ автоматически — **не закладываем**.
+- **Halo-слои в `style.json` обоих тем.** Любые будущие правки
+ trails-слоёв требуют согласованной правки соответствующих
+ `*-halo-satellite` слоёв. Зафиксировано в `10-tech-risks.md` R-1.
+- **Background цвет.** В коде `applyBaseLayer()` появляется маленький
+ дубль констант фона по темам. При смене палитры тем — править здесь
+ тоже. Зафиксировано в `10-tech-risks.md` R-5.
+
+### Технический долг
+
+- Если позже появится потребность во **втором** провайдере (например,
+ для альтернативной геополитической юрисдикции) или в гибридном
+ режиме «Спутник + подписи дорог OSM поверх», логичный путь —
+ вынести фичу в `src/web/basemap.js` (ADR-002-стиль) и расширить
+ локальное состояние до `{ provider, hybrid }`. Имена глобальных
+ функций (`onBaseLayerToggle`, `restoreBaseLayerState`) остаются
+ стабильным контрактом — `index.html` и `app.js` не меняются.
+- Если в проект введут CSP-заголовок (сейчас его нет, см. ET-006
+ `07-infra-requirements.md` §4), для спутника потребуется
+ `img-src 'self' https://*.openstreetmap.org https://server.arcgisonline.com data:;`.
+
+## Классификация изменения
+
+**Minor change.** Новых контейнеров, сервисов, БД, серверных API
+ET-007 не вводит. Внешний тайл-провайдер — расширение уже
+существующего класса зависимостей (OSM-tile), а не новый
+архитектурный класс. Лейбл `arch:major-change` **не требуется**.
+Обязательного дополнительного архитектурного approve не требуется.
+
+## Ревизии
+
+- 2026-05-31 — editorial: code-review fixes (12-review.md attempt 2/3).
+ Решения P/M/S/O/H **не пересматривались**. Правки:
+ - §5 пункт 1: реальные id halo-слоёв
+ (`trails-track-halo-satellite`, `trails-path-bridleway-halo-satellite`)
+ вместо несуществующих `trails-grade1..5-halo-satellite` /
+ `paths-bridleway-halo-satellite` (P1-1).
+ - §5 пункт 2 (POI labels): добавлена правка `text-color` на
+ спутнике + явный baseline возврата per-theme — без этого тёмный
+ `#333333` поверх чёрного halo был нечитаем (P1-2).
+ - §6: зафиксирована единая satellite-константа `#2a2a2a` для обеих
+ тем; baseline dark исправлен `#1a1a1a` → `#1a1a2e` под фактическое
+ значение `style-dark.json:28` (P1-4).
+ - Добавлен §8: контракт с существующим `toggleLayer('basemap')` /
+ `layerState.basemap` — паттерн save&restore через
+ `_savedBasemapState` (P1-5).
+ - Добавлен §9: синхронизация halo-слоёв с пользовательскими
+ чекбоксами «Грунтовки»/«Тропы» — хелпер
+ `applyTrailHaloVisibility` (P1-6).
+
+## Связанные документы
+
+- `docs/work-items/ET-007/01-brd.md`
+- `docs/work-items/ET-007/02-trz.md`
+- `docs/work-items/ET-007/03-acceptance-criteria.md`
+- `docs/work-items/ET-007/04-test-plan.yaml`
+- `docs/work-items/ET-007/04b-ui-test-cases.md`
+- `docs/work-items/ET-007/07-infra-requirements.md`
+- `docs/work-items/ET-007/08-data-requirements.md`
+- `docs/work-items/ET-007/10-tech-risks.md`
+- `docs/architecture/README.md`
+- `docs/architecture/adr/README.md`
+- ADR-0001 (ET-005) — паттерн классических скриптов
+- ADR-002 (ET-006) — «одна фича = один скрипт + глобали»
diff --git a/docs/work-items/ET-007/07-infra-requirements.md b/docs/work-items/ET-007/07-infra-requirements.md
new file mode 100644
index 0000000..352b061
--- /dev/null
+++ b/docs/work-items/ET-007/07-infra-requirements.md
@@ -0,0 +1,163 @@
+---
+type: infra-requirements
+work_item_id: ET-007
+title: "Инфраструктурные требования — ET-007: Спутниковая карта (Схема / Спутник)"
+version: 1
+status: approved
+created_at: 2026-05-31
+authors:
+ - "agent:architect"
+---
+
+# Инфраструктурные требования — ET-007
+
+## 1. Резюме
+
+ET-007 — изменение **исключительно фронтенда**: `src/web/index.html`,
+`src/web/app.js`, `src/web/app.css`, `src/web/style.json`,
+`src/web/style-dark.json`. Новой инфраструктуры, новых контейнеров,
+новых портов и серверной конфигурации **не требуется**. Документ
+зафиксирован для полноты work-item и явно подтверждает отсутствие
+инфра-воздействия (см. `06-adr/ADR-004-satellite-base-layer.md`).
+
+## 2. Контейнеры и сервисы
+
+| Аспект | Требование |
+|--------|------------|
+| Новые контейнеры | Нет |
+| Изменения существующих сервисов (api, osrm, nginx) | Нет |
+| Изменения `docker-compose.yml` | Нет |
+| Изменения `Dockerfile` | Нет — все правки попадают в образ через уже существующий `COPY src/web/ ./src/web/` |
+| Изменения подключения скриптов в `index.html` | Нет новых `