## 1. Код

- [x] 1.1 `internal/tgbot/render.go`: добавить хелпер `esc` (экранирование
  внешнего текста для HTML parse mode Telegram) и `idCode` (обёртка download id в
  `<code>` для tap-to-copy)
- [x] 1.2 `internal/tgbot/bot.go`: в `send()` выставить `ParseMode = HTML`; в
  `refreshCard()` выставить `ParseMode = HTML` на edit-конфиге (оба варианта:
  с клавиатурой и без)
- [x] 1.3 Заменить вывод download id на `idCode(id)` во всех сообщениях
  (`bot.go`: pending/принято/дубль/refine/`opErr`; `render.go`: карточки, дефолтная
  ветка `renderCard`, распознаю/раскладываю, done/failed, **`renderDesync`**)
- [x] 1.4 Экранировать все внешние фрагменты в `render.go` через `esc`: display
  name / распознанное название, источник/контекст, путь плана, причины,
  provider **и `provider_id`**, `error_code`, `error_msg`. Не забыть
  **`renderDesync`** (внешний `displayTitle` уходит в `send` с HTML).
  **Порядок:** `esc` применяем ПОСЛЕДНИМ шагом — над уже усечённым текстом
  (после `shorten`/`tailPath`/`firstLine`), иначе обрез посреди сущности
  `&lt;` даст битую разметку → Telegram 400 → сообщение не доставится.

## 2. Тесты

- [x] 2.1 `internal/tgbot/bot_test.go`: обновить ожидания текста под `<code>`-id
- [x] 2.2 Добавить тест: внешний фрагмент со спецсимволами (`<`/`>`/`&`) в
  уведомлении экранируется (не ломает разметку) — покрыть desync/failed-путь и
  кейс усечения длинного значения со спецсимволом у границы `shorten`

## 3. Спека

- [x] 3.1 Дельта `notifications` (два ADDED-требования); `openspec validate
  --strict telegram-download-id-code`
