# Гейт-контракт «живая кнопка-проводник» (Вариант A)

> Переносимый декларативный контракт для кнопки продолжения на экранах с обязательными полями.
> Артефакт для **дизайн-системы** и для **ТЗ разработчику**. Стек-агностично: описывает контракт + поведение + a11y, **не реализацию**. Эталонная vanilla-реализация — `gate.js` (+ правила `components.css`), но любой стек, выполняющий контракт, даёт тот же UX.

## Зачем
Прежняя кнопка в неактивном состоянии имела `pointer-events:none` → тап по ней не давал ничего («мёртвый тап»): пользователь не понимал, что не так и сколько осталось. Контракт делает кнопку **всегда живой**: она либо ведёт дальше, либо **проводит** к первому незаполненному полю.

## Контракт (декларативные атрибуты)
| Атрибут | На чём | Назначение |
|---|---|---|
| `data-gate` | корень-предок (scope) | граница, в которой контроллер считает поля |
| `data-gate-field` | поле/группа-вопрос | «я — обязательное поле этого гейта» |
| `data-filled` | поле | **единственный сигнал «отвечено»**. Ставит адаптер ввода; при ре-рендере — как производная модели |
| `data-gate-scope` + `hidden` | под-секции | multi-step: видимая секция = активный scope (напр. домен опросника) |
| `data-hard-gate` | scope | жёсткий стоп (напр. кризис-перехват): продолжать нельзя, даже если всё заполнено. Ставится **синхронно** с `data-filled` |
| `data-gate-hardgate` / `[role=alert]` | блок-перехват | цель переноса фокуса при жёстком стопе |
| `data-gate-state` = `pending` \| `ready` \| `blocked` | кнопка | состояние. **Пишет только контроллер**, не разметка |
| `data-gate-count` + `aria-live="polite"` | строка-хинт (опц.) | живой счётчик «Осталось ответить — N» |

**Разделение ответственности:** адаптер типа ввода знает одно — когда выставить `data-filled`. Контроллер не знает о типах инпутов, только о сигнале. Поэтому текст/радио/сегмент/шкала/слайдер/любой будущий контрол подключаются одинаково.

## Требования к поведению
- **Т1.** Кнопка в `pending` — визуально приглушена, но **кликабельна и фокусируема** (никакого `pointer-events:none`).
- **Т2.** Когда все `data-gate-field` в активном scope несут `data-filled` и нет `data-hard-gate` → состояние `ready` (полный акцент), активация ведёт дальше.
- **Т3.** Активация (click / Enter / Space) кнопки в `pending` → НЕ переход, а: прокрутка к первому `data-gate-field` без `data-filled` + визуальный спотлайт-кольцо + перенос фокуса на него.
- **Т4.** `Enter` на `ready`-кнопке ведёт дальше (нет «мёртвой клавиши»). Активацию ловит единый guard на предке (capture), чтобы сработать раньше любых пер-экранных обработчиков.
- **Т5.** Живой счётчик (где включён) обновляется в `aria-live` региону при каждом изменении заполненности.
- **Т6.** `data-hard-gate` приоритетнее полноты: при нём состояние `blocked` (кнопка продолжения скрыта/недоступна, возврат — растянут), **без единого кадра `ready`**, фокус один раз переносится на блок-перехват (`role=alert` озвучивает).
- **Т7.** Multi-step: счёт и «первый незаполненный» — только в пределах видимого scope; возврат на уже заполненную секцию → `ready`.
- **Т8.** `pending`-активация на multi-step НЕ перелистывает секцию (guard гасит до пер-экранной навигации).
- **Т9.** `prefers-reduced-motion` → спотлайт статичный, без плавной прокрутки.
- **Т10.** Деградация без JS: `pending` приглушена-но-тапабельна, ссылка ведёт дальше (не блокирует жёстко).

## Приёмочные тесты
1. Пустой экран: тап/Enter/Space по кнопке → спотлайт+фокус на первом поле, перехода нет; немого тапа нет.
2. Всё заполнено, без hard-gate: Enter по кнопке → переход.
3. Multi-step: вернуться на заполненную секцию → кнопка `ready`; счётчик секции верен.
4. Hard-gate (кризис): заполнить всё + триггер → `blocked`, кнопка продолжения скрыта, **ни одного кадра `ready`**, фокус на перехвате.
5. `pending`-тап на multi-step → секция НЕ листается.

## Перенос на любой стек
Контракт — это **атрибуты + сигнал `data-filled` + поведение Т1–Т10**, а не код. В компонентном фреймворке (React/Vue/др.): поле выставляет `data-filled` из своего state; общий хук/директива реализует контроллер (скан scope, состояние кнопки, спотлайт, счётчик, hard-gate). UX и a11y идентичны. В ТЗ разработчику передаётся этот документ (уровень требований; конкретную реализацию выбирает разработчик — ADR-13).

## Эталон
`gate.js` (контроллер) + `components.css` (`.btn-next[data-gate-state]`, `.gate-spotlight`, `.onbnav.is-hardgated`). Подключение: `gate.js` как канонический ассет ДС наравне с `tokens.css`/`components.css`. Применён на 7 экранах онбординга: identity, safety, sleep, gad7 (чисто), program-fit (разнотипные инпуты), tfi (scope=домен), phq9 (hard-gate на кризис-перехвате п.9) + шаблоны программ `program/templates/questionnaire` и `program/templates/exercise`.
