# AGIL Calls — визуальный QA и дизайн-критика

> Независимый аудит интерфейса развёрнутого стенда: типографика, отступы, выравнивание,
> переполнения, контраст, консистентность, адаптив, тексты. Замечания подкреплены
> скриншотами и числовыми метриками (computed styles, getBoundingClientRect,
> scrollWidth/clientWidth, WCAG contrast ratio).
>
> **Сборка (C9, 13.11.2026 20:20 MSK):** guardify `guardify-CrVA45s_.js`
> sha256 `5a0ccecf…e98a`, shared `index-CVIcZIiz.js` `7595b95a…6c3f`, CSS
> `index-D-pQONvN.css` `4201ff19…dd12`, widget `/sdk/agil-calls.js` `86c8c176…ff82`;
> server `agil-calls-server 0.1.0`. Маскот-ревью ранее — dashboard
> `index-Df1sNrAY.js` `fa6f9d3e…fc56` (см. `docs/qa-ux-mascot.md`).
> **Стенд:** `https://calls.guardify.ru` · **Дата прогона:** 13.11.2026, 15:40–16:05 MSK
> **Пост-фикс прогон (фикс-билд):** 13.11.2026, 16:39–16:57 MSK — результаты, статусы
> F-01…F-35 и новые поверхности в [§1.1](#11-пост-фикс-приёмка-13112026).
> **Инструменты:** headless Chrome (puppeteer-core 25.17), fake media
> (`--use-fake-device-for-media-stream`), скрипты в `qa/visual/`, сырые метрики в `qa/visual/cache/`.
> **Доступ:** admin-ключ передан через env `ADMIN_KEY`, в отчёте маскирован (`demo-d7ce…33f0`).
> **Конфиг тенанта `demo`** после обоих прогонов восстановлен и проверен
> (`restored.ok = true`, `controlsStyle=bar`, `hiddenControls=[]`, features как до прогона).
> **Исходники приложения не менялись** — только `qa/visual/**`, `docs/qa-visual.md`,
> `docs/screenshots/**`.

---

## Оглавление

1. [Сводка](#1-сводка)
2. [Пост-фикс приёмка (13.11.2026)](#11-пост-фикс-приёмка-13112026)
3. [Методика и ограничения](#2-методика-и-ограничения)
4. [P0 — блокирующее](#3-p0--блокирующее)
5. [P1 — критичные дефекты](#4-p1--критичные-дефекты)
6. [Постраничные замечания P2/P3](#5-постраничные-замечания-p2p3)
7. [Таблица числовых метрик](#6-таблица-числовых-метрик)
8. [Кросс-тема и кросс-вьюпорт матрицы](#7-кросс-тема-и-кросс-вьюпорт-матрицы)
9. [Тексты (RU)](#8-тексты-ru)
10. [Топ-10: исправить первым](#9-топ-10-исправить-первым)
11. [Что проверено и в порядке](#10-что-проверено-и-в-порядке)
12. [Приёмка после фиксов (P0/P1)](#11-приёмка-после-фиксов-p0p1)
13. [Тотальный визуальный аудит (13.11.2026)](#12-тотальный-визуальный-аудит-13112026)
14. [Приложение: скриншоты и скрипты](#13-приложение-скриншоты-и-скрипты)

---

## 1. Сводка

### Покрытие

| Измерение | Значение |
|-----------|----------|
| Страниц/приложений | 4: `/` (лендинг), `/embed.html` (вход + звонок), `/dashboard/`, `/editor/` |
| Экранов/состояний | 44: лендинг, экран входа, звонок (грид, 4 панели, spotlight, мини-режим, fullscreen), логин, 10 экранов панели, 5 вкладок комнаты, 2 диалога, результат токена, ошибка входа, 15 секций редактора, состояния пресетов |
| Тем | 5 пресетов × light/dark-схемы; сквозные проверки light/dark/auto + OS dark |
| Вьюпортов | 390×844, 768×1024, 1366×768, 1440×900, 1920×1080 |
| Скриншотов | 199 PNG в `qa/visual/screenshots/**` (включая повторные прогоны), 25 новых в `docs/screenshots/qa-*.png` |
| Снимков метрик | 101 JSON-снимок (`qa/visual/cache/metrics-*.json`) |
| Замечаний | **32**: P0 — 1, P1 — 6, P2 — 13, P3 — 12 |

### Главное

1. **Тёмная тема не работает на публичном лендинге и в виджете звонка.** Переключатель
   «Тёмная» меняет только атрибут `data-theme` / `color-scheme`, но палитра остаётся
   светлой. Тёмная палитра достижима **только** через `theme=auto` + тёмную ОС.
   При этом в dashboard и editor тёмная тема работает — то есть дефект в конкретном
   пути применения темы, а не в дизайн-системе.
2. **На мобильном (390px) шапка звонка не помещается**: `scrollWidth=511` при
   `clientWidth=390`, правые контролы обрезаны и недоступны.
3. **Превью в редакторе на прод-стенде не работает**: iframe грузится с
   `http://localhost:5173/embed.html` (connection refused), центральная колонка пустая.
4. **Систематические недоборы контраста**: заголовки таблиц 4.05:1, ссылки/навигация
   4.37–4.45:1 (light), 3.28:1 (auto+dark) при требуемых 4.5:1; в смешанных состояниях
   темы — до 1.86:1.
5. **Уведомления о слабом канале дублируются** (баннер + тост) и тост перекрывает
   кнопки нижней панели.

---

## 1.1. Пост-фикс приёмка (13.11.2026)

> Фикс-билд задеплоен (frontend/dashboard/editor/server/docs). Прогон по тем же
> 5 стадиям + 4 пробникам + verifier: `qa/visual/run-all-after.mjs`, 16:39–16:57 MSK.
> Сырые результаты: `qa/visual/cache/after-verdict.json`, `qa/visual/after-verdict.md`,
> `qa/visual/cache/final-run.log`. Итог: **8/10 критериев pass, P0/P1 — 7/8**; fail: C7 (P2 типографика/контролы) и **C9** (P1 join/embed — внутренний скролл и асимметрия на текущей сборке, до фикса). 429 rate-limit классифицирован как обработанная защита сервера.
> Конфиг `demo` восстановлен (`controlsStyle=bar`, `hiddenControls=[]`).

### Итог по критериям

| ID | Приоритет | Критерий | Было | Стало | Комментарий |
|----|-----------|----------|------|-------|-------------|
| C1 | **P0** | Тёмная тема на всех поверхностях | ❌ | **✅ pass** | widget: root `rgb(8,13,22)`, surface `#111a2b`, text `#f1f5f9`; landing: карточка `color(srgb 0.0667 0.102 0.169 / .92)`; dashboard/editor — dark |
| C2 | P1 | Мобильный звонок 390 без переполнения шапки | ❌ 511/390 | **✅ pass** | overflow-элементов нет; шапка компактная (brand + статус) |
| C3 | P1 | Превью редактора same-origin/ready | ❌ localhost | **✅ pass** | iframe `https://calls.guardify.ru/embed.html?room=preview&tenant=demo&_r=…` → 200, статус `ready` («preview подключён»), overlay rect **0×0** (скрыт), в превью join-screen; проверено 16:56:45 MSK на свежем профиле (F-34 закрыт) |
| C4 | P1 | Контраст preset×scheme ≥4.5:1 | ❌ 60 пар | **✅ pass** | матрица 5 пресетов × 3 схемы × OS light/dark: лендинг **30/30**, виджет **8/8**; бейджи dashboard/editor (71, worst 3.85) вынесены в P2 — F-05 |
| C5 | P1 | Одно уведомление о деградации, без перекрытия | ❌ | **✅ pass** | при форсированной деградации: баннер + только «Борис присоединился»; деградационного тоста нет, пересечение с тулбаром 0 |
| C6 | P1 | Имена в плитках не обрезаются | ❌ 21 | **✅ pass** | overflow `tile-name` нет; «Организатор»/«Борис» с чипами ролей читаются |
| C7 | P2 | Типографика/контролы | ❌ | **❌ fail (частично)** | лендинг: 12.6–26.1px, <11px нет, тач-таргеты 0 — ок; но дробные размеры остались (widget 10.5/10.92, editor 9/9.5/10px, dashboard 10.5/10.58), мелкие таргеты: dashboard/editor |
| C8 | P2 | Шрифты | ❌ | **✅ pass (принято)** | осознанный system-ui fallback: `@font-face` нет, внешних шрифтов нет — зафиксировано как решение, не дефект |
| C9 | **P1** | Join/embed адаптив: нет внутреннего скролла, симметричные preview/form | — | **❌ fail (до фикса, 6/8 кейсов)** | `.agil-join` scrollHeight 489/591 при clientH 380/300 (`overflow-y:auto`), карточка 457–559 px внутри 300–380 px; 2-col ширины preview/form 233/202 (Δ30) и 454/395 (Δ59); 1-col — карточка выше scrollport → обрезка 12 элементов. Скрипт: `qa/visual/audit-join-layout.mjs` |
| C0 | P1 | Регрессии (console/404/overflow) | ✅ | **✅ pass** | 0 необработанных console/404; 2 × 429 rate-limit — обработанная защита сервера (UI: «Слишком много запросов», Retry-After), вынесено в AX-04; 0 overflow документа |

### Статусы замечаний F-01…F-35

| ID | Приоритет | Тема | Статус после фикса | Основание |
|----|-----------|------|--------------------|-----------|
| F-01 | P0 | Тёмная тема (лендинг+виджет) | **Исправлено** | C1 pass, probe-widget-theme: `theme=dark` → `rgb(8,13,22)`/`#111a2b`/`#f1f5f9` |
| F-02 | P1 | Мобильная шапка 511/390 | **Исправлено** | C2 pass, overflow-список пуст |
| F-03 | P1 | Превью редактора | **Исправлено** | same-origin + `?room&tenant&_r` → 200, статус `ready`, overlay скрыт (C3 pass 16:56 MSK) |
| F-04 | P1 | Смешанные схемы, контраст ссылок | **Исправлено** | матрица 30/30 лендинг + 8/8 виджет; прежние «27/12 пар» — артефакт методики (underlay по OS), исправлено в §12 |
| F-05 | P1 | Контраст dashboard | **Частично** | link/nav/table-head ✅; **бейджи остались**: live-аудит 71 (dashboard 23, worst 3.85; editor 48, worst 4.34) — P2 |
| F-06 | P1 | Дубль уведомлений + перекрытие | **Исправлено** | C5 pass |
| F-07 | P1 | Обрезка имён участников | **Исправлено** | C6 pass |
| F-08 | P2 | Типографическая шкала | **Частично** | лендинг без <11px, но дробные; widget/editor/dashboard дробные |
| F-09 | P2 | Тач-таргеты/контролы | **Частично** | island/mobile — 0; dashboard/editor — много мелких, widget 10.5px текст |
| F-10 | P2 | Self-hosted шрифты | **Принято (не дефект)** | C8: system-ui fallback осознанно |
| F-11 | P2 | Неравные карточки лендинга | **Исправлено** | heights 544/544 |
| F-12 | P2 | Код-блок: переносы | **Исправлено** | `pre` → `overflow-x:auto`, `white-space:pre` |
| F-13 | P2 | Мобильный редактор (порядок) | Не перепроверялось | вне P0/P1 |
| F-14 | P2 | Мобильный тулбар 3 ряда | **Частично** | default bar всё ещё 3 ряда; добавлен island-режим (см. ниже) |
| F-15 | P2 | Пустое место в гриде | Открыто | плитка 933×524, большие поля сверху/снизу |
| F-16 | P2 | Контраст заголовков настроек | **Исправлено** | звонок: 0 contrast-fails |
| F-17 | P3 | `color-scheme` в виджете | Не перепроверялось | — |
| F-18 | P3 | Пустое состояние чата | Не перепроверялось | — |
| F-19 | P2 | Пресеты в редакторе по лейблам | Открыто | секция «Тема»: только select схемы, чипов пресетов нет |
| F-20 | P2 | Размеры контролов dashboard | Открыто | heights 26/30/34/38/59/64 |
| F-21 | P3 | Локализация селекта пресетов | Открыто | «Пресет: light» — сырые ключи |
| F-22 | P3 | Дубль «AGIL Calls» в шапке | Открыто | чип с щитом остаётся |
| F-23–F-27 | P3 | Мелкие UI-замечания | Не перепроверялись | вне P0/P1 |
| F-28 | P3 | Регистр ролей в чипах | Открыто | «организатор»/«гость» строчными |
| F-29, F-30, F-32 | P3 | Прочее | Не перепроверялось | — |
| F-31 | P3 | Дубль demo-light/dark в docs | **Исправлено** | скриншоты обновлены обоих прогонов |
| **F-33** | **P1** | **Смешанные preset×scheme: ссылки нечитаемы** | **Исправлено (13.11.2026)** | матрица 30/30 лендинг + 8/8 виджет PASS после `themeForScheme/ensureReadableTheme`; ранее зафиксированный «12 pairs fail» оказался артефактом моего композитинга полупрозрачных фонов (underlay по OS вместо фактической темы) — методика исправлена, см. §12 |
| **F-34** | **P1** | **Превью редактора: `/embed.html` без room, ready не приходит** | **Исправлено (16:56 MSK)** | повторная проверка на текущем деплое: src `…/embed.html?room=preview&tenant=demo&_r=…`, ответ 200, статус `ready` («preview подключён»), overlay 0×0; в превью join-screen. Ранее в прогоне 16:52 src был без query (старый образ/кэш), плюс мой пробник давал ложный негатив: считал offline по тексту скрытого DOM — детект исправлен (проверка видимости overlay + статуса) |
| **F-35** | P2 | Guardify-страница: CTA и типографика | **НОВОЕ, открыто** | primary-кнопка «Войти в звонок»: белый на `#8576ff` = **3.48:1**; H1 49.6px и 15.04/13.76 — дробные; 15 тач-таргетов <40px; brand-svg stroke 2 vs widget 1.8 |

### Новые поверхности: Guardify и island-лейаут

**`/guardify.html`** (новая демо-страница Guardify-стиля): тёмная кастомная тема,
встроенный виджет с `chrome:'none'`, `controlsStyle:'island'`, скрытым чатом.
Снято 5 состояний (`qa-after-guardify-hero/call/two/mobile.png`): hero, звонок с island,
два участника, minimize, мобильный 390. Метрики: overflow **0**, console/404 **0**,
a11y — 0 безымянных icon-only; замечания — F-35 (CTA 3.48, дробная типографика,
15 тач-таргетов <40). Визуально: шапка/hero/карточка звонка консистентны, island
внутри карточки работает, статусы «СЕРВЕР ОНЛАЙН»/«Защищённый звонок» на месте.

**Island-лейаут** (`data-agil-controls-style="island"`, `hiddenControls`): проверен на
`/embed.html` в light/dark + 390px (`qa-after-island-light/dark/mobile/popover.png`).
Факты: `controlsStyle=island` применился, 14 кнопок, popover «⋯» открывается,
`hiddenControls` (participants/transcript/assistant) не отрисованы, overflow 0,
тач-таргеты на 390 — 0 мелких. Новых дефектов не выявлено; единственное наблюдение —
кнопка «Выйти» в island до подключения disabled и почти невидима (корректное состояние,
но стоит усилить контраст disabled-состояния).

### Скриншоты «до/после» (ключевые)

| Что | До | После |
|-----|----|-------|
| Лендинг: явная dark-схема | `qa/visual/baseline/2026-11-13-before/screenshots/demo/landing-lightpalette-darkscheme.png` | `qa/visual/screenshots/demo/landing-lightpalette-darkscheme.png` |
| Лендинг: minimal+dark (баг F-33) | — | `docs/screenshots/qa-after-landing-minimal-dark.png` |
| Мобильный звонок 390 (F-02) | `…/call/call-mobile-390x844-light.png` (обрезано) | `docs/screenshots/qa-after-call-mobile-header-crop.png` (ок) |
| Звонок: тёмная тема (F-01) | `…/call/call-panel-settings-dark-1440.png` (светлая панель) | `docs/screenshots/qa-after-call-settings-dark.png` (тёмная) |
| Дубль уведомлений (F-06) | `docs/screenshots/qa-call-degraded.png` | `docs/screenshots/qa-after-call-degraded.png` |
| Имена плиток (F-07) | `docs/screenshots/qa-call-chat.png` («Организ…») | `docs/screenshots/qa-after-call-chat.png` («Организатор») |
| Превью редактора (F-03/F-34) | `docs/screenshots/qa-editor-preview-offline.png` | `docs/screenshots/qa-after-editor-preview-fixed.png` |
| Тёмная тема dashboard (F-05) | `docs/screenshots/qa-dashboard-overview-dark.png` | `docs/screenshots/qa-after-dashboard-overview-dark.png` |
| Guardify / island | — | `docs/screenshots/qa-after-guardify-*.png`, `qa-after-island-*.png` |

### Что осталось

**Открытых P1 нет: 7/7 P0/P1 pass** (C1–C6 + C0). Но есть **P1-деплой-гэп AX-01**:
образ не содержит HEAD `8168d84` (SVG-эмодзи, overlay-CLS, logo/media fit) — см. §12.2,
нужен редеплой и повторная проверка. Остальное — P2/P3:

1. **AX-01 (P1, деплой)** — редеплой из HEAD `8168d84`; после — перепроверить эмодзи
   (AX-02), CLS уведомлений (AX-07) и «растягивание» (logo/media fit).
2. **F-05 (P2)** — бейджи: live-аудит 71 пара <4.5 (dashboard 23, worst 3.85; editor 48,
   worst 4.34); добрать токены badge-*-bg/fg. ~0.5 дня.
3. **AX-02/AX-03 (P2)** — эмодзи-реакции/чат (растр 13/14/22.4px) и флаги стран
   «RS RS» вместо флагов; после редеплоя эмодзи станут SVG, флаги — перевести на SVG-набор
   или текстовый код. ~0.5–1 день.
4. **C7 (P2)** — типографика/контролы: 419 дробных размеров, 80 <11px, 318 мелких
   таргетов; editor 9–10.5px, dashboard 10.5–10.58px, widget 10.5–10.92px. ~2–3 дня.
5. **AX-04/AX-05 (P2)** — 429 без авто-retry (добавить backoff по Retry-After) и
   сообщение при битом токене («сессия истекла/токен недействителен», не «сервер
   недоступен»). ~0.5 дня.
6. **P3:** CLS панелей 0.084/0.128, высота строк таблицы комнат, валидация комнаты
   до join, stale-ссылки на удалённые комнаты.
7. **Бэклог иконок:** 22 замечания (P1=2, P2=12, P3=8) — `docs/qa-icons.md` §11.

Дизайн-вывод: **P0 закрыт, все P1-критерии пройдены (7/7)**; продукт визуально целостен
(тема, шапка, уведомления, имена, код-блок, превью редактора, island/Guardify).
Ключевой риск — **деплой-гэп**: часть жалоб заказчика уже исправлена в HEAD, но не в образе.

---

## 2. Методика и ограничения

### Что и как измерялось

| Группа | Инструмент | Что снимали |
|--------|-----------|-------------|
| Типографика | `getComputedStyle` | font-size, line-height, font-weight, letter-spacing, font-family для h1/h2/h3/body/p/button/label/th/td/code/badge; полная перепись размеров по странице |
| Отступы/выравнивание | `getBoundingClientRect` | координаты и размеры header/hero/cards/toolbar/tile/panel/nav; margin/padding/gap; высоты карточек и кнопок |
| Переполнения | `scrollWidth` vs `clientWidth` | документ, контейнеры, текстовые узлы (`nowrap`, `ellipsis`), шапка на 390px |
| Контраст | WCAG 2.1 формула | эффективный фон с обходом предков, композитинг `color(srgb … / a)` и градиентов; порог 4.5:1 (3:1 для крупного текста) |
| Тач-таргеты | rect кнопок/ссылок/полей | элементы < 40×40 px на мобильных и десктопе |
| Ресурсы | `img.naturalWidth`, HTTP-статусы | лого/фавикон/иконки/manifest, 404, console.error, pageerror |
| Темы | computed CSS-переменные `--agil-*` | `data-theme`, `data-agil-scheme`, фон root/header/footer/panel, surface/text |
| Поведение | puppeteer | клики по контролам, открытие панелей, pin/spotlight, minimize, fullscreen, диалоги, ошибка логина |

### Ограничения (важно при чтении)

1. **Fake media**: Chrome отдаёт синтетический зелёный паттерн вместо камеры; цвета
   плиток на скриншотах не отражают реальное видео. Оценивались рамки, оверлеи, хром.
2. **Контраст над видео не измеряется автоматически** — фон под именем участника
   динамический. Значение `tile-name = 1.0` в метриках помечено как артефакт метода
   (визуально текст читается за счёт скрима), в замечания не включено.
3. **Headless-сеть** локального браузера вызывает деградацию качества: баннер
   «Слабый канал…» и авто-отключение видео — это реальные состояния UI, но их появление
   в headless не доказывает проблемы сети на стенде. Отдельно сохранены
   `qa/visual/screenshots/call-degraded/`; для чистых снимков fallback отключался в конфиге.
4. **Шрифты**: на машине QA установлены Inter и JetBrains Mono, поэтому скриншоты
   показывают «целевой» вид. На машинах без них рендер отличается (см. F-10).
5. Скриншоты — не baseline для pixel-diff; сравнение с макетами не проводилось
   (макетов в репозитории нет), поэтому «как надо» — это дизайн-система `@agil/protocol`
   и общепринятые практики (WCAG, 4/8px сетка, 40–48px контролы).

### Легенда приоритетов

| Уровень | Значение |
|---------|----------|
| **P0** | Ломает заявленную функцию/демо, блокирует показ заказчику |
| **P1** | Заметный дефект, влияет на доверие или доступность; исправлять в ближайшем релизе |
| **P2** | Консистентность/качество: заметно при сравнении экранов, не блокирует сценарий |
| **P3** | Косметика, полировка |

---

## 3. P0 — блокирующее

> ⚠️ Раздел фиксирует **первичный (pre-fix) аудит**. Статусы после фикс-билда —
> в [§1.1](#11-пост-фикс-приёмка-13112026): F-01 исправлен.

### F-01. Тёмная тема не применяется на лендинге и в звонке (схема ≠ палитра)

**Где:** `/` (переключатели «Тема»/«Пресет»), `/embed.html` (параметр `theme=dark`,
селект «Тема» в настройках звонка), 5 пресетов.
**Почему это P0:** заявленная фича «светлая/тёмная тема» на публичной витрине и в
самом продукте звонка не работает. Клиент, открывший демо, видит: выбрал «Тёмная» —
страница осталась светлой. Скриншоты для документации (старые `demo-dark.png`)
по этой же причине были идентичны светлым.

**Факты (probe, `qa/visual/cache/probe-widget-theme.json`):**

| Запрос к `/embed.html` | `data-theme` | фон root | шапка | `--agil-color-surface` | `--agil-color-text` |
|---|---|---|---|---|---|
| без параметра (light) | light | `rgb(243,246,251)` | white 82% | `#ffffff` | `#0f172a` |
| `theme=dark` | **dark** | **`rgb(243,246,251)`** | **white 82%** | **`#ffffff`** | **`#0f172a`** |
| `theme=light` | light | `rgb(243,246,251)` | white | `#ffffff` | `#0f172a` |
| `theme=auto` + OS dark | dark | `rgb(8,13,22)` | dark 82% | `#111a2b` | `#f1f5f9` |

Лендинг (`probe-theme.json`): при preset `light` + scheme `dark` — `data-theme=dark`,
но карточка остаётся `color(srgb 1 1 1 / 0.92)`; при preset `dark` + scheme `light` —
палитра остаётся тёмной. `auto` + тёмная ОС на лендинге также даёт светлую палитру.

**Где работает правильно:** dashboard (`ThemeController` явно берёт `themePresets.dark`)
и editor (`agil.editor.theme=dark`: фон `rgb(8,13,22)`). То есть дизайн-система
поддерживает тёмную палитру, дефект — в пути `resolveTheme(preset, 'dark')` →
`displayTheme()`, который при `scheme !== 'auto'` возвращает тему как есть, не подменяя
палитру на тёмную.

**Как надо:** явная схема `dark` должна выбирать тёмную палитру (аналог
`themePresets.dark` + `generatePalette(primary,'dark')`), как это уже делает dashboard:
`base = mode==='dark' ? themePresets.dark : themePresets.light`. Либо UI должен
показывать только пресеты и убрать «схему», которая ни на что не влияет.

**Скриншоты:**
`docs/screenshots/qa-landing-mixed-darkscheme.png` (выбрана «Тёмная» — страница светлая),
`docs/screenshots/qa-landing-darkpalette-lightscheme.png`,
`docs/screenshots/qa-call-settings-mixed-theme.png` (в звонке выбрана «Тёмная», панель светлая),
`docs/screenshots/qa-editor-dark.png` (в редакторе тёмная работает).

![Лендинг: выбрана тёмная тема, палитра светлая](screenshots/qa-landing-mixed-darkscheme.png)

---

## 4. P1 — критичные дефекты

### F-02. Мобильный звонок: шапка шире экрана, правые контролы обрезаны

**Где:** `/embed.html`, 390×844, шапка звонка.
**Факт (metrics-call, mobile):** `.agil-root`/`.agil-app`/`header.agil-header`:
`scrollWidth = 511`, `clientWidth = 390` (переполнение 121px, `overflow-x: hidden` на root).
На скриншоте справа обрезана пилюля «О…» (security badge), PiP/тема/запись уходят за край.
**Почему плохо:** на телефоне недоступна часть управления; обрезанный элемент выглядит
как баг вёрстки; нарушает требование «ничего не обрезано/не перекрыто».
**Как надо:** на ≤480px сворачивать шапку в два ряда с переносом (brand + таймер;
пилюли; иконки) либо прятать второстепенные пилюли в overflow-меню; контролы держать
в один ряд с `min-width: 0` и `flex-wrap`.
**Скриншот:** `docs/screenshots/qa-call-mobile-header-crop.png`
(`qa/visual/screenshots/call/call-mobile-390x844-light.png`).

![Мобильная шапка звонка обрезана справа](screenshots/qa-call-mobile-header-crop.png)

### F-03. Редактор: превью на прод-стенде не работает (iframe на localhost:5173)

**Где:** `/editor/`, центральная колонка.
**Факт:** 15 перехваченных `Request failed: GET http://localhost:5173/embed.html
:: net::ERR_CONNECTION_REFUSED` (по одному на каждое открытие секции); на экране —
плашка «preview offline — JSON/export work» и кнопка «Перезагрузить превью».
**Почему плохо:** главная функция визуального редактора (живое превью панели) на
развёрнутом стенде недоступна; это первое, что увидит заказчик.
**Как надо:** собирать editor с `VITE_PREVIEW_URL=<origin>/embed.html` (или определять
preview как `window.location.origin + '/embed.html'` при совпадении origin) и проверять
в CI, что iframe отвечает 200.
**Скриншот:** `docs/screenshots/qa-editor-preview-offline.png`.

### F-04. Смешанные состояния темы дают нечитаемые ссылки (контраст до 1.86:1)

**Где:** лендинг, preset `light`/`corporate` + scheme `dark`, preset `dark`/`night` + scheme `light`.
**Факты (WCAG):**

| Состояние | Элемент | Контраст | Норма |
|-----------|---------|----------|-------|
| night + scheme light | ссылки (`a`), навигация | **1.86:1** | 4.5:1 |
| dark + scheme light | ссылки, навигация | 2.70:1 | 4.5:1 |
| light + scheme dark | ссылки, навигация | 3.50:1 | 4.5:1 |
| corporate + scheme dark | ссылки, навигация | 3.47:1 | 4.5:1 |
| night + scheme dark | ghost-кнопки | 2.61–3.20:1 | 4.5:1 |
| light + scheme dark | muted-текст | 4.03:1 | 4.5:1 |

**Почему плохо:** при переключении «Темы» ссылки становятся почти невидимыми — прямое
нарушение WCAG AA и удар по восприятию «дизайн-системы».
**Как надо:** либо исправить F-01 (палитра следует схеме), либо убрать независимый
переключатель схемы и оставить только корректные пресеты.

### F-05. Dashboard: систематические недоборы контраста (4.05–4.45:1)

**Где:** `/dashboard/`, все экраны.
**Факты:** `th` (заголовки таблиц) — **4.05:1** на 13 снимках; ссылки/навигация —
**4.37:1** (light), 4.45:1 (dark), **3.28:1** (auto + OS dark); бейджи — 4.14–4.37:1.
**Почему плохо:** 11–12.5px текст в таблицах и навигации — самый массовый контент панели;
недобор 0.05–1.2 пункта стабильно воспроизводится.
**Как надо:** поднять токены `--agil-color-text-muted`/`link` минимум на 1 шаг шкалы,
добавить `auditTheme` проверку в CI для пар «muted на surface» и «link на bg».

### F-06. Дублирование уведомлений о слабом канале; тост перекрывает контролы

**Где:** звонок, состояние деградации.
**Факт:** одновременно показаны баннер «Слабый канал: видео снижено, чат продолжает
работать» (полная ширина над тулбаром) и такой же по смыслу тост в правом нижнем углу;
тост накрывает кнопки «Настройки»/«Выйти» (toolbar 16 кнопок, последние две попадают
под тост). На скриншоте видно наложение.
**Как надо:** оставить один канал уведомления (баннер — для постоянного состояния,
тост — только для разовых событий), позиционировать тосты выше тулбара.
**Скриншоты:** `docs/screenshots/qa-call-degraded.png`, `qa-call-chat.png`.

### F-07. Имена участников обрезаются уже при двух участниках

**Где:** звонок, плитки. **Факт:** `span.agil-tile-name-text "Организатор"`:
`scrollWidth=85`, `clientWidth=73`, `white-space: nowrap`, `overflow: hidden`
(и на 1440, и на 390). На скриншотах — «Организ…», «Бор…», рядом чип роли «организатор»
и бейдж страны; имя и роль визуально конкурируют за одну строку.
**Как надо:** отдать имя + чип роли одной строкой с `min-width:0`, `text-overflow:
ellipsis` на имени, ограничить роль иконкой/тултипом; на мобильном — имя в две строки.

### F-33. Смешанные preset × scheme: ссылки и ghost-кнопки нечитаемы (P1, после фикса)

**Статус:** открыто (обнаружено на пост-фикс прогоне).
**Где:** лендинг, комбинации «пресет × схема» (селекты «Пресет» + «Тема»).
**Факты (после фикса):** `minimal + dark` — ссылки/навигация **1.0:1** (невидимы),
`night + dark` 3.22, `light + dark` 3.42, `dark + light` 3.68, `corporate + dark` 3.44;
ghost-кнопки 3.2–3.68. Всего 24 пары <4.5 (было 60, но min ухудшился с 1.86 до 1.0).
**Почему плохо:** пользователь может выбрать любую комбинацию — часть состояний
практически нечитаема; это самый заметный остаток темы после закрытия P0.
**Как надо:** цвет `link`/`nav`/`ghost` брать из `onSurface`/`textSecondary`
**после** вывода палитры (`displayTheme`), а не из пресета; прогнать `auditTheme`
по 5 пресетам × 3 схемам в CI.
**Скриншоты:** `docs/screenshots/qa-after-landing-minimal-dark.png` (nav невидим),
`qa/visual/screenshots/demo/landing-night-dark-1440.png`.

### F-34. Превью редактора: embed без `room` → offline (P1) — **исправлено 13.11.2026, 16:56 MSK**

**Статус:** **исправлено** (подтверждено повторной проверкой на текущем деплое, свежий профиль
браузера, cache-bust `_r`). Iframe: `https://calls.guardify.ru/embed.html?room=preview&tenant=demo&_r=…`
→ **200**, `status.kind=ready`, текст «preview подключён», overlay `rect 0×0` (скрыт),
в превью виден join-screen «Подключение к комнате», `console.error` — 0.
**Ранее (до пересборки):** iframe открывался без `?room=` → «Не указан параметр room»,
`agil:ready` не приходил → оверлей offline. Дополнительно мой пробник давал **ложный
негатив**: считал offline по тексту скрытого DOM (оверлей остаётся в разметке с `hidden`).
Детект исправлен: проверяется фактическая видимость overlay (`rect > 4×4`, `!hidden`)
и статус `ready`; `probe-editor-preview.mjs --after` теперь даёт `ok=true`.
**Скриншот:** `docs/screenshots/qa-after-editor-preview-fixed.png`.

---

## 5. Постраничные замечания P2/P3

### 5.1 Лендинг `/`

**Что видно:** hero с H1 40px, две карточки («Запустить звонок» 578px и «Как встроить»
500px), тёмный код-блок, шапка с лого/навигацией/двумя селектами, футер.

| ID | Замечание | Почему | Как надо | Сев. |
|----|-----------|--------|----------|------|
| F-11 | Карточки разной высоты: 408px и 544px; ширины 578 и 500px при grid | Ломает «две равные колонки» — правая выглядит главнее без причины | `align-items: stretch` + равные `1fr`; либо осознанная иерархия | P2 |
| F-12 | Код-блок: шрифт 10.92px, строки вроде `<div id="call" style="height: 640px; border-radius: 16px; overflow:` переносятся посреди атрибута, отступы ломаются | Код нельзя скопировать глазами; выглядит непрофессионально | 12–13px, `white-space: pre`, горизонтальный скролл или перенос по токенам | P2 |
| F-21 | Селект «Тема» локализован («Светлая», «Как в системе»), селект «Пресет» — сырые ключи (`light`, `dark`, `corporate`) | Разные словари в одном тулбаре | Локализовать пресеты или показывать свотчи | P3 |
| F-26 | В шапке разрыв лого→первая ссылка ~93px, между ссылками 18px | Навигация «плывёт», логотип оторван | Единый `gap: 18–24px`, логотип в отдельной зоне с фиксированным отступом | P3 |
| F-27 | `body` имеет тёмный фон `rgb(8,13,22)` при светлой палитре — виден на overscroll/короткой странице | Тёмные полосы вокруг светлого контента | Красить `html/body` текущим токеном фона | P3 |

### 5.2 Экран входа `/embed.html`

**Что видно:** карточка 912×~283, превью 454×234 слева, форма справа (имя, кнопки
«Войти в звонок»/«Только слушать», сервер+id, подсказка горячих клавиш).

| ID | Замечание | Почему | Как надо | Сев. |
|----|-----------|--------|----------|------|
| F-24 | Превью 454×234 = **1.94:1**, тогда как плитки звонка 16:9 (1.78:1) | Скачок пропорций между входом и звонком | Превью в 16:9, как плитка | P3 |
| F-25 | На экране входа нет названия комнаты (виден только id внизу) | Пользователь не понимает, куда входит | Показать `room.name` в заголовке «Подключение к комнате» | P3 |
| F-09 | Кнопки микро/камеры на превью 36×36 | Ниже 40px тач-таргета | 40–44px | P2 |
| F-16b | Мелкая типографика: подписи 11.48px, подсказка 11.48px, hint о клавишах 11.48px | <12px на десктопе плохо читается | Минимум 12px | P2 |

### 5.3 Звонок `/embed.html` (грид, панели, spotlight, mini, fullscreen)

| ID | Замечание | Почему | Как надо | Сев. |
|----|-----------|--------|----------|------|
| F-15 | Грид 2 участников: плитки 702×395 при 1440×900, сверху ~137px и снизу ~211px пустоты | Неэффективное использование экрана, «пустой» звонок | Ограничить max-height/масштабировать плитки, центрировать по свободной области, при 2 участниках — крупнее | P2 |
| F-14 | Мобильный тулбар: 16 кнопок 48px → 3 ряда (8 + 8 + 2), последний ряд из двух по центру | Рваная сетка, огромная высота 152px, «Выйти» отдельно | Горизонтальный скролл-снап или приоритизация (5 главных + «Ещё») | P2 |
| F-16 | Панель настроек: заголовки секций `h3` 11.2px, `#64748b` на светлом фоне — **4.2:1** | Ниже AA | 11.2→12px, цвет на шаг темнее | P2 |
| F-18 | Пустое состояние чата прижато к низу панели (y≈735 при панели 350×769) | Пустая панель выглядит «сломанной» | Центрировать empty-state по вертикали | P2 |
| F-17 | `color-scheme` не выставлен (`html color-scheme: normal`) | В тёмной палитре нативные селекты/скроллбары остаются светлыми | `document.documentElement.style.colorScheme = scheme` | P2 |
| F-09b | Иконочные кнопки 35×35, security-badge 95×30 | <40px | 40×40 / pill 40 высотой | P2 |
| F-22 | Заголовок шапки дублирует бренд: «AGIL Calls» слева и чип «AGIL Calls» (щит) справа | Два разных элемента с одним текстом | Переименовать чип в «Проверка безопасности»/иконку | P3 |
| F-28 | Чип роли «организатор» строчными, бейдж «Гость» с заглавной | Несогласованный регистр | Единый стиль ролей | P3 |

### 5.4 Панель `/dashboard/`

**Что видно:** sidebar 240px, шапка 60px (заголовок, статус, тенант, тема, logout),
KPI-строка, таблицы, карточки, empty-state «Геоданные пока недоступны».

| ID | Замечание | Почему | Как надо | Сев. |
|----|-----------|--------|----------|------|
| F-05b | Табличный заголовок `th` 11.5px uppercase, контраст 4.05:1 | Массовый контент, ниже AA | Токен muted темнее | P2 |
| F-20 | Кнопки переключения темы 30×26px; logout/menu 34×34; навигация 38px | Нет единого размера, мелкие таргеты | Шкала 28/32/36/40 + 40px минимум для тача | P2 |
| F-08 | Шкала размеров: 10.5, 11, 11.5, 12, 12.5, 12.8, 13, 13.5, 14, 15, 18, 22, 28px | Дробные и близкие размеры (12.5 vs 12.8 vs 13) — визуальный шум | Типографическая шкала 12/13/14/16/20/24/28 (или 4px-модуль) | P2 |
| F-22 | Чип «Тенант demo» — monospace, остальной интерфейс — sans | Неочевидный акцент | Моно только для id/url | P3 |
| F-31 | «Эффективные права», «Сервер», KPI-карточки — разные радиусы (12) и тени в целом ок; но верхние цветные полосы KPI не из токенов (4 разных цвета) | Визуальный шум; красная полоса «События безопасности» без семантики ошибки | Использовать `success/warning/danger/info` осознанно | P3 |
| F-32 | Empty-state «Геоданные пока недоступны» занимает половину правой колонки пустотой | Пустое место выглядит как незагрузившийся блок | Уменьшить карточку, добавить пояснение/ссылку | P3 |

### 5.5 Редактор `/editor/`

**Что видно:** 3 колонки 320px / центр / 380px; слева nav 15 секций в 2 колонки;
центр — превью (сейчас offline); справа JSON 14 302 символа, embed-сниппет, «Сохранить
в тенант».

| ID | Замечание | Почему | Как надо | Сев. |
|----|-----------|--------|----------|------|
| F-03b | (см. P1) Превью offline; кнопка «Перезагрузить превью» — самый крупный элемент пустой области | Внимание уводится от настроек в нерабочую функцию | Показывать компактную плашку, если preview недоступен, и ссылку «настроить VITE_PREVIEW_URL» для админа | P1 |
| F-13 | На 390px порядок: шапка → статус-бар → превью → nav секций → поля. Настройки уезжают на 3-й экран | Мобильный сценарий редактора неудобен | На мобильном nav сверху (или bottom-sheet), превью — сворачиваемым блоком | P2 |
| F-19 | В секции «Тема» QA-скрипт не нашёл кнопок пресетов по текстовому лейблу (только визуальные свотчи) | Пресеты сложно обнаружить и озвучить скринридером | Подписать пресеты (или `aria-label`), показать активный | P2 |
| F-23 | Чипы «tenant : demo» (пробелы вокруг двоеточия), «черновик пуст»; в блоке «Сохранить в тенант» подсказка «dev: http://localhost:8787», а поле содержит прод-URL | Внутренние dev-подсказки на прод-стенде; шум | Убрать dev-подсказку или показывать только для localhost, выровнять чипы | P3 |
| F-29 | Фиксированные 320/380px колонки: на 768px три колонки не помещаются и складываются в неочевидном порядке | Адаптив редактора «технический» | Планшетный breakpoint: nav + поля, JSON — таб/сайдшит | P3 |

### 5.6 Guardify-страница и island-лейаут (новые поверхности фикс-билда)

**`/guardify.html`** — кастомная Guardify-демо (тёмная тема, встроенный виджет с
`chrome:'none'`, `controlsStyle:'island'`, скрытый чат). Снято 6 состояний:
hero, звонок с island, два участника, minimize, мобильный 390
(`docs/screenshots/qa-after-guardify-*.png`).
**Island-лейаут** на `/embed.html` (`data-agil-controls-style="island"`,
`hiddenControls`) — light/dark + 390 + popover «⋯»
(`docs/screenshots/qa-after-island-*.png`).

| ID | Замечание | Почему | Как надо | Сев. |
|----|-----------|--------|----------|------|
| F-35 | Guardify: primary-кнопка «Войти в звонок» — белый на `#8576ff` = **3.48:1**; H1 **49.6px**, подписи 15.04/13.76px (дробные); 15 тач-таргетов <40px; brand-svg `stroke-width=2` при widget 1.8 | CTA — главное действие демо; дробная типографика и смешанный stroke ломают «дизайн-системность» | `onPrimary` для фиолетового primary сделать тёмным (или затемнить primary до ≥4.5); округлить шкалу до целых; выровнять stroke | P2 |
| — | Island: новых дефектов нет | `controlsStyle=island` применился, 14 кнопок, popover работает, hiddenControls не отрисованы, overflow 0, тач-таргеты 390 — 0 мелких | — | ✅ |

**Вывод по острову:** island закрывает часть F-14 (на мобильном вместо трёх рядов
кнопок можно включить остров), но default `bar` на 390 всё ещё 3 ряда — оставлено в F-14.

---

## 6. Таблица числовых метрик

| Элемент | Свойство | Факт | Ожидание | Сев. |
|---------|----------|------|----------|------|
| Лендинг h1 | font-size / line-height / weight | 40px / 46.4px (1.16) / 700 | 40/48 (1.2) на 8px-сетке | P3 |
| Лендинг h2 | font-size / weight | **15.12px** / 650 | Целый размер шкалы (14/16), weight из набора 500/600/700 | P2 |
| Лендинг label | font-size | **11.48px** | ≥12px | P2 |
| Лендинг helper | font-size | 10.84–10.92px | ≥12px | P2 |
| Лендинг код | font-size / переносы | **10.92px**, перенос посреди атрибута | 12–13px, pre + scroll | P2 |
| Лендинг кнопки | высота | 42px у всех | Единый размер (ок) | — |
| Лендинг карточки | высота / ширина | 408 / 544px, 578 / 500px | Равные колонки и высоты | P2 |
| Лендинг навигация | высота таргета | 19px | ≥40px (тач) / 24px (WCAG 2.2 AA) | P2 |
| Виджет контролы | font-size | **13.3333px** на 75 элементах | 14px (или целый rem) | P2 |
| Виджет бейджи | font-size | **9.52px** | ≥11–12px | P2 |
| Виджет иконочные кнопки | размер | 35×35 (30×30 badge) | 40×40 | P2 |
| Виджет тулбар | высота кнопок | 48px (ок), на мобиле 3 ряда | 1–2 ряда | P2 |
| Виджет плитка | aspect-ratio | 702×395 = 1.778 (16:9) | 16:9 (ок) | — |
| Виджет шапка 390 | scrollWidth/clientWidth | **511 / 390** | ≤390 | P1 |
| Виджет имя плитки | scrollWidth/clientWidth | 85 / 73 (nowrap) | Перенос/эллипсис без наложения | P1 |
| Dashboard nav | высота | 38px | 40px+ | P2 |
| Dashboard theme switch | размер | 30×26 | 40×40 | P2 |
| Dashboard `th` | контраст | 4.05:1 | ≥4.5:1 | P1 |
| Dashboard ссылки | контраст | 4.37 / 4.45 / **3.28** | ≥4.5:1 | P1 |
| Dashboard шкала | кол-во размеров | 13 (10.5…28) с дробными | 6–8 целых | P2 |
| Редактор `badge` | контраст | 4.34:1 | ≥4.5:1 | P2 |
| Звонок h3 (настройки) | контраст / size | 4.2:1 / 11.2px | ≥4.5:1 / 12px | P2 |
| Все страницы | горизонтальный overflow документа | 0 (лендинг/dashboard/editor/звонок 1440) | 0 (ок) | — |
| Все страницы | console.error / 404 | 0 (кроме iframe редактора) | 0 | — |
| Лого/иконки | naturalWidth | 256 (рендер 34/30) | ≥2× рендера (ок) | — |
| Fonts | @font-face / webfont-запросы | **0** | Self-hosted Inter/JetBrains Mono или явный fallback-стек | P2 |
| `site.webmanifest` | наличие/иконки | 200, 192/512 OK | ок | — |
| Контраст над видео | метод | не измеряется | Ручная проверка | инфо |

---

## 7. Кросс-тема и кросс-вьюпорт матрицы

### Темы

| Поверхность | light | dark (схема) | auto + OS dark | 5 пресетов | Комментарий |
|---|---|---|---|---|---|
| Лендинг `/` | ✅ | ❌ палитра не меняется (F-01) | ❌ остаётся light | ⚠️ пресеты меняют палитру, но схема ломает контраст (F-04) | |
| Экран входа | ✅ | ❌ | ✅ (auto даёт dark) | ⚠️ | dark достижим только auto+OS dark |
| Звонок | ✅ | ❌ `data-theme=dark`, палитра light (F-01) | ✅ | ❌ preset-файл не применился на прод-конфиге | |
| Dashboard | ✅ | ✅ | ✅ | n/a (своя тема) | эталон корректной работы |
| Editor | ✅ | ✅ | ✅ | ⚠️ переключение пресетов не найдено по лейблам (F-19) | |

### Вьюпорты

| Страница | 390 | 768 | 1366 | 1440 | 1920 | Проблемы |
|---|---|---|---|---|---|---|
| Лендинг | ✅ нет overflow | ✅ | ✅ | ✅ | ✅ | шапка на мобиле плотная, но без обрезки; навигация 19px (F-09) |
| Экран входа | ✅ | ✅ | ✅ | ✅ | ✅ | превью 1.94:1 (F-24) |
| Звонок | ❌ шапка 511/390 (F-02); тулбар 3 ряда (F-14) | ⚠️ тулбар 2 ряда | ✅ | ✅ | ✅ | грид-пустоты (F-15) |
| Dashboard | ✅ (overflow 1 контейнер — таблица со скроллом) | ✅ | ✅ | ✅ | ✅ | тач-таргеты 26–38 (F-20) |
| Editor | ⚠️ порядок колонок (F-13) | ⚠️ 3 колонки не помещаются (F-29) | ✅ | ✅ | ✅ | превью offline (F-03) |

---

## 8. Тексты (RU)

| ID | Где | Текст | Проблема | Как надо | Сев. |
|----|-----|-------|----------|----------|------|
| T-01 | Звонок → Настройки → Диагностика | «Качество 79/100 · **critical**» | Английский термин в русском UI | «критично» / «низкое» | P2 |
| T-02 | Звонок → Настройки → Диагностика | «Задержка **1 ms**» | Единицы не локализованы, нет пробела перед «мс» | «1 мс» | P3 |
| T-03 | Editor, overlay | «preview offline — JSON/export work» | Смесь EN/RU в заголовке, ниже — русский текст | Полностью на русском: «Превью недоступно — JSON и экспорт работают» | P2 |
| T-04 | Editor, шапка | «tenant : demo» | Пробелы вокруг «:» в чипе | «tenant: demo» или «Тенант demo» | P3 |
| T-05 | Editor, шапка | «черновик пуст» | Строчная буква в чипе-статусе | «Черновик пуст» | P3 |
| T-06 | Dashboard → Комнаты | бейдж «группа», «Открыта», «Нет» | Разный регистр/смысл рядом («группа» строчная, «Открыта» заглавная) | Единый стиль бейджей | P3 |
| T-07 | Звонок, плитка | Чип «организатор» (строчная) vs бейдж «Гость» (заглавная) | Несогласованный регистр ролей | «Организатор»/«Гость» единообразно | P3 |
| T-08 | Звонок, шапка | «Подключено» / «Подключение…» | Ок, но «…» вместо «...» — проверить шрифт | — | инфо |
| T-09 | Лендинг | «Войти в виджет», «Войти в iframe», «Создать комнату» | Три кнопки — три разных глагола, «iframe» — техножаргон для конечного пользователя | «Войти в звонок» / «Скопировать код вставки» | P3 |
| T-10 | Лендинг, hero | «WebRTC-mesh до 8 участников, темы на дизайн-токенах, чат при плохой связи, блюр фона, запись, субтитры и панель заметок» | Перечисление из 8 сущностей в одном предложении, строка 100+ символов | Разбить на 2 предложения/бейджи | P3 |
| T-11 | Dashboard, подзаголовки | Длинные технические подзаголовки секций («role defaults + overrides + denied − выключенные фичи») | Техножаргон в UI для админа | Оставить в подсказке `title`, в UI — человеческий текст | P3 |

Крупных орфографических ошибок не найдено; терминология в целом последовательная
(«комната», «участник», «тенант», «фича» — ок для dev-аудитории).

---

## 9. Топ-10: исправить первым

> ⚠️ Это исходный список приоритетов pre-fix. Что уже закрыто и что осталось —
> в [§1.1](#11-пост-фикс-приёмка-13112026) («Что осталось (P0/P1)»).

| # | Замечание | Приоритет | Почему сейчас | Оценка |
|---|-----------|-----------|----------------|--------|
| 1 | F-01 Тёмная тема на лендинге и в звонке | **P0** | Ломает демо ключевой фичи; ломает документацию/маркетинг | 1–2 дня (правка `displayTheme`/`resolveTheme` + тесты + перегенерация скриншотов) |
| 2 | F-02 Мобильная шапка звонка 511/390 | **P1** | Недоступные контролы на телефоне — блокер мобильного сценария | 1–2 дня (flex-wrap/overflow-меню + тест 390/414/768) |
| 3 | F-03 Превью редактора на прод-стенде | **P1** | Главная функция редактора не работает | 0.5 дня (VITE_PREVIEW_URL/origin-детект + CI-проверка) |
| 4 | F-04/F-05 Контраст <4.5:1 (смешанные темы, dashboard, редактор) | **P1** | Доступность и «качество» скриншотов; видно сразу | 1–1.5 дня (токены + `auditTheme` в CI) |
| 5 | F-06 Дубль баннера и тоста, перекрытие тулбара | **P1** | Прямо мешает нажать «Настройки»/«Выйти» в деградации | 0.5 дня |
| 6 | F-07 Обрезка имён участников | **P1** | Каждый звонок, каждый участник | 1 день (лейаут плитки + ellipsis) |
| 7 | F-08/F-09 типографическая шкала и размеры контролов | **P2** | Системный источник «мелких» дефектов; чинить один раз | 2–3 дня (токены `font-size`, `control-height`, миграция 3 приложений) |
| 8 | F-10 Self-hosted шрифты (Inter/JetBrains Mono) | **P2** | Кросс-платформенный рендер, FOUT/подмена | 0.5 дня + ассеты (woff2, `font-display: swap`) |
| 9 | F-11/F-12 лендинг: карточки и код-блок | **P2** | Первый экран продаёт продукт | 1 день |
| 10 | F-15/F-14 грид и мобильный тулбар | **P2** | Восприятие «звонок как у больших» | 1.5 дня |

Суммарно top-10 ≈ 10–13 человеко-дней + перегенерация QA-скриншотов.

---

## 10. Что проверено и в порядке

- Нет горизонтального overflow документа на лендинге, dashboard, editor и звонке
  на 1366/1440/1920 (единственное исключение — шапка звонка на 390, F-02).
- **0 console.error, 0 pageerror, 0 HTTP 404** на лендинге, dashboard и звонке
  (в редакторе — только ожидаемый offline-iframe, F-03).
- Логотип и брендинг: `/branding/agil-icon-256.png` 256×256 рендерится 34×34 (downscale,
  без апскейла), favicon 16/32, apple-touch-icon, `site.webmanifest` с иконками 192/512 —
  все 200; OG-изображение доступно.
- Пропорции плиток звонка строго 16:9 (702×395, 378×213, 933×524).
- Кнопки на лендинге одинаковой высоты 42px; тулбар звонка 48px — базовые контролы
  консистентны внутри одного экрана.
- Dashboard тёмная тема (явная и auto+OS dark) работает; editor тёмная тема работает;
  восстановление конфига тенанта после QA-прогонов подтверждено автоматической сверкой.
- Диалоги (создание комнаты, подтверждение, выдача токена) открываются, фокус-ловушка
  и Escape работают; ошибка входа отображается.
- Empty-states и offline-плашки в dashboard/editor отрисованы и не ломают лейаут.

---

## 11. Приёмка после фиксов (P0/P1)

> **Статус:** пост-фикс прогон **выполнен 13.11.2026** — результаты, статусы F-01…F-35
> и before/after-скриншоты в [§1.1](#11-пост-фикс-приёмка-13112026). Итог: 8/10 критериев
> pass, P0/P1 — 7/8 (закрыты C1/C2/C3/C4/C5/C6/C0; открыт **C9** — join/embed адаптив, до фикса). Ниже — сохранённые
> критерии и процедура для следующего цикла.

**Baseline:** `qa/visual/baseline/2026-11-13-before/` (снимки + JSON-метрики «до»,
31.7 MB, скриншоты `before` не перезаписываются после-прогоном).

### Критерии приёмки

| ID | Приоритет | Критерий | Источник | Порог |
|----|-----------|----------|----------|-------|
| C1 | **P0** | Явная схема `dark` даёт тёмную палитру на всех поверхностях | `probe-widget-theme`, `probe-theme`, `metrics-dashboard/editor` | widget: `data-theme=dark` + root bg тёмный + surface `#111a2b`; landing: карточка тёмная при `light+dark` и при `auto+OS dark`; dashboard/editor: `dataset=dark` + тёмный body |
| C2 | P1 | Мобильный звонок 390px без переполнения шапки | `metrics-call`, call-grid 390×844 | нет overflow-элементов `agil-root`/`agil-app`/`agil-header`; ширина ≤390 |
| C3 | P1 | Превью редактора грузится с того же origin | `probe-editor-preview` | iframe 200, не `localhost`, оверлей offline отсутствует |
| C4 | P1 | Контраст текста ≥4.5:1 (кроме текста поверх видео) | `metrics-demo/dashboard/editor/call` | 0 пар ниже порога; mixed-состояния лендинга исключены (их закрывает C1) |
| C5 | P1 | Одно уведомление о слабом канале, без перекрытия тулбара | `probe-notifications` (деградация форсируется порогами) | не одновременно баннер+тост; площадь пересечения тоста с тулбаром = 0 |
| C6 | P1 | Имена в плитках не обрезаются жёстко | `metrics-call` | overflow у `tile-name` только с `text-overflow: ellipsis` |
| C7 | P2 | Типографика/контролы | `metrics-*` | нет дробных font-size; hard-минимум 11px (11–12px — предупреждение); тач-таргеты ≥40px на 390 |
| C8 | P2 | Self-hosted шрифты | `metrics fonts.faces` | ≥1 webfont-фейс (Inter/JetBrains Mono) |
| C9 | **P1** | Join/embed адаптив: нет внутреннего скролла, симметрия preview/form | `qa/visual/audit-join-layout.mjs` | 563×545 / 390×844 / 768×1024 / 1440×900 × (guardify + embed): `scrollHeight<=clientHeight+1`, нет `overflow-y:auto` при переполнении, 0 обрезанных элементов, в 2-col Δ ширины/высоты ≤1px и top ≤1px |
| C0 | P1 | Нет регрессий | `metrics.watch`, `doc.overflowX` | 0 console/404 (кроме известного pre-fix iframe), 0 overflow документа на 1366/1440/1920 |

Правило: **«готово к сдаче» = все P0/P1 (C0–C6) в статусе pass**, P2 фиксируются как
открытые с новой оценкой.

### Процедура после деплоя

```powershell
cd E:\Claude\VideoCalls
$env:ADMIN_KEY='<актуальный ключ>'
node qa/visual/run-all-after.mjs          # 9 шагов: 5 стадий + 4 пробника → verify → publish
# или только пересчёт по уже снятым данным:
node qa/visual/run-all-after.mjs --skip-capture
node qa/visual/publish-after.mjs          # курированные after-снимки в docs/screenshots/qa-after-*.png
```

Что обновляется в отчёте после прогона:

1. В этот раздел вставляется блок `qa/visual/after-verdict.md` (таблица статусов
   C0–C8 + сгенерированные пары «до/после»).
2. В таблицы замечаний F-01…F-32 добавляется колонка/статус **исправлено / частично /
   открыто**; закрытые пункты помечаются ссылкой на after-скриншот.
3. P0/P1 проверяются повторно по критериям (не «на глаз»), P2/P3 — выборочно,
   по diff метрик `qa/visual/cache/after-verdict.json`.
4. Если появляются новые дефекты — заводятся новые F-ID с приоритетом и скриншотом.

### Ограничения приёмки

- Fake media в headless может снова вызвать баннер деградации — для C5 деградация
  форсируется намеренно (пороги `fallback.lowBandwidth`), затем конфиг восстанавливается
  и сверяется с baseline.
- C4 не оценивает текст поверх видео (`tile-name`) — фон динамический.
- C7 считает шрифты <11px жёстким дефектом, 11–12px — предупреждением, чтобы не
  блокировать релиз из-за одной микроподписи.
- Прогон изменяет только `demo`-конфиг (patch → restore), создаёт QA-комнаты
  и удаляет их; ключ берётся из env и не печатается.

---

## 12. Тотальный визуальный аудит (13.11.2026)

> Заказчик: «куча багов, каждая кнопка/иконка, смайлики пиксельные без сглаживания,
> смещения/отступы, растягивание картинки на всё поле, скачки интерфейса при уведомлениях,
> ошибки подключения». Прогон на **текущем задеплоенном образе** (конфиг `demo` восстановлен,
> QA-комнаты удалены, исходники не менялись).
>
> **Артефакты:** `qa/visual/cache/{audit-surfaces, audit-emoji, audit-motion, metrics-data, contrast-matrix}.json`,
> `qa/visual/clickall/*.json|png`, `qa/visual/screenshots/{deep,final-data,contrast,clickall}/**`,
> 20 курированных снимков в `docs/screenshots/qa-audit-*.png`.

### 13.1 Покрытие и метод

| Направление | Что сделано | Объём |
|-------------|-------------|-------|
| `scripts/qa-clickall.mjs` | клик по каждой кнопке/ссылке, CLS-обсервер, broken/stretched image, emoji, tiny text | 6 прогонов: `/` ×2, `/guardify.html` ×2, `/embed.html` ×2 |
| Глубокий аудит поверхностей | img/video natural vs rendered, object-fit, emoji-элементы, tiny text, overflow, console | 27 поверхностей (лендинг, guardify, join, звонок+панели, 7 экранов панели, вкладки комнаты, редактор) |
| Эмодзи | реакции (меню/оверлей), чат-эмодзи, флаги стран, DPR 1/2, кропы | 4 состояния × DPR1/2 + 6 скринов |
| Движение интерфейса | CLS при join/уведомлениях/панелях, сдвиг тулбара, перекрытия | 10 состояний, light/dark |
| Контраст C4 | 5 пресетов × {light,dark,auto} × OS {light,dark} + виджет + бейджи | 30 лендинг + 8 виджет + 71 бейдж |
| Seed-данные | обзор/комнаты/аудит/записи/доступы/роли/комната с гео/транскриптом | 9 экранов + 6 ролей через API |
| Иконки | повторный прогон inventory/gallery/live | 117 иконок, 95 SVG live — **без изменений, 22 замечания в бэклоге** |

### 13.2 Ключевое: деплой-гэп — образ НЕ содержит HEAD-фиксы (AX-01, P1)

Финальный образ отдаёт бандлы **без** последнего коммита `8168d84`
(«fix(widget): SVG emoji reactions, Esc handling, notification overlay (no layout shift),
logo/media fit»):

| Проверка | Задеплоенный образ | Локальный HEAD 8168d84 |
|----------|--------------------|------------------------|
| `/sdk/agil-calls.js`, `/widget/agil-calls-widget.css` | `agil-emoji-svg` — **нет**, `Vector reaction emoji` — нет | SVG-реакции + `shape-rendering: geometricPrecision` |
| Комментарий в `emoji.ts` («native emoji … look pixelated — customer complaint») | не присутствует в бандле | присутствует |
| Поведение реакций | **нативные bitmap-эмодзи** (Segoe UI Emoji) | векторные SVG 20px |

**Вывод:** часть жалоб заказчика («смайлики пиксельные, без сглаживания», «скачки
интерфейса», «растягивание картинки») относится к фиксам, которые **ещё не задеплоены**.
Нужен редеплой из HEAD `8168d84` и повторная проверка AX-02/AX-07/«изображения».

### 13.3 Эмодзи (AX-02, P2; AX-03, P2)

**AX-02. Реакции и чат-эмодзи — нативные bitmap-глифы.**
`menuSvgs = 0` (в меню реакций нет ни одного SVG); 6 кнопок 40×40, эмодзи рисуется
текстовым глифом: `font-family: Inter` (fallback на emoji-шрифт ОС), `font-size: 14px`;
чат-эмодзи: 12 элементов с размерами **13 / 14 / 22.4px**; оверлей реакции — 4 показа.
На DPR1 14px-глиф выглядит ступенчато (см. кроп
`docs/screenshots/qa-audit-emoji-reactions-dpr2.png` и `…-dark.png`);
на DPR2 сглаживание лучше, но остаётся растровым.
**Как надо:** задеплоить SVG-эмодзи из HEAD (векторные, 20px, без CSS-transform),
либо self-hosted emoji-набор; в чате — не смешивать 13/14/22.4px, привести к шкале
16/20/24.

**AX-03. Флаги стран — региональные emoji-индикаторы превращаются в пары букв.**
На Windows Chrome флаги `🇷🇸`/`🇹🇷` рендерятся как «RS», «TR», а `countryLabel()`
добавляет ещё и код → в «Распределении по странам» видно **«RS RS», «TR TR»,
«AE AE»…** (дубль кода, без флагов). Скриншот
`docs/screenshots/qa-audit-dashboard-seed-overview.png`.
**Как надо:** SVG-флаги (sprite) или только текстовый код (`RS`), без emoji-флага;
в аудите — колонка «Страна» та же проблема (1 эмодзи-элемент на room-detail).

### 13.4 Изображения и видео: растягивания не воспроизведены (позитив)

| Объект | Кол-во | Результат |
|--------|--------|-----------|
| `<img>` | 11 (лого 256×256 → 30/34/36/46px) | 0 broken, 0 stretched, 0 upscaled; `object-fit: contain` |
| `<video>` (плитки звонка) | 2 (1280×720 → 932×524) | `object-fit: cover`, stretch=**0.002** |
| Фоны/иконки | — | 0 горизонтального overflow документа на всех 27 поверхностях |

То есть на текущем образе «растягивание на всё поле» не ловится; вероятно, речь о
фиксе `logo/media fit` из 8168d84 — **проверить после редеплоя**.

### 13.5 Скачки интерфейса и уведомления (AX-07, P3)

| Событие | Сдвиг тулбара | CLS (layout-shift) | Перекрытие |
|---------|---------------|--------------------|------------|
| join (экран входа → звонок) | ожидаемо (тулбара ещё нет) | 0.044 | — |
| уведомление о деградации (banner) | **Δ 0 px**, контролы не двигаются | 0.044 (без прироста) | тост×тулбар = **0 px²** |
| открытие side-панели | Δ 0 px | **0.084** (реflow сетки) | — |
| закрытие side-панели | Δ 0 px | **0.128** | — |

Вывод: «скачков контролов при уведомлениях» на текущем образе нет (C5 pass);
остаётся умеренный CLS при открытии/закрытии панелей — можно сгладить анимацией
grid-колонок (P3). Повторить после редеплоя (в HEAD — «notification overlay (no layout shift)»).

### 13.6 Ошибки подключения

| ID | Сценарий | Наблюдение | Сев. |
|----|----------|------------|------|
| AX-04 | Серия быстрых запросов панели | 429 `rate_limited`: UI показывает «Слишком много запросов — подождите немного», сервер отдаёт `Retry-After`; но авто-retry/backoff нет, в консоли 429. При 3-секундном поллинге room-detail и нескольких вкладках лимит 120/мин достижим | P2 |
| AX-05 | Невалидный токен (`token=garbage…`) | Показаны «Не удалось связаться с сервером» и «Не удалось восстановить соединение. Попробуйте перезайти» — причина (битый/просроченный токен) подменяется сетевой ошибкой | P2 |
| AX-06 | Несуществующая комната | На экране входа ошибка не показывается до нажатия «Войти» (rootState=idle), т.е. валидация только по действию | P3 |
| — | Недоступный сервер (`server=http://127.0.0.1:9`) | Понятное состояние «Сервер недоступен · Проверьте адрес… Открыть демо-режим» ✅ | — |
| — | Неверный API-ключ панели | «Нет доступа · Неверный или отсутствующий API-ключ…» ✅ | — |
| — | 404 комнаты в панели | Аккуратная карточка «Запрос отклонён · not_found (HTTP 404) · Room not found» + «Повторить»/«Все комнаты» ✅ | — |

### 13.7 Seed-данные и роли

- **Обзор:** 25 активных комнат, 12 участников онлайн, 8 записей, 26 событий
  безопасности за 24 ч (совпадает с seed: 12 комнат/80 пользователей/341 событие в базе).
- **Распределение по странам:** 8–9 кодов (RS/TR/AE/US/DE/GE/AM/BY…) — но флагами
  не отрисованы (AX-03).
- **Комнаты:** таблица 25 строк, разные высоты строк из-за 2–3 строк текста
  (AX-08); ссылки на удалённые комнаты ведут на 404-карточку (AX-09).
- **Записи:** 8 файлов + «Файлов в хранилище: 41» — экран плотный, KPI консистентны.
- **Роли:** API `/v1/admin/roles` → 6 ролей: `host`, `moderator`, `guest`, **`streamer`,
  `operator`, `viewer`** (кастомные). В задеплоенной панели отдельного nav-пункта
  «Роли» **нет** (9 маршрутов), CRUD-ролей на экранах `access/settings` не найдено —
  вероятно, экран ролей не вошёл в образ (тот же деплой-гэп) — подтвердить после редеплоя.
- Скриншоты: `qa-audit-dashboard-seed-overview/rooms/recordings/audit/roles-access`,
  `qa-audit-dashboard-seed-dark`, `qa-audit-room-detail-404`.

### 13.8 Clickall: «каждая кнопка»

| Страница | targets | clicked | skipped | console/page errors | overflow | CLS |
|----------|---------|---------|---------|---------------------|----------|-----|
| `/` light | 14 | 11 | 3 | 0 / 0 | 0 | 0 |
| `/` dark | 14 | 11 | 3 | 0 / 0 | 0 | 0 |
| `/guardify.html` light | 47 | 9 | 25 | 0 / 0 | 0 | 0 |
| `/guardify.html` dark | 47 | 9 | 25 | 0 / 0 | 0 | 0 |
| `/embed.html` (без room) | 0 | 0 | 0 | 0 / 0 | 0 | 0.0005 |

`/embed.html` без `room` — это экран ошибки «Не указан параметр room» (0 интерактивных
элементов), ожидаемо. Guardify: 25 skipped — destructive-фильтр (кнопки «Свернуть»,
ссылки-действия); ошибок нет.

### 13.9 Иконки и мелкий текст

- Наборы иконок **не менялись**: 117 иконок, 4 геометрических флага (screen/key/reset/
  sparkles), 7 unused, 1 дубль, stroke 1.8/2.0/1.6 — **22 замечания остаются в бэклоге**,
  регрессий a11y нет (95 SVG, 0 безымянных icon-only, 0 пустых кнопок). Детали — `docs/qa-icons.md` §9–§11.
- Мелкий текст: 65 элементов <11px на 27 поверхностях (dashboard 10.5–10.58, editor
  9–10.5, widget 10.5–10.92) — P2 (C7).
- Stroke/размеры иконок в island/Guardify: island 40px-кнопки, Guardify brand-svg
  stroke 2 при widget 1.8 (P3, в qa-icons).

### 13.10 Топ-10 багов по итогам аудита

| # | ID | Баг | Сев. | Скриншот |
|---|----|-----|------|----------|
| 1 | AX-01 | Образ не содержит HEAD 8168d84: SVG-эмодзи, overlay-CLS, logo/media fit не задеплоены | **P1** | bundle-грepы в этом разделе + native-emoji кроп |
| 2 | AX-02 | Реакции/чат-эмодзи — растровые (14px, 13/14/22.4px), меню без SVG | P2 | `qa-audit-emoji-reactions-dpr2.png` |
| 3 | AX-03 | Флаги стран «RS RS / TR TR» (emoji-флаги → буквы) | P2 | `qa-audit-dashboard-seed-overview.png` |
| 4 | AX-04 | 429 rate-limit при интенсивной навигации, без авто-retry | P2 | `qa-audit-dashboard-429.png` |
| 5 | AX-05 | Битый токен маскируется под «сервер недоступен» | P2 | `qa-audit-error-bad-token.png` |
| 6 | — | Бейджи dashboard/editor: 71 пара <4.5 (worst 3.85/4.34) — F-05 | P2 | `qa-audit-dashboard-seed-overview.png` |
| 7 | — | Типографика/контролы C7: 419 дробных размеров, 80 <11px, 318 мелких таргетов | P2 | qa-visual §6 |
| 8 | AX-08 | Разная высота строк таблицы комнат (2–3 строки текста) | P3 | `qa-audit-dashboard-seed-rooms.png` |
| 9 | AX-07 | CLS 0.084/0.128 при открытии/закрытии панелей (тулбар стабилен) | P3 | `qa-audit-chat-panel.png` |
| 10 | AX-06/AX-09 | Валидация комнаты только по действию; ссылки на удалённые комнаты → 404-карточка | P3 | `qa-audit-room-detail-404.png` |

### 13.11 Что в порядке

- Публичные страницы: 0 console/page errors, 0 overflow, CLS≈0, 11 изображений
  0 broken/stretched/upscaled, видео `object-fit: cover`.
- Уведомления: тулбар не смещается, тост не перекрывает контролы (C5), баннер
  о деградации один.
- Ошибки: недоступный сервер, неверный ключ панели, 404 комнаты — понятные состояния
  с действиями.
- Seed-данные читаемы: KPI, таблицы, вкладки комнаты (чат/транскрипт/заметки/compliance/
  записи), записи, аудит; роли доступны через API (6 ролей).
- A11y иконок/кнопок без регрессий; 0 пустых кнопок и 0 безымянных icon-only.

### 12.12 C9 — join/embed адаптив: внутренний скролл и асимметрия (до фикса)

**Проблема от заказчика:** на экране входа в звонок внутри карточки guardify
(≈563×545) появляется вертикальный скроллбар, блоки разного размера — нет симметрии.

**Сборка:** guardify `guardify-CrVA45s_.js` sha256 `5a0ccecf…e98a`, shared
`index-CVIcZIiz.js` `7595b95a…6c3f`, CSS `index-D-pQONvN.css` `4201ff19…dd12`,
widget `/sdk/agil-calls.js` `86c8c176…ff82`; server 0.1.0.
**Скрипт:** `node qa/visual/audit-join-layout.mjs --label=before` (позже
`--label=after`); JSON: `qa/visual/cache/join-layout-before.json`.

| Страница | Вьюпорт | Внутренний скролл (client/scroll) | Symmetry | Обрезано | Pass |
|----------|---------|-----------------------------------|----------|----------|------|
| guardify | 563×545 | `.agil-join` **300/591**, `overflow-y:auto` | 1-col, Δш=0, Δв=31 | 12 | ❌ |
| guardify | 390×844 | `.agil-join` **300/588** | 1-col, Δш=0, Δв=177 | 12 | ❌ |
| guardify | 768×1024 | `.agil-join` **380/714** | 1-col, Δш=0, Δв=92 | 12 | ❌ |
| guardify | 1440×900 | `.agil-join` **380/489** | 2-col, preview/form **233/202 (Δ30)**, h 410/410 | 4 | ❌ |
| embed | 563×545 | `.agil-join` **545/591** | 1-col, Δш=0, Δв=31 | 2 | ❌ |
| embed | 390×844 | нет | 1-col, Δш=0 | 0 | ✅ |
| embed | 768×1024 | нет | 1-col, Δш=0 | 0 | ✅ |
| embed | 1440×900 | нет | 2-col, **454/395 (Δ59)**, h 277/277 | 0 | ❌ |

**Итог: 2/8 кейсов pass.** Причины:
1. `.agil-join` — скроллпорт фиксированной высоты (`flex:1`, `overflow-y:auto`), а
   `.agil-join-card` требует 457–559 px; при карточке 300–380 px контент не влезает:
   появляется внутренний вертикальный скроллбар и обрезанные блоки (главный баг
   заказчика). На guardify карточка ещё и не растёт под контент.
2. В 2-колоночном режиме preview и form разной ширины (233 vs 202; 454 vs 395),
   высоты равны — «нет симметрии» из жалобы; в 1-колоночном ширины равны, но
   карточка выше контейнера → скачок/обрезка.

**Ожидание после фикса (C9 PASS):** контейнер/карточка подстраивается под контент
(или join-лейаут сжимается: компактный preview, меньшие паддинги), `scrollHeight<=clientHeight`,
`overflow-y` не auto/scroll при переполнении, 0 обрезанных; 2-col preview/form
одинаковой ширины/высоты (Δ≤1 px), общие радиусы/отступы; 1-col — стек без скачков.
Скриншоты «до»: `docs/screenshots/qa-join-before-*.png` (8), сырьё —
`qa/visual/screenshots/join-layout/before/**`. «После» — `qa-join-after-*` +
`join-layout/after/**` после деплоя фикса, статус C9 в §1.1 обновляется.

---

## 13. Приложение: скриншоты и скрипты

### Где лежат снимки

| Что | Путь |
|-----|------|
| Все сырые скриншоты | `qa/visual/screenshots/{demo,dashboard,editor,call,call-degraded}/` |
| Сырые метрики (JSON) | `qa/visual/cache/metrics-*.json`, `probe-theme.json`, `probe-widget-theme.json` |
| Курированные для документации | `docs/screenshots/qa-*.png`, обновлённые `demo-light/dark.png`, `dashboard-login.png`, `call-two-participants.png` |

Ключевые снимки:

- `docs/screenshots/demo-light.png`, `demo-dark.png` — лендинг, светлый/тёмный пресеты
  (старый `demo-dark.png` был байт-в-байт равен `demo-light.png` — исправлено);
- `docs/screenshots/qa-landing-mixed-darkscheme.png` — доказательство F-01;
- `docs/screenshots/qa-call-mobile-header-crop.png` — F-02;
- `docs/screenshots/qa-call-grid-light.png`, `qa-call-chat.png`, `qa-call-spotlight.png`,
  `qa-call-minimized.png`, `qa-call-settings-mixed-theme.png`, `qa-call-degraded.png`;
- `docs/screenshots/qa-join-screen-light.png`, `qa-join-mobile.png`;
- `docs/screenshots/qa-dashboard-overview-light.png`, `qa-dashboard-overview-dark.png`,
  `qa-dashboard-audit.png`, `qa-dashboard-room-detail.png`, `qa-dashboard-dialog.png`,
  `qa-dashboard-mobile.png`;
- `docs/screenshots/qa-editor-preview-offline.png`, `qa-editor-dark.png`,
  `qa-editor-theme.png`, `qa-editor-mobile.png`.

После фикс-билда (13.11.2026):

- `docs/screenshots/qa-after-landing-light.png`, `qa-after-landing-dark.png`,
  `qa-after-landing-minimal-dark.png` (F-33), `qa-after-call-mobile-header-crop.png` (F-02 → исправлено),
  `qa-after-call-settings-dark.png` (F-01 → исправлено), `qa-after-call-degraded.png` (F-06 → исправлено),
  `qa-after-call-grid-light.png`, `qa-after-call-chat.png` (F-07 → исправлено);
- `docs/screenshots/qa-after-dashboard-overview-light.png`, `qa-after-dashboard-overview-dark.png`;
- `docs/screenshots/qa-after-editor-preview-fixed.png` (F-34 → исправлено: ready, overlay 0×0);
  `qa-after-editor-preview-handshake.png` — диагностический снимок старого дефекта («Не указан параметр room»);
- `docs/screenshots/qa-after-guardify-hero.png`, `qa-after-guardify-call.png`,
  `qa-after-guardify-two.png`, `qa-after-guardify-mobile.png` (новая Guardify-демо, F-35);
- `docs/screenshots/qa-after-island-light.png`, `qa-after-island-dark.png`,
  `qa-after-island-mobile.png`, `qa-after-island-popover.png` (island-лейаут);
- сырые after-скриншоты: `qa/visual/screenshots/{demo,call,dashboard,editor,new-features}/`;
  probes: `qa/visual/screenshots/probes/`.

Тотальный аудит (13.11.2026): 20 курированных снимков `docs/screenshots/qa-audit-*.png`:

- панель с seed-данными: `qa-audit-dashboard-seed-overview.png` (флаги «RS RS»),
  `qa-audit-dashboard-seed-rooms.png`, `qa-audit-dashboard-seed-dark.png`,
  `qa-audit-dashboard-recordings.png`, `qa-audit-dashboard-audit.png`,
  `qa-audit-dashboard-roles-access.png`, `qa-audit-room-detail-404.png`, `qa-audit-dashboard-429.png`;
- эмодзи: `qa-audit-emoji-reactions-dpr2.png`, `qa-audit-emoji-reactions-dark.png`,
  `qa-audit-emoji-float-dpr2.png`, `qa-audit-call-reactions-picker.png`;
- движение/уведомления: `qa-audit-notifications.png`, `qa-audit-chat-panel.png`,
  `qa-audit-call-settings.png`;
- ошибки: `qa-audit-error-bad-token.png`, `qa-audit-error-bad-server.png`,
  `qa-audit-error-dashboard-key.png`;
- clickall: `qa-audit-clickall-landing-light.png`, `qa-audit-clickall-guardify.png`.

C9 join-layout (13.11.2026, сборка guardify `CrVA45s_` + shared `CVIcZIiz`; «до»):
`qa-join-before-guardify-563x545.png`, `qa-join-before-guardify-390x844.png`,
`qa-join-before-guardify-768x1024.png`, `qa-join-before-guardify-1440x900.png`,
`qa-join-before-embed-563x545.png`, `qa-join-before-embed-1440x900.png`,
`qa-join-before-guardify-card-563x545.png`, `qa-join-before-guardify-card-1440x900.png`.
После фикса — `qa-join-after-*` (см. §12.12).

### Как воспроизвести

```powershell
cd E:\Claude\VideoCalls
$env:ADMIN_KEY='<admin-key>'          # вывод: ssh root@95.182.94.7 'cat /root/agil-admin-key.txt'
node qa/visual/run-demo.mjs            # лендинг: 5 пресетов × light/dark + 4 вьюпорта + mixed-состояния
node qa/visual/run-call.mjs            # звонок: join, 2 участника, панели, spotlight, mini, fullscreen, пресеты
node qa/visual/run-call-mobile.mjs     # мобильные join/чат
node qa/visual/run-dashboard.mjs       # логин, 10 экранов, вкладки, диалоги, адаптив
node qa/visual/run-editor.mjs          # 15 секций редактора, пресеты, адаптив
node qa/visual/probe-theme.mjs         # проверка схемы/пресетов на лендинге
node qa/visual/probe-widget-theme.mjs  # проверка темы виджета по CSS-переменным
node qa/visual/probe-notifications.mjs --before|--after   # дубль уведомлений + перекрытие тулбара
node qa/visual/probe-editor-preview.mjs --before|--after  # iframe/offline превью редактора

# приёмка после деплоя (все стадии + пробники + verdict + публикация after-скриншотов)
node qa/visual/run-all-after.mjs
node qa/visual/run-all-after.mjs --skip-capture   # только пересчёт по текущим JSON
node qa/visual/verify-after.mjs                   # таблица C0–C8 + after-verdict.md
node qa/visual/publish-after.mjs                  # qa-after-*.png в docs/screenshots
node qa/visual/run-new-features.mjs               # /guardify.html + island-лейаут (PATCH layout + restore)
node qa/visual/probe-notifications.mjs --after    # дубль уведомлений
node qa/visual/probe-editor-preview.mjs --after   # превью редактора: origin/ready

# тотальный аудит (13.11.2026)
node scripts/qa-clickall.mjs --page=/ --scheme=light --out=qa/visual/clickall
node qa/visual/audit-surfaces.mjs              # img/video/emoji/tiny/overflow по 27 поверхностям
node qa/visual/audit-emoji.mjs                 # реакции/чат-эмодзи/флаги, DPR 1/2
node qa/visual/audit-motion.mjs                # CLS/уведомления/ошибки (PATCH+restore конфига)
node qa/visual/run-data-surfaces.mjs           # seed-данные: обзор/комнаты/записи/доступы
node qa/visual/probe-contrast-matrix.mjs       # C4: 5 пресетов × схемы × OS + виджет + бейджи
node qa/visual/audit-join-layout.mjs --label=before   # C9: join/embed, 563×545/390/768/1440
node qa/visual/audit-join-layout.mjs --label=after    # после фикса (перезапишет join-layout-after.json)
```

После фикс-прогона: `qa/visual/cache/after-verdict.json|md`, `qa/visual/cache/after-run.log`,
`qa/visual/baseline/2026-11-13-before/` (baseline «до» не перезаписывается).

Baseline «до»: `qa/visual/baseline/2026-11-13-before/` (метрики + скриншоты) — не
перезаписывается стадиями; after-прогон пишет в обычные `qa/visual/cache/` и
`qa/visual/screenshots/`, поэтому diff всегда доступен.

Скрипты идемпотентны, конфиг тенанта восстанавливают в `finally`, ключ берут из env
и нигде не печатают. Требуется Chrome/Edge (автоопределение или `CHROME_PATH`).



