diff --git a/docs/specs/039-large-backdrops.md b/docs/specs/039-large-backdrops.md
index 4f24ec3c..7d349a40 100644
--- a/docs/specs/039-large-backdrops.md
+++ b/docs/specs/039-large-backdrops.md
@@ -1,58 +1,137 @@
# ТЗ #39 — Безопасная работа с большими подложками
- Issue: https://github.com/Matysh/houseplan-card/issues/39
-- Приоритет: P2
-- Статус ТЗ: research-first, thresholds утверждаются по benchmark
+- Приоритет: P3 (триаж владельца 2026-08-15), полный трек
+- Ревизия: 2 (2026-08-29) — research-фаза проведена, константы предложены по бенчмарку
## Цель
-До upload предупреждать о raster, способном исчерпать память tablet/WebView,
-и предлагать уменьшенную копию без потери текущего плана при ошибке.
+До каких-либо тяжёлых аллокаций предупреждать о растре, способном исчерпать
+память планшета/WebView, и предлагать уменьшенную копию — не трогая текущий
+план и оригинал файла при любой ошибке.
-## Локальная диагностика
+## Текущее состояние (анкеры кода)
-Для PNG/JPEG/WebP до сетевого запроса:
+`_pickPlanFile` (`houseplan-editor-runtime.ts:8243`) принимает PNG/JPEG/WebP/SVG
+по MIME/расширению и БЕЗ какой-либо диагностики: (1) весь файл конвертируется в
+base64 ручным循 циклом `String.fromCharCode` — пик памяти ≈3.7× размера файла
+ещё до всякого предупреждения; (2) разрешение не проверяется вовсе; (3) aspect
+берётся через `
` — то есть полный decode УЖЕ происходит на этом шаге.
+Сервер режет только байты: `MAX_FILE_BYTES` 50 МБ (`http_api.py:252`) и квота
+(`plans.py:216`). Транзакция уже честная: staging не трогает конфиг до
+успешного save (комментарий `runtime:8365-8380`), Undo и явное удаление живут
+отдельно — эта часть issue выполняется существующим кодом и закрепляется тестом.
-- MIME/signature и file bytes;
-- decoded width×height через `createImageBitmap`, fallback isolated `
`;
-- megapixels и minimum decoded RGBA memory `w*h*4`;
-- estimated working peak: bitmap + canvas + encoded output (conservative 2.5×);
-- рекомендуемый target dimensions.
+## Research: бенчмарк-матрица (проведено 2026-08-29)
-Object URLs всегда revoke; decode timeout/error не заменяет текущий backdrop.
-SVG получает file-size/security validation, но не raster memory/downscale.
+Desktop headless Chromium 139 (песочница CI), генерация PNG в OffscreenCanvas,
+decode `createImageBitmap`, даунскейл до 4096px + JPEG q0.9:
-## Threshold protocol
+| MP | px | файл PNG | RGBA | decode | downscale |
+|---|---|---|---|---|---|
+| 4 | 2000² | 3.6 МБ | 15 МБ | 39 мс | 21 мс |
+| 16 | 4000² | 12.7 МБ | 61 МБ | 118 мс | 74 мс |
+| 32 | 5657² | 22.9 МБ | 122 МБ | 235 мс | 102 мс |
+| 32+alpha | 5657² | 25.9 МБ | 122 МБ | 247 мс | 101 мс |
+| 64 | 8000² | 40.3 МБ | 244 МБ | 395 мс | 108 мс |
+| 100 | 10000² | 58.1 МБ | 381 МБ | 589 мс | 144 мс |
+| 165 | 12845² | 87.3 МБ | 629 МБ | 956 мс | 130 мс |
-Перед code default провести matrix на reference low-end wall tablet WebView и
-desktop Chromium: 4/8/16/32/64 MP, transparency, JPEG/WebP. Ship constants
-документируются как `WARN_DECODED_BYTES` и `MAX_SAFE_DIMENSION`. Рекомендуемый
-начальный guard для исследования: warning при >32 MP либо estimated peak
->128 MiB; это не становится product constant без результатов.
+Decode линеен (~6 мс/МП), даунскейл дешёв (<150 мс из любого размера) — окно
+«предупредить и уменьшить» практично. Скрипт кладётся в
+`demo/benchmark_backdrop_decode.mjs` для повторной калибровки.
+
+**Честная оговорка протокола**: reference low-end wall tablet в среде
+недоступен; десктоп decode не показывает OOM вовсе (165 МП ок). Константы
+берутся консервативно от известных лимитов WebView/WebKit (Android WebView
+renderer OOM в сотнях МБ decoded; лимит стороны canvas Chromium/WebKit
+16384px), собраны В ОДНОМ модуле и печатаются в предупреждении — полевая
+рекалибровка после жалоб = правка одного файла. Пороги смягчены в сторону
+«предупреждаем раньше»: цена ложного срабатывания — один необязательный диалог.
+
+## Константы (`src/backdrop-probe.ts`, единственный источник)
+
+- `WARN_DECODED_BYTES = 128 МиБ` (≈32 МП RGBA) — порог предупреждения;
+- `HARD_DIMENSION = 16384` px по стороне — потолок канваса браузеров, дальше
+ только Cancel;
+- `DOWNSCALE_TARGET_PX = 4096` px длинная сторона (хватает 4K-дисплею плана);
+- `DOWNSCALE_JPEG_QUALITY = 0.9`.
+
+## Диагностика: заголовки, не decode
+
+Ключевое отличие от ревизии 1: разрешение читается **парсером заголовков**
+(PNG IHDR + colour type/tRNS → alpha; JPEG SOF0/2; WebP VP8/VP8L/VP8X + флаг
+alpha) — чистый модуль `backdrop-probe.ts`, без ImageBitmap, canvas и сетевых
+запросов. Никакая аллокация размером с изображение не происходит до решения
+пользователя. `probeBackdrop(bytes, mime)` → `{kind: 'safe'|'warn'|'hard'|
+'unknown', width, height, alpha, decodedBytes}`; `unknown` (битый/усечённый
+заголовок при растровом MIME) ведёт себя как `warn` без чисел разрешения —
+fail-closed к диалогу, не к тихому продолжению. SVG в probe не заходит:
+прежняя валидация типа/размера, растеризации нет (как сейчас).
## UX
-- Safe: обычный upload.
-- Warning: dialog показывает resolution, file size, estimated memory и действия
- «Загрузить уменьшенную копию», «Оставить оригинал», Cancel.
-- Hard browser limit/decode failure: только Cancel и инструкция уменьшить файл
- desktop tool; нельзя продолжать blind.
-- Downscale сохраняет aspect; longest side до benchmark target. PNG с alpha
- остаётся PNG, opaque photo — JPEG/WebP с documented quality; metadata не
- требуется.
+- `safe` — прежний путь (плюс замена ручного base64-цикла на
+ `FileReader.readAsDataURL`: минус ~2× пикового JS-heap на любом файле).
+- `warn` — диалог (`hp-dialog`, паттерн существующих подтверждений): разрешение,
+ размер файла, оценка decoded-памяти; действия «Загрузить уменьшенную копию»
+ (основное), «Оставить оригинал», «Отмена». Числа — из probe, до decode.
+- `hard` (сторона > 16384, decode-ошибка или таймаут 10 с на этапе
+ уменьшения) — только «Отмена» + совет уменьшить файл на десктопе; текущая
+ подложка и staging не меняются.
+- Уменьшение: `createImageBitmap(file, {imageOrientation: 'from-image'})` (EXIF
+ учтён) → OffscreenCanvas (fallback `