diff --git a/docs/specs/213-device-marker-geometry.md b/docs/specs/213-device-marker-geometry.md new file mode 100644 index 00000000..7d3ca21e --- /dev/null +++ b/docs/specs/213-device-marker-geometry.md @@ -0,0 +1,542 @@ +# Issue #213 — геометрия, размеры, LQI и hover маркеров устройств + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/213 +- **Связанные завершённые задачи:** #179, #211, #212 +- **Нормативный дизайн-пакет:** архив #179, версия 1.1.1 +- **Тип:** bug / polish, обычный трек +- **Пользовательское изменение:** да +- **Touch editor:** best effort / intentionally degraded + +## 1. Сценарий и персона + +**Персона:** домочадец или гость, который читает состояние дома в View/kiosk, +и администратор, который проверяет те же маркеры в редакторе устройств. + +**Сценарий:** пользователь меняет масштаб плана либо открывает House Plan на +экране с другим DPR и ожидает, что круг, внешний shell и MDI-глиф останутся +визуально соосными. Маркер со значением должен реагировать на мышь по всей +видимой капсуле, LQI — снова менять цвет плавно, а замок рядом с дверью — +выглядеть как Lock/Unlock из принятого пакета #179. + +## 2. Что человек увидит до и после + +До изменения на отдельных масштабах внутренний элемент выглядит сдвинутым на +один пиксель относительно кольца, MDI-глиф маловат, дверной замок живёт в старой +визуальной системе, LQI перескакивает между тремя цветами, а часть видимой +капсулы не включает hover. После изменения все концентрические слои остаются +визуально по центру, фактический размер маркера не меняется, глиф становится на +10% крупнее, LQI снова использует плавную шкалу, дверной замок соответствует +#179, а вся капсула является mouse-hover областью. + +## 3. Проблема и подтверждённые причины + +Текущий renderer после #212 вычисляет core так: + +```text +resolved card size × per-marker scale × --device-visual-factor(0.9) +``` + +Затем shell, border и padding рассчитываются отдельными дробными выражениями. +Flex layout позиционирует core через независимо округлённые shell/padding/border. +На проверенной матрице браузерных размеров геометрические центры отличаются на +layout quantum; на отдельных scale/DPR антиалиасинг превращает его в заметную +асимметрию на один физический пиксель. Постоянный `translate(1px)` исправил бы +только один случай и сломал соседние размеры. + +Дополнительно подтверждено: + +1. общий `--device-visual-factor: 0.9` остаётся отдельным поздним множителем; +2. MDI viewport равен `0.5 × core`; +3. opening lock использует старый компактный круг, зелёный locked и не использует + shell/core Lock/Unlock пакета #179; +4. `.device-shell` не владеет pointer hit testing, а круглая `dev::before` + область не покрывает вытянутую часть `with-values`; +5. `markerLqiColor()` после #179 возвращает три фиксированных цвета, хотя общий + `lqiColor()` уже содержит прежний непрерывный red→green HSL-gradient. + +Текущий smoke проверяет отношения размеров на 32/56/96 px, но не прогоняет +дробную матрицу, несколько DPR и pixel-centroid симметрию. + +## 4. Нормативные источники и приоритет + +При расхождении применяются: + +1. решения владельца в теле #213 и последующих комментариях; +2. это ТЗ после зелёного SPEC-REVIEW; +3. дизайн-пакет #179 версии 1.1.1 и принятые уточнения #211; +4. контракты #212 для фактического размера, 44×44 hit area и pointer modality; +5. `docs/SCOPE.md`, `docs/TOUCH-SUPPORT.md`, `docs/UX-MODES.md` и пользовательское + руководство; +6. текущая реализация как compatibility baseline там, где источники выше ничего + не меняют. + +SVG Lock/Unlock из #179 нормативны по форме, отношению shell/core, глифу, цветам, +stroke и shadows. Как и в #179/#211, production использует динамический DOM/MDI, +а не включает reference SVG в runtime bundle. Для Dark применяется принятый +No-Blur fallback; owner override Dark Unlock остаётся amber. + +## 5. Цели + +1. Убрать scale-зависимое визуальное расхождение центров shell/core/glyph. +2. Удалить отдельный поздний коэффициент `0.9`, сохранив текущие фактические + размеры всех существующих настроек и поверхностей. +3. Увеличить базовый MDI viewport на 10% без отдельного glyph multiplier. +4. Перевести opening Lock/Unlock на принятую визуальную систему #179 без + изменения security/action contract. +5. Сделать всю видимую value/legacy capsule mouse-hover областью. +6. Вернуть marker LQI прежнюю плавную цветовую шкалу, меняя только цвет. + +## 6. Scope + +В задачу входят: + +- общий device face в full View/kiosk, Device editor/preview и + `houseplan-space-card`; +- base-size resolution и fallback/default bases, включая vacuum puck; +- круглый Icon, основной Text и Double/value/legacy sections; +- MDI glyph viewport для динамических и пользовательских MDI-иконок; +- opening lock badge в View и его light/dark/locked/unlocked/unknown projection; +- marker-only LQI color; +- instance-local mouse hover для полной внешней капсулы; +- unit/source-contract, browser smoke, visual/golden и performance evidence; +- пользовательская, архитектурная и testing-документация. + +## 7. Не входит в задачу + +- изменение сохранённых координат, `icon_size`, per-marker `size`, angle, + display/value/action либо backend schema; +- изменение фактического внешнего размера marker относительно v1.65.0-beta.8; +- изменение размера core/shell из-за увеличения MDI-глифа; +- новые пользовательские scale, LQI или hover настройки; +- изменение источника, агрегации, форматирования, доступности либо числового + значения LQI; +- изменение low/mid/high категорий в aria-label/data attributes; +- изменение непрерывной комнатной LQI-заливки; +- расширение либо сокращение click/tap/action области value capsule; +- новый hover на touch/pen, sticky hover или новая touch-editor parity; +- изменение lock service calls, подтверждения Unlock либо secure invariant; +- backdrop-filter на marker/opening lock; +- принятие golden baseline на Windows или выпуск без команды владельца. + +## 8. Контракт размеров + +### 8.1. Удаление позднего `0.9` + +После изменения `.dev` и vacuum puck не содержат +`--device-visual-factor`, эквивалентного post-resolution multiplier либо цепочки +`resolved × 0.9` внутри face geometry. + +Размер, переданный общему face как base, уже является текущим effective base. +Для v1.65.0-beta.8 reference: + +```text +new effective core = current effective core +new effective shell = current effective shell +new effective puck = current effective puck +``` + +Это выполняется для default card, явного `icon_size`, kiosk multiplier, +per-marker `size`, preview и static card. Числовые пользовательские значения и +их относительные отношения не меняются. Нормализация legacy public size units, +если она нужна для совместимости, принадлежит единой границе base resolution; +внутри face нет второго коэффициента. + +Base constants меняются на текущие effective values: fallback full/static, +preview и vacuum используют уже уменьшенные значения. Запрещено сохранять +старые bases и маскировать их новым переименованным `0.9` в CSS. + +### 8.2. MDI-глиф + +Базовый viewport MDI меняется с `0.50 × core` на `0.55 × core`. Это изменение +самого base ratio, без `--glyph-factor`, вложенного `scale(1.1)` либо второй +трансформации. + +Правило одинаково для default, custom MDI и state-swapped glyph; marker angle +по-прежнему вращает глиф вокруг центра. Core, shell, anchor, pulse и hit area от +увеличения glyph не меняются. Reference/runtime matrix должна подтверждать, что +глиф не клиппится на минимальном размере. + +### 8.3. Совместимость + +- storage/card config не переписывается; +- Open → Save не материализует новые поля или bases; +- default и явно настроенные marker sizes сохраняют текущий pixel result с + допуском layout quantum; +- relative ratio между двумя разными per-marker size остаётся прежним; +- координата остаётся центром icon core; +- минимальная интерактивная область остаётся не меньше 44×44 CSS px. + +## 9. Контракт соосности и растеризации + +### 9.1. Геометрия + +Shell и core круглого marker строятся от одной координаты центра. Позиция core +не выводится из независимо округлённой суммы left/top padding и border. + +Для Text/Double/value/legacy shell: + +- сохранённая точка остаётся центром icon core; +- прямые стороны и полукруглый торец сохраняют inset пакета; +- core и соответствующий ему круглый конец внешней capsule имеют общий центр; +- расширение секций идёт только в выбранную сторону; +- смена стороны не меняет anchor. + +Допустимы CSS grid, явные tracks, общий concentric pseudo-layer либо другой +layout без per-size special cases. Запрещены hardcoded `translateX/Y(1px)`, +таблица исключений по viewport и UA/DPR sniffing. + +### 9.2. Проверяемая симметрия + +На матрице base core от 24 до 112 CSS px, включая дробные значения с шагом не +крупнее 0.25 px, и DPR `1`, `1.25`, `1.5`, `2`: + +- DOM-centres core и его shell-end отличаются не более чем на один browser + layout quantum; +- alpha/color-weighted painted centroids расходятся менее чем на 0.5 физического + пикселя по каждой оси; +- противоположные видимые inset не дают устойчивой однопиксельной ступени; +- проверка умеет покраснеть на v1.65.0-beta.8 либо на документированном mutant, + восстанавливающем прежний layout. + +Golden остаётся визуальным доказательством на выбранных failing-before-fix +размерах; один DOM `getBoundingClientRect()` без pixel check недостаточен. + +## 10. Opening Lock/Unlock + +### 10.1. Известные состояния + +Значок замка у двери/ворот использует визуальный контракт Lock/Unlock #179: + +- package shell/core ratio и concentric geometry; +- Locked: black/`#252525` theme projection и белый locked glyph; +- Unlocked: amber `#F0A00C`, с theme-specific glyph из принятого reference; +- Light/Dark stroke, inner shadow и external shadows из #179/#211; +- Dark No-Blur production fallback; +- корректный closed/open lock MDI path, а не перекраска одного glyph. + +Opening badge сохраняет нынешний компактный общий footprint, позицию возле +створки и способ вычисления offset. Package ratios применяются внутри этого +footprint; он не превращается в полноразмерный device marker и не участвует в +device layout/declump. + +### 10.2. Unknown и действия + +Unknown/unavailable сохраняет нейтральный `lock-question` смысл, но использует +тот же новый shell/core каркас без ложного locked/unlocked цвета. + +Клик по opening badge только открывает карточку проёма. Lock выполняется лишь +явной кнопкой карточки, Unlock — после подтверждения. Pointer/keyboard paths +маркеров устройств не получают нового lock action. Скрытый/disabled/missing +exact opening reference ведёт себя по прежним #117 правилам. + +## 11. Hover всей внешней capsule + +Для `.device-shell.with-values` видимая border-box capsule целиком является +mouse-hover областью, включая: + +- core; +- промежуток между core и value section; +- value/legacy sections; +- внутренний padding до внешнего stroke; +- все четыре направления right/bottom/left/top. + +Над любой точкой capsule активируются те же visual hover, z-index и tooltip, +что над core. Hover остаётся instance-local и разрешён только canonical +`data-pointer-hover` после настоящей fine mouse. Touch/pen и compatibility mouse +не включают и не оставляют hover; unavailable по-прежнему не получает visual +hover. + +Расширение касается hover/tooltip. Click/tap/action hit contract и 44×44 core +target не расширяются скрыто на value sections. Реализация обязана отделить +hover ownership от action ownership, если browser bubbling иначе превратил бы +всю capsule в новую click target. + +Preview воспроизводит визуальный hover при переданном mouse gate, но остаётся +неинтерактивным. Static plan marker остаётся read-only; его footer button не +имеет отношения к этой capsule. + +## 12. Плавный marker LQI + +Цвет числа LQI снова использует прежнюю непрерывную функцию: + +```text +<= 40 → hue 0 (red endpoint) +40..180 → linear hue 0..120 +>= 180 → hue 120 (green endpoint) +``` + +Нормативный output совпадает с общим `lqiColor()`: +`hsl(round(hue), 85%, 55%)`. Внутри диапазона соседние значения меняют hue +плавно; на бывших границах `40/41` и `179/180` нет перехода между отдельными +palette colors. + +Меняется только `presentation.lqiColor` marker. Сохраняются: + +- resolved average/source и число; +- `show_signal`, отсутствие missing/unavailable подписи и LQI=`0`; +- low/mid/high `lqiBand` для понятного aria-label и data attributes; +- позиция строки под shell и Double-bottom offset; +- room fill gradient и room tooltip color. + +Три package цвета #179 больше не являются нормативом для marker LQI, но +остаются semantic colors других состояний и pulse. + +## 13. UX, accessibility и touch + +- View и kiosk на desktop/touch остаются release-blocking; +- essential state/action не зависит от hover; +- focus-visible не зависит от mouse gate; +- aria-label продолжает содержать число LQI и категорию low/mid/high; +- увеличение glyph не меняет accessible name/role/tab order; +- opening lock сохраняет существующий понятный glyph и click-to-info; +- Device editor остаётся desktop-first, а его touch safety floor не меняется; +- pan/pinch/long press/pointercancel не создают hover или action. + +## 14. Данные, migration и i18n + +Новых config/layout/localStorage/backend полей нет. Schema и сериализация не +меняются, data migration не нужна. Existing card/marker size values должны +читаться с тем же фактическим результатом. + +Новых пользовательских строк не ожидается. Существующие RU/EN LQI a11y bands +сохраняются; изменить нужно только описание цветовой шкалы в руководствах. Если +реализация потребует новый текст, обязательна RU/EN parity без расширения +пользовательских настроек. + +## 15. Архитектурный контракт + +```text +card config / marker scale / surface base + → one resolved effective base size + → resolveDevicePresentation + → renderDeviceFace + → one concentric face geometry on full / preview / static + +resolved marker LQI + → continuous marker color + unchanged semantic band + → shared full / preview / static face + +opening exact lock state + → shared #179 lock visual tokens/geometry + → opening badge → click opens info only + +real mouse modality + → full capsule hover owner + → visual hover + tooltip; action owner remains unchanged +``` + +Ожидаемые зоны изменений: + +- `src/styles.ts` — base values, concentric layout, glyph base ratio, capsule + hover и opening-lock tokens; +- `src/device-presentation.ts` — continuous marker LQI color при сохранённом band; +- `src/device-face.ts` — только если нужен явный hover/action layer; +- `src/houseplan-card.ts` — resolved full base, opening lock DOM и hover owner; +- `src/space-render.ts`, `src/hp-device-preview.ts` и vacuum styles — только для + base/parity wiring, второй renderer запрещён; +- unit/source tests, targeted smokes, reference capture, docs и changelogs. + +Нельзя вводить второй device face renderer, отдельную LQI-функцию с другой +формулой, global pointer latch, per-marker ResizeObserver или per-frame JS. + +## 16. Edge cases + +- default и explicit global `icon_size`, kiosk scale и per-marker 0.5…3; +- base 24…112 px, дробные quarter-pixel значения, DPR 1/1.25/1.5/2; +- Light/Dark hot switch и цветной план; +- Icon, Text, Double right/left/top/bottom и legacy third section; +- длинное значение, CJK/кириллица, `0`, `false` и unit; +- selected/focus/active/alarm/virtual/unavailable вместе с новой geometry; +- custom MDI, rotated glyph и state-swapped lock/cover/light icons; +- opening lock locked/unlocked/locking/unlocking/unknown/unavailable; +- opening у двери и ворот, hidden openings и registry-less exact lock; +- LQI `0`, `1`, `39`, `40`, `41`, `42`, `178`, `179`, `180`, `181`, `255`, + missing и value badge, который сам показывает LQI; +- mouse впервые входит с capsule section, touch→mouse hybrid, pointerleave, + space/mode/hidden/disconnect cleanup; +- two cards on one page and non-interactive preview/static surfaces; +- 200 markers without added layout loop/compositor layer. + +## 17. Acceptance criteria + +1. **AC1 — size contract.** Отдельный `--device-visual-factor`/эквивалентный + late multiplier удалён; default, explicit global/per-marker, kiosk, preview, + static и vacuum сохраняют effective v1.65.0-beta.8 size с допуском одного + layout quantum. **Доказательство:** source/unit facts + computed-style browser + matrix + reference capture. +2. **AC2 — glyph.** MDI viewport использует прямой base `0.55 × core`, без + дополнительного multiplier/transform; core/shell/anchor не меняются и glyph + не клиппится. **Доказательство:** source assertion + icon/custom/rotation + browser smoke + 32/56/96 reference. +3. **AC3 — concentric geometry.** Icon/Text/Double shell-end и core выполняют + §9 на дробной size/DPR matrix; нет hardcoded pixel compensation. + **Доказательство:** DOM-centre assertions + pixel-centroid smoke, красный до + фикса или на documented mutant + reviewed golden. +4. **AC4 — anchor/parity.** Saved coordinate, side anchor, relative scale, + Text expansion и 44×44 minimum action area не меняются на full, preview и + static surfaces. **Доказательство:** unit layout facts + preview/static/icon + scale smokes. +5. **AC5 — opening locks.** Door/gate locked и unlocked совпадают с #179 по + package layers/tokens в Light/Dark при сохранённом compact footprint; + unknown нейтрален. **Доказательство:** reference/runtime visual matrix + + opening browser smoke/golden. +6. **AC6 — lock security.** Opening badge только открывает info; lock/unlock + service и confirmation invariant не меняются, disabled/missing references не + рисуют ложный action. **Доказательство:** existing opening binding, + registry-less, lock-action и lock-invariant smokes. +7. **AC7 — capsule hover.** На всех четырёх позициях любая точка внешней + capsule включает один mouse visual hover/z-index/tooltip; unavailable, + touch/pen и compatibility mouse не включают его. **Доказательство:** real + PointerEvent desktop/hybrid browser smoke + computed styles. +8. **AC8 — action boundary.** Value/legacy section не становится новым click/ + tap action target; core/44×44 action semantics, confirmation и gesture safety + прежние. **Доказательство:** pointer target/action-count smoke. +9. **AC9 — continuous marker LQI.** Marker color совпадает с `lqiColor()` на + representative и boundary-adjacent values; 40/41 и 179/180 не используют + дискретные palette jumps. **Доказательство:** pure boundary/dense-range tests + + browser computed color. +10. **AC10 — only color changes.** LQI text/source/format/visibility/position, + low-mid-high aria/data band и room fill/tooltip gradient не меняются. + **Доказательство:** presentation/a11y unit + existing room fill regression. +11. **AC11 — data/i18n compatibility.** Schema, saved settings и RU/EN key set + не меняются; Open → Save не материализует defaults. **Доказательство:** + serialization/i18n unit + code review. +12. **AC12 — performance.** Нет per-frame JS, per-marker observer/media listener, + backdrop-filter, UA/DPR sniffing либо роста постоянных animation layers; + 200-marker budget не ухудшен материально. **Доказательство:** source review + + pre-beta performance smoke. +13. **AC13 — release artifacts.** Оба changelog, RU/EN guide, architecture, + testing, targeted smoke/reference и visual fingerprints актуальны. + **Доказательство:** docs diff + `check-docs` + release review. + +## 18. План автотестов + +### 18.1. Цикл реализации + +```bash +npm run typecheck +npm test +npm run build +node scripts/check-docs.mjs --external +``` + +Unit/source coverage: + +- отсутствие late 0.9 factor и наличие canonical effective bases; +- direct glyph base 0.55; +- shared concentric layout contract без fixed pixel nudge; +- dense continuous LQI color и неизменный band/a11y; +- opening-lock state/token projection; +- hover/action ownership inventory; +- config/i18n parity. + +### 18.2. Targeted browser smoke перед S7 + +Обязательны: + +- расширенный `demo/smoke_device_icon_design.mjs`; +- новый либо отдельный pixel-alignment smoke с fractional size/DPR matrix; +- `demo/smoke_device_preview_parity.mjs`; +- `demo/smoke_static_icon.mjs`; +- `demo/smoke_icon_scale.mjs`; +- `demo/smoke_state_value.mjs`; +- capsule hover/action smoke для четырёх badge positions; +- `demo/smoke_touch_tips.mjs` и hybrid pointer regression; +- `demo/smoke_opening_binding.mjs`; +- `demo/smoke_registryless_opening.mjs`; +- `demo/smoke_lock_action.mjs` и `demo/smoke_lock_invariant.mjs`. + +До исправления требуется зафиксировать красное доказательство минимум для +alignment, late 0.9 source contract, glyph 0.50, opening-lock visual, capsule +hover и categorical marker LQI. Critical guards получают mutation proof либо +эквивалентное documented failing-before-fix сравнение. + +### 18.3. Golden и pre-beta + +Light/Dark golden/reference matrix включает: + +- 32/56/96 и выбранные дробные failing sizes; +- Icon, Text, Double four-side/legacy; +- default/hover/focus/active/alarm/virtual/unavailable; +- custom/rotated glyph; +- opening Lock/Unlock/Unknown рядом с SVG #179; +- LQI low-end, несколько промежуточных оттенков и high-end; +- desktop hover и post-touch no-hover. + +Windows capture диагностический. Baseline принимается только из reviewed полного +Linux CI artifact. Перед beta выполняются full smoke, golden и performance gates +по release runbook; backend не затронут. + +## 19. Риски и меры + +| Риск | Мера | +| --- | --- | +| Удаление 0.9 увеличит custom sizes | effective-size matrix для default/explicit/kiosk/per-marker до и после | +| Новый layout сдвинет anchor или side capsule | shared anchor facts и four-side smoke | +| Pixel тест окажется привязан к платформе | DOM invariant + alpha centroid; golden canon только Linux | +| Больший glyph клиппится у сложного MDI path | representative custom/state-swapped matrix на min size | +| Opening lock станет слишком крупным | сохраняется текущий compact footprint, меняются внутренние ratios | +| Hover capsule расширит click target | разные hover/action owners и action-count smoke | +| Continuous marker LQI затронет room fill | reuse `lqiColor`, отдельный marker projection и room regression | +| Refactor добавит layout/compositor cost | CSS-only geometry, no observers/blur, pre-beta performance | + +## 20. Откат + +Откат одним implementation revert возвращает прежний layout, factor, glyph, +opening badge, hover и marker LQI colors. Миграции данных нет, поэтому storage +rollback не нужен. Golden baseline возвращается только через нормативный reviewed +Linux artifact, не ручным принятием Windows capture. + +## 21. Release-артефакты + +В user-visible implementation commit одновременно обновляются: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #213; +- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`: effective size без отдельного + factor, glyph, smooth marker LQI, opening lock и capsule hover; +- `docs/ARCHITECTURE.md`: base-size boundary, shared concentric face и + marker-LQI band/color separation; +- `docs/TESTING.md`: fractional/DPR pixel alignment, lock reference и + hover/action/LQI regressions; +- reference/golden/source fingerprints и screenshots, если их источник изменён. + +Release note: исправлено центрирование новых маркеров на разных масштабах, +увеличены внутренние глифы, дверные замки приведены к новому дизайну, hover +охватывает всю капсулу, а LQI снова использует плавную цветовую шкалу. Сначала +beta; stable — только после promotion rule. + +## 22. План реализации + +1. Добавить failing source/unit/browser guards для шести подтверждённых проблем. +2. Перенести current effective sizes на единую base-resolution boundary и + удалить late factor из face/puck. +3. Перестроить concentric shell/core tracks и увеличить direct glyph base. +4. Подключить full-capsule hover с отдельным action owner. +5. Вернуть continuous marker LQI color при неизменном band/a11y. +6. Перевести opening lock на #179 tokens/geometry, сохранив compact footprint и + security path. +7. Обновить parity surfaces, docs/changelogs/reference fixtures. +8. Выполнить fast gates и все named smokes §18.2, затем передать в S7. +9. Перед beta выполнить полный Linux golden/smoke/performance gate. + +## 23. Принятые технические предположения + +Следующее принято предположительно и может быть изменено ревьюером без нового +продуктового решения, если все AC сохраняются: + +1. Opening lock сохраняет нынешний компактный footprint и offset; #179 задаёт + его внутренние proportions/tokens, а не новый абсолютный размер. +2. Unknown opening lock использует package shell/default theme и текущий + `lock-question`, потому что архив не содержит отдельного Unknown asset. +3. Запрос владельца расширяет именно hover/tooltip value capsule; click/tap + target остаётся прежним, так как отдельного решения расширить действие нет. +4. Low/mid/high band остаётся только semantic/a11y facet; цвет независимо + разрешается общей continuous `lqiColor()`. +5. Base compatibility может нормализоваться один раз на границе surface size, + но внутри `.dev`, puck и face запрещён любой поздний 0.9-equivalent factor. +6. Pixel-centroid smoke использует controlled solid background и color/alpha + segmentation; точные thresholds калибруются failing-before-fix/mutant и не + становятся browser-vendor whitelist. +7. Общий concentric layout предпочтительнее размера, округлённого через CSS + `round()`: последний зависит от поддержки WebView и не решает shared-center + invariant сам по себе.