feat(moon): the moon with any background, and its status in General settings (#718)

The owner decided on 30.09 that the moon is not part of the "Follow the Sun"
environment but a switch of its own: with a static background (global or a
space's own) the card showed no moon even with the switch on, and the switch
said nothing about why the moon was missing right now.

With a static background there is no environment, so the moon stands in its
own layer, `.hp-moon-sky`: the first child of `.stage` / `.hp-static-stage`,
the whole scene, no z-index, filter or will-change, under the plan by DOM
order, fading with the #101 View weight. Inside is the very #661 element, so
place, size, art and fades are unchanged, and a background switch moves it to
its new parent in the same render without a flicker. The phase comes from the
same `resolveDayCycle`, computed only while the moon is on and on View; without
`sun.sun` both cards keep their 30 s clock ticker and re-render only when the
phase changes (the environment is still compared by its whole fingerprint).

General settings get a second caption line under the moon switch
(`data-moon-status`): one snapshot per opening, judged by the lazy chunk as if
the switch were on, first reason wins (no home, day, below 3°, under 3 %),
numbers rounded and clamped below the threshold they missed. `moonStatus`
decides "shown" with the same `moonShownAt` as the element. It lives in a
WeakMap beside the draft, so it never makes the dialog dirty; a closed
opening's result is dropped. The dialog loads the chunk through the gate's
loader (`withMoon`), now shared by every caller while a load is in flight, so
there is still one fingerprint check and one retry token.

Bundle (same build, against origin/dev): initial View 300 072 -> 300 248 B gzip
(+176 B, under the 500 B of the spec; budget and ceiling not raised); lazy
editor 238 558 -> 238 991 B (+433 B, the line and English strings); lazy moon
11 385 -> 11 712 B (+327 B, layer CSS and status). `src/moon.ts` stays out of
the initial and the editor graph; bundle-budget now refuses an editor/moon
overlap. Monolith metrics: hostRefs 4 885 -> 4 888 — the three `host.` reads of
`src/editors/moon-status.ts` (hass, `_settingsDialog`, requestUpdate) through
its own three-member interface, not the editor port; the other five metrics
are unchanged. houseplan-editor-runtime.ts grows by two lines (import, call).

Tests: AC9/AC10/AC15 and the sky layer in test/moon.test.mjs (the #661
"static -> nothing" check inverted), AC14 and the opening lifecycle in
test/moon-settings.test.mjs, smokes demo/smoke_moon_static.mjs (AC1-AC6; AC1
and AC3 were red on dev) and demo/smoke_moon_status.mjs (AC11/AC12), AC7 in
smoke_daycycle_layer_budget. Golden: two new scenes
(static-bg-moon-gibbous-white-light, static-bg-moon-crescent-south-dark,
matrix v70), the harness checks the moon's parent by background and waits for
the status line in the General settings frames. Four new mutants; the clock
ticker one is a browser guard (201 at the guideline of 200).

Issue: #718
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 05:25:21 +00:00
committed by claude[bot]
parent 712d41b6d6
commit b998b0b34a
33 changed files with 1553 additions and 146 deletions
+11
View File
@@ -2,6 +2,17 @@
## Unreleased
- The moon now appears with any background, not only "Follow the Sun": with a
static background — the theme's or a colour of your own, for the whole
installation or a single space — it stands in the same top-left corner behind
the plan at dawn, dusk and night, in View, kiosk, the panel and the space
card, while the background keeps its colour. If "Moon over the plan at dusk
and night" was already on, the moon appears on static backgrounds after this
update; switch it off in General settings › Sun and Moon to hide it. Under
that switch a new line tells whether the moon is shown right now and, if not,
why — daytime, the moon below 3°, new moon or no home location in Home
Assistant ([#718](https://github.com/Matysh/houseplan-card/issues/718)).
## v1.79.0-beta.1 — 2026-09-30
- In 2.5D View the plan no longer moves: rooms, furniture, other decor and room
+11
View File
@@ -8,6 +8,17 @@
## Не выпущено
- Луна теперь видна при любом фоне, а не только на «Следует за Солнцем»: при
статичном фоне — фоне темы или своём цвете, для всей установки или одного
пространства — она стоит в том же левом верхнем углу за планом в сумерках и
ночью, в просмотре, киоске, панели и карточке пространства, а фон сохраняет
свой цвет. Если переключатель «Луна на плане в сумерках и ночью» уже был
включён, после обновления луна появится и на статичном фоне; чтобы её убрать,
выключите его в общих настройках, раздел «Солнце и Луна». Под переключателем
появилась строка: видна ли луна сейчас, а если нет — почему (день, луна ниже
3°, новолуние или в Home Assistant не заданы координаты дома)
([#718](https://github.com/Matysh/houseplan-card/issues/718)).
## v1.79.0-beta.1 — 2026-09-30
- В объёмном 2.5D-виде план больше не сдвигается: комнаты, мебель, прочий декор
+13 -8
View File
@@ -134,21 +134,26 @@ An older frontend ignores the field and shows Flat. An older backend preserves
it through the unknown-settings policy. Full backup/import carries `settings`
whole, so the value survives.
## Moon (#661)
## Moon (#661, #718)
`settings.moon` is an optional global boolean: the moon on the "Follow the Sun"
background for every space, user, device and kiosk. Only exact `true` switches
it on; absence, `false` or anything else read as off. Saving `false` removes
the key. There is no per-space moon: a space shows it only when its effective
`bg_mode` is `daynight`. The backend accepts only a boolean
`settings.moon` is an optional global boolean: the moon behind the plan at
dawn, dusk and night for every space, user, device and kiosk, with any
background (#718 — before it, only on "Follow the Sun"). Only exact `true`
switches it on; absence, `false` or anything else read as off. Saving `false`
removes the key. There is no per-space moon switch, and the effective
`bg_mode` of a space is no longer a condition: a space with a static
background, its own or the global one, shows the moon too. The backend accepts only a boolean
(`vol.Optional("moon"): bool`), and the privacy-safe support projection copies
only a validated boolean. New installations get `moon: true` in
`DEFAULT_CONFIG`; an existing config has no key and keeps its look after the
update (docs/SUN.md). "Reset to defaults" in General settings sets `true`. The
store and model versions do not change; there is no migration.
An older frontend ignores the field and shows no moon. An older backend
preserves it through the unknown-settings policy. Full backup/import carries
An older frontend ignores the field and shows no moon; a #661 frontend with a
#718 config shows it only on `daynight`, as before. #718 adds no field and no
migration: an installation that already has `moon: true` with a static
background sees the moon there after the update, and the same switch turns it
off. An older backend preserves it through the unknown-settings policy. Full backup/import carries
`settings` whole, so the value survives; a one-space export does not carry
global settings.
+86 -30
View File
@@ -376,26 +376,26 @@ The wash lives where the Flat `.sunlayer` lives (floor group, above room fills
and Glow) as `.sunlayer.iso-sunwash`, one `.iso-sunbeam[data-opening]` per lit
window. Witness: `demo/smoke_iso_sun.mjs`.
## Moon — `settings.moon` (#661)
## Moon — `settings.moon` (#661, any background since #718)
At dawn, dusk and night on the "Follow the Sun" background, the moon in its
current phase stands in the top-left corner of the scene: a thin crescent, a
half, a full disc. General settings › Sun and Moon › «Moon over the plan at
dusk and night» switches it for the whole installation.
At dawn, dusk and night, with any background, the moon in its current phase
stands in the top-left corner of the scene: a thin crescent, a half, a full
disc. General settings › Sun and Moon › «Moon over the plan at dusk and night»
switches it for the whole installation; there is no per-space moon switch.
**When it is shown** — all at once, otherwise there is no moon:
- `settings.moon === true` (global; absent, `false` or anything else is off);
- the effective `bg_mode` of the space is `daynight` — the moon lives in the
four-phase environment, so a space with its own `static` has no moon, and
there is no per-space moon switch;
- the environment phase is `dawn`, `dusk` or `night` (the same
`resolveDayCycle`, browser-clock fallback included);
- the day-cycle phase is `dawn`, `dusk` or `night`: `resolveDayCycle(hass,
now)`, whatever the background — with a valid `sun.sun` (`dayCycleSunOf`:
finite azimuth and elevation, boolean `rising`) day is elevation ≥ 6°
(exactly 6° is day), otherwise the browser-local clock, day 08:00–18:00. The
effective `bg_mode` of the space is not a condition (#718 K1);
- the topocentric altitude is ≥ 3° (`MOON_ELEVATION_MIN = RAY_ELEVATION_MIN`);
- the illuminated fraction is ≥ 3 % (`MOON_MIN_ILLUMINATION`, about ±1.5 days
around new moon);
- a View surface: View, kiosk or `houseplan-space-card` (editors have no
environment);
- a View surface: View, kiosk, panel or `houseplan-space-card` (editors, the
PDF export and the space dialog preview have no moon);
- `hass.config.latitude/longitude` are finite numbers.
**Where the numbers come from.** Home Assistant publishes no moon altitude
@@ -423,12 +423,29 @@ quantised to 0.01 only so the element changes when the fingerprint does. The
dark side is the same art at 8 % opacity.
**Place, size, layer.** Fixed top-left corner, box `min(200px, 25cqmin)` of the
scene (the environment is the size container), inset 5 % of the box; it does
not move with pan or zoom and never takes the pointer. It is the last child of
`.hp-day-cycle-env`: above the phase gradients and the sun glow, below the plan
paper, rooms, devices, labels and UI. A plan that fills the scene covers the
moon partly or entirely — that is the environment's norm, like the sun glow
(owner decision 7).
scene, inset 5 % of the box; it does not move with pan or zoom and never takes
the pointer. Over "Follow the Sun" it is the last child of `.hp-day-cycle-env`:
above the phase gradients and the sun glow, below the plan paper, rooms,
devices, labels and UI. A plan that fills the scene covers the moon partly or
entirely — that is the environment's norm, like the sun glow (owner
decision 7).
**Static background (#718 K3).** No environment is created — no
`.hp-day-cycle-env`, no `daycycle`/`phase-*` classes, no
`.hp-paper-outline-svg`; the scene keeps the chosen colour or the theme's. The
same `.hp-moon` element stands in its own layer `<div class="hp-moon-sky"
aria-hidden="true">`, the first child of `.stage` (full card) or
`.hp-static-stage` (space card) — where the environment would be. The layer is
the whole scene (`position:absolute; inset:0; overflow:hidden;
pointer-events:none; container-type:size`), so the box and place are the same;
it has no `z-index`, `filter` or `will-change` and lies under the plan by DOM
order (`.zoomwrap` and the space card's plan are `z-index:1`). The layer's
`opacity` is the View weight of the #101 transition, as the environment's;
editors have neither. Switching between `daynight` and `static` (a space tab,
the background segment previewed in the open dialog, a config push) moves the
element to its new parent in the same render, in its final state, on the same
box — no flicker. The chunk renders and styles the layer; until it is here
there is no layer.
**Movement.** Opacity only, 2 s on the background curve (`RAY_FADE_MS`),
none under `prefers-reduced-motion`: rising through 3°, setting through it,
@@ -439,13 +456,47 @@ the ticker keeps the last rendered inputs, and an equal fingerprint
(`visible | k`) costs no render. A card that renders without a moon, leaves the
page or is hidden drops or pauses its ticker.
**Weight.** Astronomy, art (≈ 9 KB gzip) and template are one lazy chunk,
`moon-runtime-*`, loaded when the moon is switched on, the environment exists
and it is not daytime; until it arrives there is no moon, a failed load is
retried at most every 30 s through the exact-build loader of the isometric
runtime. The initial View graph carries only the gate (`src/moon-gate.ts`).
One element, no CSS filter and no `will-change`: the composited layer count
does not change (`demo/smoke_daycycle_layer_budget.mjs`).
With a static background the phase for the moon comes from the same
`resolveDayCycle` (`moonSkyState` in `src/moon-gate.ts`), computed only while
the moon is on and the surface is View. With `sun.sun` it follows the Home
Assistant state updates the cards already re-render on. Without it the card
keeps its 30 s clock ticker (`_syncDayCycleClock`, both cards) and re-renders
only when the phase changes (`dayCycleClock`: the environment is compared by
its whole fingerprint, the moon's sky by its phase), so the moon leaves at
08:00 and comes at 18:00 without a state update. The moon switched off or an
editor: no phase, no ticker.
**Weight.** Astronomy, art (≈ 9 KB gzip), template, the static-background
layer and the status function are one lazy chunk, `moon-runtime-*`, loaded
when the moon is switched on on a View surface and it is not daytime — with any
background — and when General settings open (for the status line, by day and
with the moon off too). One load per page through the gate's loader
(`withMoon`): a chunk of another build is never installed, a failed load is
retried at most every 30 s, and every caller meanwhile waits for the same load.
The initial View graph carries only the gate (`src/moon-gate.ts`); the editor
graph only the status line and its strings (`src/editors/moon-status.ts`) —
`src/moon.ts` is in neither (`scripts/bundle-budget.mjs` refuses an overlap of
the moon graph with the initial or the editor graph). One element, no CSS
filter and no `will-change`: the composited layer count does not change, with
either background (`demo/smoke_daycycle_layer_budget.mjs`).
**Status line (#718 K7).** Under the switch in General settings a second
caption line, inside `aria-describedby`, no `aria-live`, anchored
`data-moon-status="shown|no_home|day_sun|day_clock|low|new"`: «Now: shown (24°
above the horizon, 79% lit).» or «Now: not shown (reason).», the first reason
that holds — no home coordinates; day (by `sun.sun` with its elevation, or by
the clock); the moon below 3°; under 3 % lit. It is judged once per opening on
a snapshot taken when the dialog opens (`now`, `hass.config`, `sun.sun`), as
if the switch were on — the moon no longer depends on the background, so one
status serves the installation; the switch, «Reset» and the background segment
do not change it. `moonStatus` in `src/moon.ts` decides «shown» with the same
`moonShownAt` as the element (AC10 checks the equivalence every hour of a
month), rounds to whole numbers and keeps a hidden reason's number below its
threshold (2.6° reads «2°»). The status lives beside the draft, never in it:
the line arriving leaves «Save» disabled. While the chunk loads, or when it
failed, there is no line; a closed opening's result is dropped. The line
belongs to the browser the dialog is open in — a wall tablet with another
clock or time zone may differ.
**Limits (documented, not bugs).**
@@ -455,6 +506,8 @@ does not change (`demo/smoke_daycycle_layer_budget.mjs`).
not follow the hemisphere (owner decision): the crescent is "vertical".
- Earthshine is not modelled; the dark side is an 8 % silhouette for legibility.
- The moon does not follow its azimuth — fixed corner of the scene.
- One art for every background (owner decision, #718): on a mid-grey custom
colour the disc has less contrast than on white or at night.
- A dense layout (plan filling the screen) shows only the part of the disc in
the margins.
- Coordinates come from Home Assistant: an installation that kept the default
@@ -504,11 +557,14 @@ saving General settings removes it.
- `src/day-cycle-render.ts` — the shared constant environment layers and
plan-outline variables for full and static cards.
- `src/moon.ts`, `src/moon-runtime.ts`, `src/moon-gate.ts`,
`src/moon-art.generated.ts` — the moon (#661): pure astronomy and mask, the
lazy chunk with the element and its ticker, the initial-graph gate, the
generated art (`node scripts/generate-moon-assets.mjs` from
`assets/moon/houseplan-1.0.0`); unit-tested in `test/moon.test.mjs`,
end-to-end in `demo/smoke_moon.mjs`.
`src/moon-art.generated.ts` — the moon (#661, #718): pure astronomy, mask and
status, the lazy chunk with the element, the static-background layer and the
ticker, the initial-graph gate with the page-wide loader, the generated art
(`node scripts/generate-moon-assets.mjs` from `assets/moon/houseplan-1.0.0`);
`src/editors/moon-status.ts` — the General settings line. Unit-tested in
`test/moon.test.mjs` and `test/moon-settings.test.mjs`, end-to-end in
`demo/smoke_moon.mjs`, `demo/smoke_moon_static.mjs` and
`demo/smoke_moon_status.mjs`.
- `src/houseplan-card.ts` — the memoised wedge layer, four-phase background
lifecycle, and both settings dialogs (compass dial included).
- `src/space-render.ts` / `src/space-card.ts` — static-card environment and
+37 -8
View File
@@ -1657,19 +1657,27 @@ choice. Shadows from trees, awnings or other building wings are not modelled.
### Moon
On the **Follow the sun** background, at dawn, dusk and night, the moon in its
current phase stands in the top-left corner of the scene — a thin crescent, a
half, a full disc. One switch in General settings turns it on: **Sun and Moon**
→ **Moon over the plan at dusk and night**. It is on for new installations and
off for upgraded ones until switched on: an update never changes how a plan
looks.
With any background — **Follow the sun**, the theme background or a colour of
your own — at dawn, dusk and night the moon in its current phase stands in the
top-left corner of the scene — a thin crescent, a half, a full disc. One switch
in General settings turns it on: **Sun and Moon** → **Moon over the plan at dusk
and night**. It is on for new installations and off for upgraded ones until
switched on: an update never changes how a plan looks. If the switch was
already on with a static background, the moon appears on that background after
the update; the same switch removes it.
Dusk and night are decided as for the Follow the sun background: with a valid
`sun.sun` by the sun's elevation (below 6°), without it by the browser clock
(day is 08:00–18:00). A static background stays as it is: the chosen colour
around the plan, no gradients and no outline glow — only the moon is added.
| Condition | Result |
|---|---|
| Daytime, moon below 3° above the horizon, or new moon (under 3 % lit) | No moon |
| The moon rises above 3° or sets below it | Fades in/out over 2 seconds; immediately with reduced motion |
| Phase | Changes continuously, every day; the lit side is always on the left, waning runs the same states backwards |
| Space with its own static background | No moon — it belongs to the Follow the sun background |
| Space with its own static background | The same moon: same place, size and phase; the background keeps its colour |
| Changing the background, or a tab with another background | The moon does not flicker and stays in place |
| The plan covers the corner of the scene | The moon is behind the plan: the part of the disc in the margins shows; the plan, devices and labels are always on top |
| Editors | No moon |
| Full card, kiosk, static space card | The same |
@@ -1680,7 +1688,28 @@ integration or external data. A Home Assistant that kept the default location
shows someone else's moon — as with `sun.sun`; a wrong tablet clock gives a
wrong phase and moment. Position accuracy is about 1°, so crossing 3° may
differ from ephemerides by a few minutes. The terminator tilt and earthshine
are not modelled; the dark side is a faint silhouette so a crescent reads.
are not modelled; the dark side is a faint silhouette so a crescent reads. One
image serves every background: on a mid-grey colour of your own the moon has
less contrast.
Under the switch a «Now: …» line says whether the moon is shown this minute
and, if not, why — the first reason that holds:
| Line | When |
|---|---|
| Now: shown (24° above the horizon, 79% lit). | The moon is shown; altitude and illumination in whole numbers |
| Now: not shown (the home location is not set in Home Assistant). | No home latitude and longitude — even by day |
| Now: not shown (the sun is 25° above the horizon; the moon shows once it is below 6°). | Daytime by `sun.sun` |
| Now: not shown (daytime by the clock, 08:00–18:00: Home Assistant has no sun.sun data). | Daytime by the browser clock |
| Now: not shown (the moon is at 2°; it shows from 3° above the horizon). | The moon is below 3° |
| Now: not shown (new moon, 2% lit; it shows from 3%). | Under 3 % lit |
The line is computed once when the dialog opens and tells what the switch would
show when on: the switch itself, the background chosen in the dialog and the
space settings do not change it. It belongs to the browser the dialog is open
in: a wall tablet with another clock or time zone may show or hide its moon at
another moment. While the moon's data loads, or if loading failed, there is no
line — the general hint stays.
## 16. Robot vacuums
+38 -8
View File
@@ -1797,7 +1797,7 @@ WebP и безопасный SVG; лимит 2 МиБ относится к со
## 15. Солнце и Луна: фон, оконные лучи и луна
Фон и оконные лучи — две независимые функции; луна живёт на фоне. Фон **Следует за Солнцем** работает без компаса:
Фон, оконные лучи и луна — независимые функции; луна стоит за планом при любом фоне. Фон **Следует за Солнцем** работает без компаса:
он использует корректные данные `sun.sun`, а при их отсутствии автоматически
переходит на локальные часы браузера. Направление севера и `sun.sun` обязательны
только для оконных лучей. Общие значения можно переопределить в конкретном
@@ -1857,19 +1857,28 @@ WebP и безопасный SVG; лимит 2 МиБ относится к со
### Луна
При фоне **Следует за Солнцем** в сумерках (утренних и вечерних) и ночью в левом
верхнем углу сцены видна луна в текущей фазе — тонкий серп, половина, полная.
Включается одним переключателем в общих настройках, раздел **Солнце и Луна** →
**Луна на плане в сумерках и ночью**. У новых установок переключатель включён,
у обновлённых старых — выключен, пока его не включат: обновление не меняет вид
плана.
При любом фоне — **Следует за Солнцем**, фоне темы или своём цвете — в сумерках
(утренних и вечерних) и ночью в левом верхнем углу сцены видна луна в текущей
фазе — тонкий серп, половина, полная. Включается одним переключателем в общих
настройках, раздел **Солнце и Луна** → **Луна на плане в сумерках и ночью**. У
новых установок переключатель включён, у обновлённых старых — выключен, пока его
не включат: обновление не меняет вид плана. Если переключатель уже был включён,
а фон статичный, после обновления луна появится и на нём; убрать её — тем же
переключателем.
Сумерки и ночь определяются так же, как у фона «Следует за Солнцем»: при
корректном `sun.sun` — по высоте Солнца (ниже 6°), без него — по часам
браузера (день 08:00–18:00). Статичный фон при этом не меняется: вокруг плана
остаётся выбранный цвет, градиентов и свечения контура нет, добавляется только
луна.
| Условие | Результат |
|---|---|
| День, луна ниже 3° над горизонтом или новолуние (освещено меньше 3 %) | Луны нет |
| Луна поднимается выше 3° или уходит ниже | Плавно появляется/исчезает за 2 секунды; при системном уменьшении движения — сразу |
| Фаза | Меняется непрерывно, каждый день; освещённая сторона всегда слева, убывание идёт теми же состояниями в обратном порядке |
| Пространство с собственным статическим фоном | Луны нет — она часть фона «Следует за Солнцем» |
| Пространство с собственным статическим фоном | Луна та же: то же место, размер и фаза, фон — выбранный цвет |
| Смена фона или переход на вкладку с другим фоном | Луна не мигает и остаётся на месте |
| План закрывает угол сцены | Луна за планом: видна та часть диска, что в полях; план, устройства и подписи всегда поверх неё |
| Редакторы | Луны нет |
| Полная карточка, киоск, статическая карточка пространства | Одинаково |
@@ -1881,6 +1890,27 @@ WebP и безопасный SVG; лимит 2 МиБ относится к со
появления. Точность положения около 1°: момент пересечения 3° может разойтись с
эфемеридами на несколько минут. Наклон терминатора и пепельный свет не
воспроизводятся, тёмная сторона — едва заметный силуэт для читаемости серпа.
Картинка одна для всех фонов: на среднем сером своём цвете луна контрастнее не
станет.
Под переключателем — строка «Сейчас: …»: видна ли луна в эту минуту, а если нет,
то почему — первая по порядку причина:
| Строка | Когда |
|---|---|
| Сейчас: показывается (на 24° над горизонтом, освещено 79 %). | Луна видна; высота и освещённость округлены до целых |
| Сейчас: не показывается (в Home Assistant не заданы координаты дома). | Нет широты и долготы дома — даже днём |
| Сейчас: не показывается (Солнце на 25° над горизонтом, луна видна, когда оно ниже 6°). | День по `sun.sun` |
| Сейчас: не показывается (день по часам, 08:00–18:00: в Home Assistant нет данных sun.sun). | День по часам браузера |
| Сейчас: не показывается (луна на высоте 2°, видна от 3° над горизонтом). | Луна ниже 3° |
| Сейчас: не показывается (новолуние, освещено 2 %, видна от 3 %). | Освещено меньше 3 % |
Строка считается один раз при открытии диалога и говорит, что было бы при
включённом переключателе: положение тумблера, выбор фона в диалоге и
настройки пространств на неё не влияют. Она относится к браузеру, в котором
открыт диалог: если у настенного планшета другие часы или часовой пояс, его
луна может появиться или исчезнуть в другой момент. Пока данные луны
загружаются или если загрузка не удалась, строки нет — остаётся общее пояснение.
## 16. Роботы-пылесосы
@@ -16,8 +16,8 @@ to prove that the Node witness actually kills it.
| Paint, cascade and layer composition | 26 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
| Pointer geometry and trusted interaction | 45 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. |
| Responsive DOM layout | 36 | The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size. |
| Custom-element and HA browser lifecycle | 85 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
| **Total** | **200 / 200** | Growth above the cap fails `mutation-gate --check`. |
| Custom-element and HA browser lifecycle | 86 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
| **Total** | **201 / 200** | Growth above the cap fails `mutation-gate --check`. |
## Measured effect
@@ -242,6 +242,7 @@ The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy
- `locale-failure-toast-dropped`
- `marker-reject-keeps-optimistic-candidate`
- `marker-rollback-keeps-enqueue-time-revision`
- `moon-static-clock-tick-off`
- `namespace-loader-returns-english`
- `near-axis-authoring-snap-bypassed`
- `onboarding-loader-skips-namespace-ensure`