fix(ui): background no longer moves — static grid, three glow spots fading in and out on their own slow cycles (owner 2026-08-25)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# Task 01 — Still Background: Static Grid + Three Opacity-Only Glow Fades
|
||||
|
||||
**Phase:** `25_background_no_motion` · **Story:** `.agent/user_stories/background-no-motion.md`
|
||||
|
||||
## Objective
|
||||
Redesign the background block of `frontend/assets/styles.css` to the
|
||||
owner's spec (no movement; different bright spots slowly fading in and
|
||||
out), and pin the new contract at source level.
|
||||
|
||||
## Work
|
||||
0. **Capture the "before" evidence FIRST** (pre-change): with the DB up
|
||||
(`podman compose up -d db`), boot the app the same way
|
||||
`tests/e2e/conftest.py`'s `app_server` fixture does (uvicorn
|
||||
`app.main:app` on a free port, wait for it to answer), and with a small
|
||||
throwaway Playwright script screenshot the `/` page at 1280×800 →
|
||||
`.agent/screenshots/25_background_no_motion/before.png`, then again
|
||||
~4s later → `before_4s.png` (the pair shows the down-right jitter +
|
||||
the uniform brightness pulse). Kill the server when done.
|
||||
1. `frontend/assets/styles.css` — **grid layer `body::before`:** remove
|
||||
the `animation: bg-grid-drift 60s linear infinite;` declaration and
|
||||
delete the whole `@keyframes bg-grid-drift { … }` block. Keep the 44px
|
||||
cells, the 60% `--line` alpha 1px lines, and the widened radial mask —
|
||||
the grid remains as a **static texture**. Update the block's comment:
|
||||
drift removed (owner 2026-08-25) — the 0.73px/s sub-pixel drift
|
||||
rasterizes as a once-per-second down-right jitter; the owner wants no
|
||||
movement.
|
||||
2. `frontend/assets/styles.css` — **glow layers.** Replace the
|
||||
whole-layer breathe with three independent spot layers:
|
||||
- `body::after` — keep the phase-08 indigo spot exactly:
|
||||
`background-image: radial-gradient(circle 56rem at 12% 8%,
|
||||
rgb(109 120 242 / 0.14), transparent 62%);` and
|
||||
`animation: bg-glow-a 26s ease-in-out infinite;`
|
||||
- **new `html::before`** — the phase-08 cyan spot:
|
||||
`background-image: radial-gradient(circle 60rem at 88% 92%,
|
||||
rgb(34 211 238 / 0.10), transparent 62%);` and
|
||||
`animation: bg-glow-b 34s ease-in-out -12s infinite;`
|
||||
- **new `html::after`** — a third indigo spot:
|
||||
`background-image: radial-gradient(circle 52rem at 14% 86%,
|
||||
rgb(109 120 242 / 0.09), transparent 62%);` and
|
||||
`animation: bg-glow-c 42s ease-in-out -23s infinite;`
|
||||
- All three layers (and the grid layer) must declare:
|
||||
`content: ""; position: fixed; inset: 0; z-index: -1;
|
||||
pointer-events: none;` — no `transform`, no `background-position`,
|
||||
no `filter` on any of them.
|
||||
- Delete `@keyframes bg-glow-breathe { … }` and add the three
|
||||
**opacity-only** keyframe blocks (nothing else may appear in any
|
||||
`bg-*` keyframe):
|
||||
```css
|
||||
@keyframes bg-glow-a { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
|
||||
@keyframes bg-glow-b { 0%, 100% { opacity: 0.20; } 50% { opacity: 1; } }
|
||||
@keyframes bg-glow-c { 0%, 100% { opacity: 0.15; } 50% { opacity: 1; } }
|
||||
```
|
||||
3. `frontend/assets/styles.css` — **reduced motion:** in the existing
|
||||
`@media (prefers-reduced-motion: reduce)` block that stills the
|
||||
background (currently `body::before, body::after { animation: none; }`,
|
||||
right after the spinner block), extend the selector list to all four
|
||||
layers: `body::before, body::after, html::before, html::after
|
||||
{ animation: none; }`. Do not touch the typing/spinner/thinking
|
||||
reduced-motion blocks.
|
||||
4. `frontend/assets/styles.css` — **section comment:** rewrite the
|
||||
"Animated background" comment to document the new spec: no movement
|
||||
(owner 2026-08-25); three independent soft spots, opacity-only fades
|
||||
at 26/34/42s with negative delays → out of phase, so the total light
|
||||
fluxuates smoothly and irregularly; `html::before`/`html::after` are
|
||||
background layers (root stacking context: they paint above the
|
||||
`var(--bg)` canvas and below the transparent, non-stacking `<body>`'s
|
||||
content — the no-occlusion contract is unchanged).
|
||||
5. **New `tests/unit/test_background_no_motion.py`** (repo source-pin
|
||||
pattern — reuse the helpers from `tests/unit/test_background_animation.py`
|
||||
(`_css`, `_css_no_comments`, `_rule_block`; note `_rule_block` matches
|
||||
top-level `selector { … }`, which works for `html::before`/`html::after`
|
||||
as written in step 2). Pin, at minimum:
|
||||
- `body::before` carries **no `animation`** declaration;
|
||||
`@keyframes bg-grid-drift` is absent from the file; the grid keeps
|
||||
its static texture (`background-size: 44px 44px`, the two 60%-alpha
|
||||
1px line gradients, the widened mask, both mask properties).
|
||||
- `body::after` runs `bg-glow-a 26s ease-in-out infinite`;
|
||||
`html::before` runs `bg-glow-b 34s ease-in-out -12s infinite`;
|
||||
`html::after` runs `bg-glow-c 42s ease-in-out -23s infinite`;
|
||||
each glow layer's `background-image` is exactly the single radial
|
||||
gradient from step 2 (color, radius, position, 62% transparent stop).
|
||||
- **No movement:** parse every `@keyframes bg-*` rule in the file —
|
||||
the set of declared property names across all keyframe frames is
|
||||
exactly `{opacity}` (no `transform`, `scale`, `background-position`,
|
||||
…); none of the three glow layers declares `transform` or
|
||||
`background-position`.
|
||||
- The three glow durations are distinct and each ≥ 20s (slow).
|
||||
- All four pseudo-layers: `position: fixed`, `inset: 0`,
|
||||
`z-index: -1`, `pointer-events: none`, `content: ""`.
|
||||
- Plumbing: `html` keeps `background: var(--bg)`; `body` keeps
|
||||
`background: transparent` and declares none of `z-index`,
|
||||
`transform`, `opacity`, `filter`.
|
||||
- The reduced-motion block stills all four layers (all four selectors
|
||||
present together with `animation: none`).
|
||||
- No `filter` in any background layer block; no `blur` anywhere in
|
||||
the file (comments stripped).
|
||||
6. **Adapt `tests/unit/test_background_animation.py`** (the phase-22
|
||||
source pins) to the new contract so the whole unit suite is green:
|
||||
replace the grid-drift and breathe tests with their new-contract
|
||||
equivalents (where a check is already covered by the new file, keep the
|
||||
file self-contained rather than importing from it — the repo pattern is
|
||||
local pins); keep the generic layer-plumbing tests
|
||||
(`test_both_layers_are_fixed_zminus1_noninteractive` — extend it to
|
||||
cover `html::before`/`html::after` — and
|
||||
`test_html_owns_bg_and_body_stays_transparent`) and the no-blur/no-JS
|
||||
anchor test; update the module docstring to describe the phase-25
|
||||
design and point at `.agent/user_stories/background-no-motion.md`.
|
||||
|
||||
## Testing & Quality
|
||||
- `uv run pytest tests/unit -v` green (new + adapted pins).
|
||||
- `uv run pytest --cov=app --cov-report=term-missing` TOTAL ≥ the
|
||||
pre-change number (`app/` is untouched — the >90% gate holds).
|
||||
- `uv run ruff check . && uv run pyright` clean.
|
||||
- Coverage **>90%** on new/modified code: the functional change is CSS;
|
||||
the new/modified Python is pytest source pins, exercised in full.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `styles.css`: no `bg-grid-drift`, no `bg-glow-breathe`, no
|
||||
`animation` on `body::before`; exactly three opacity-only
|
||||
`bg-glow-a/b/c` cycles on `body::after`, `html::before`,
|
||||
`html::after` (26s/34s/42s, delays 0/−12s/−23s); reduced-motion
|
||||
stills all four layers; comments document the owner's no-movement
|
||||
spec.
|
||||
- [ ] `uv run pytest tests/unit` green with
|
||||
`tests/unit/test_background_no_motion.py` present and passing.
|
||||
- [ ] `uv run pytest --cov=app` TOTAL ≥ pre-change; ruff + pyright clean.
|
||||
- [ ] `.agent/screenshots/25_background_no_motion/before.png` and
|
||||
`before_4s.png` captured **before** the CSS change.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Phase 25 — Background: No Motion, Only Fading Light
|
||||
|
||||
**Owner report (2026-08-25, chat, verbatim):** "It should be smooth,
|
||||
fluxuating, dimming and brightening, but not moving. Different bright
|
||||
spots should slowly fade in and out." **Story:**
|
||||
`.agent/user_stories/background-no-motion.md`.
|
||||
|
||||
## Root causes (phase-22 measurements as context)
|
||||
|
||||
1. **"Jitters down and to the right every second" = the grid drift.**
|
||||
`bg-grid-drift` translated the 44px grid 44px per 60s (≈0.73px/s)
|
||||
diagonally down-right — exactly the reported direction. A 1px line
|
||||
moved sub-pixel by sub-pixel rasterizes with per-frame stepping, not
|
||||
smooth motion, so it reads as a once-per-second jitter. Phase 22
|
||||
had made that drift *visible* (60% line alpha + widened mask;
|
||||
measured 1.82/765 mean pixel change over 5s in the grid zone vs
|
||||
0.48 pre-phase-22) — that is precisely why it now reads as jitter.
|
||||
Confirmed in the `before.png`/`before_4s.png` pair below: a
|
||||
grid-only region far from every glow changes by 2.98/765 mean over
|
||||
4s (4.7% of its pixels) — the grid itself is moving.
|
||||
2. **"Slowly blinks brighter and darker" = the whole-layer breathe.**
|
||||
`bg-glow-breathe` swung the entire glow layer's opacity 0.85↔1 over
|
||||
14s (alternate) plus `scale(1)↔scale(1.05)` (a faint zoom). One
|
||||
synchronized pulse of the whole background reads as a blink; the
|
||||
before pair's uniform change across both glow corners (mean
|
||||
2.19/765 over the full frame) is that single pulse.
|
||||
|
||||
## Design change (styles.css — pure CSS, zero JS, no blur, palette
|
||||
untouched; task 01)
|
||||
|
||||
| property | phase 22 | phase 25 |
|
||||
|---|---|---|
|
||||
| grid (`body::before`) | `bg-grid-drift 60s linear infinite` (0→44px, 0.73px/s) | **static** — animation removed, `bg-grid-drift` deleted (44px cells, 60% `--line` 1px lines, widened mask kept) |
|
||||
| glow | one whole-layer `bg-glow-breathe` (14s, opacity 0.85↔1 + scale 1↔1.05) | **three independent spot layers, opacity-only fades** |
|
||||
| spot A `body::after` | indigo + cyan on one layer | indigo `rgb(109 120 242 / 0.14)` 56rem @ 12%/8% — `bg-glow-a` **26s** ease-in-out infinite, low 0.25 |
|
||||
| spot B `html::before` | — | cyan `rgb(34 211 238 / 0.10)` 60rem @ 88%/92% — `bg-glow-b` **34s** ease-in-out **−12s** infinite, low 0.20 |
|
||||
| spot C `html::after` | — | indigo `rgb(109 120 242 / 0.09)` 52rem @ 14%/86% — `bg-glow-c` **42s** ease-in-out **−23s** infinite, low 0.15 |
|
||||
| keyframes | `bg-grid-drift` (background-position), `bg-glow-breathe` (opacity + transform) | `bg-glow-a/b/c` — **opacity only** (0%/100% low → 50% 1) |
|
||||
| reduced motion | stills the two body layers | stills **all four** layers |
|
||||
|
||||
`html::before`/`html::after` join as background layers: `<html>` is
|
||||
the root stacking context, so their `z-index: -1` pseudo-elements
|
||||
paint above the `var(--bg)` canvas and below the transparent,
|
||||
non-stacking `<body>`'s content (verified live — E2E test 6). 26/34/42s
|
||||
with negative delays (LCM 4641s) keep the cycles out of phase, so the
|
||||
composite pattern effectively never repeats within a viewing session.
|
||||
No LOCKED anchor changed (A11 pure CSS / zero JS / no CDN / no new
|
||||
assets; no `filter: blur`).
|
||||
|
||||
## Before / after evidence (1280×800 headless Chromium, shots ~4s
|
||||
apart; `.agent/screenshots/25_background_no_motion/`)
|
||||
|
||||
- `before.png` / `before_4s.png` (captured **pre-change**, task 01):
|
||||
down-right grid jitter + the uniform whole-layer pulse.
|
||||
- `after.png` / `after_4s.png` (captured post-change, task 02):
|
||||
brightness changes; the grid is bit-identical.
|
||||
|
||||
Region diffs (pure-stdlib PNG decode; "changed" = pixels with
|
||||
per-channel Δ sum > 12/765):
|
||||
|
||||
| region | before pair: changed / mean\|d\| | after pair: changed / mean\|d\| |
|
||||
|---|---|---|
|
||||
| full frame | 42,316 (4.1%) / 2.190 | 8,357 (0.8%) / 1.754 |
|
||||
| grid-only patch (900,30)–(1250,120) | 1,484 (4.7%) / **2.979 — the grid moves** | 0 (0.0%) / **0.000 — bit-identical** |
|
||||
| spot A center (154,64) patch | 406 (4.1%) / 3.198 | 4,018 (40.2%) / 6.595 — mid-fade |
|
||||
| spot C clip (0,500)–(500,800) | 6,469 (4.3%) / 1.274 | 0 / 3.247 — gentle fade, no >12/765 steps |
|
||||
| spot C center (179,688) patch | 63 (0.6%) / 0.789 | 0 / 6.003 — visibly brightening |
|
||||
| spot B clip (780,500)–(1280,800) | 5,784 (3.9%) / 2.037 | 0 / 1.211 — slow fade at this phase |
|
||||
|
||||
Read: after the change, the only region that moves pixel-by-pixel is
|
||||
the light itself (spot A's 26s cycle passes through its steepest
|
||||
mid-fade in the 4s window); every region that the grid drift used to
|
||||
scrub through is now byte-identical — no positional shift anywhere.
|
||||
|
||||
## E2E story gate (task 02) — `tests/e2e/test_background_no_motion.py`
|
||||
|
||||
8 tests, green in isolation (see "Results"). The deterministic
|
||||
no-movement proof is test 3: a live Chromium walk of
|
||||
`document.styleSheets` shows the set of properties declared across
|
||||
every frame of every `bg-*` @keyframes rule is exactly `{opacity}`.
|
||||
Regression suites adapted to the new contract:
|
||||
`tests/e2e/test_background_animation.py` (grid static + `bg-glow-a`
|
||||
running + the three spot timelines advance + 4-layer contracts + 360px
|
||||
pin) and `tests/e2e/test_dark_tech_theme.py`
|
||||
(`test_animated_background` → static grid + 26/34/42s spots;
|
||||
`test_reduced_motion_honored` → all four layers stilled).
|
||||
|
||||
## UI Structure Check (AGENTS.md rule 5)
|
||||
- **Layers behind content:** E2E test 6 pins `position: fixed`,
|
||||
`z-index: -1`, `pointer-events: none`, `inset: 0` on all four
|
||||
pseudo-layers live, plus the html-canvas / body-transparent
|
||||
no-occlusion pair (`rgb(10, 14, 23)` / `rgba(0, 0, 0, 0)`).
|
||||
- **No text/contrast impact:** the change touches only background
|
||||
layers (no palette token, no text on the layers); the phase-08
|
||||
contrast suite stays green.
|
||||
- **No 360px overflow:** E2E test 8 — `scrollWidth <= clientWidth` at
|
||||
360×740 with all four `fixed; inset: 0` layers live.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -1,5 +1,13 @@
|
||||
# Story: Animated Background That Actually Animates
|
||||
|
||||
> **SUPERSEDED (owner direction 2026-08-25):** the phase-22 motion
|
||||
> design (grid drift + whole-layer breathe) is superseded by the
|
||||
> owner's "no movement, only fading light" direction — see
|
||||
> `.agent/user_stories/background-no-motion.md` (phase
|
||||
> `25_background_no_motion`). The phase-22 history below is preserved
|
||||
> as-is; the old E2E suite now pins the phase-25 contract as a
|
||||
> regression.
|
||||
|
||||
**Phase:** `22_background_animation` · **E2E:** `tests/e2e/test_background_animation.py`
|
||||
|
||||
## Narrative
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# Story: A Background That No Longer Moves — Only Fading Light
|
||||
|
||||
**Phase:** `25_background_no_motion` · **E2E:**
|
||||
`tests/e2e/test_background_no_motion.py`
|
||||
**Supersedes:** `.agent/user_stories/background-animation.md`
|
||||
(phase-22 motion design — history preserved there)
|
||||
|
||||
## Narrative
|
||||
|
||||
As **the owner**, I reported (2026-08-25, chat) that the phase-22
|
||||
background "jitters down and to the right every second and it slowly
|
||||
blinks brighter and darker". I want the background to be smooth,
|
||||
fluxuating, dimming and brightening — but **not moving** — with
|
||||
**different bright spots** that slowly fade in and out.
|
||||
|
||||
- **Given** the phase-22 animated background (a 60s one-cell grid drift
|
||||
at 0.73px/s diagonally down-right + a 14s whole-layer glow breathe of
|
||||
opacity 0.85↔1 with scale 1↔1.05)
|
||||
- **When** the page is observed in a real Chromium viewport
|
||||
- **Then** nothing moves — the grid is a static texture and no
|
||||
background keyframe animates anything but `opacity` — while three
|
||||
independent bright spots each fade in and out on their own slow,
|
||||
out-of-phase cycles (26s/34s/42s), so the total light fluxuates
|
||||
smoothly and irregularly: no jitter, no blink, no static frame, no
|
||||
new overflow at 360px, and no impact on text contrast or
|
||||
interactivity.
|
||||
|
||||
## Owner report (verbatim, 2026-08-25, chat)
|
||||
|
||||
> It should be smooth, fluxuating, dimming and brightening, but not
|
||||
> moving. Different bright spots should slowly fade in and out.
|
||||
|
||||
## Owner direction (2026-08-25)
|
||||
|
||||
1. **No movement** — no grid drift, no `scale`/`transform`, no
|
||||
`background-position` animation, anywhere in the background.
|
||||
2. **Fluxuating brightness** — overall page brightness varies smoothly
|
||||
and irregularly (not one synchronized pulse).
|
||||
3. **Different bright spots** — multiple glow spots, each fading in
|
||||
and out on its own slow cycle.
|
||||
4. **The static grid stays** — the owner rejected the grid's *motion*,
|
||||
not the grid; it remains as a still texture. (If the owner later
|
||||
wants the grid gone, that is a follow-up, not this phase.)
|
||||
|
||||
## Root cause (found from the code + phase-22 measurements)
|
||||
|
||||
1. **"Jitters down and to the right every second" = the grid drift.**
|
||||
`bg-grid-drift` moved the 44px grid 44px per 60s (≈0.73px/s)
|
||||
diagonally down-right — exactly the reported direction. A 1px grid
|
||||
line translated sub-pixel by sub-pixel is rasterized with per-frame
|
||||
stepping/shimmer, not smooth motion: it reads as a once-per-second
|
||||
jitter. Phase 22 had made that drift *visible* (60% line alpha,
|
||||
wider mask — measured 1.82/765 mean pixel change over 5s in the
|
||||
grid zone); that is precisely why it now reads as jitter.
|
||||
2. **"Slowly blinks brighter and darker" = the whole-layer breathe.**
|
||||
`bg-glow-breathe` swung the ENTIRE glow layer's opacity 0.85↔1 over
|
||||
14s (alternate) plus `scale(1)↔scale(1.05)` (a faint zoom). One
|
||||
synchronized pulse of the whole background reads as a blink; the
|
||||
owner wants independent spots instead.
|
||||
|
||||
Phase 22 served the phase-08 design intent (grid drift + whole-layer
|
||||
breathe). The owner's 2026-08-25 direction supersedes that **design
|
||||
intent** — no LOCKED anchor changed (A11 stays pure CSS / zero JS / no
|
||||
CDN / no new assets; the no-`filter: blur` perf anchor is honored).
|
||||
|
||||
## Fix (styles.css — pure CSS, zero JS, no blur, palette untouched)
|
||||
|
||||
| property | phase 22 | phase 25 |
|
||||
|---|---|---|
|
||||
| grid (`body::before`) | `bg-grid-drift 60s linear infinite` (0→44px) | **static** — no animation, `bg-grid-drift` deleted (44px cells, 60% `--line` 1px lines, widened mask kept) |
|
||||
| glow layer count | one whole-layer breathe | **three independent spot layers** |
|
||||
| spot A (`body::after`) | indigo + cyan spots, 14s opacity 0.85↔1 + scale 1↔1.05 | **indigo `rgb(109 120 242 / 0.14)` 56rem at 12%/8%**, `bg-glow-a` **26s** ease-in-out infinite, low opacity **0.25** |
|
||||
| spot B (`html::before`) | — (cyan shared body::after) | **cyan `rgb(34 211 238 / 0.10)` 60rem at 88%/92%**, `bg-glow-b` **34s** ease-in-out **−12s** infinite, low **0.20** |
|
||||
| spot C (`html::after`) | — | **indigo `rgb(109 120 242 / 0.09)` 52rem at 14%/86%**, `bg-glow-c` **42s** ease-in-out **−23s** infinite, low **0.15** |
|
||||
| keyframes | `bg-grid-drift` (background-position), `bg-glow-breathe` (opacity + transform) | **`bg-glow-a/b/c` — opacity only** (0%/100% low → 50% 1) |
|
||||
| reduced motion | stills `body::before/::after` | stills **all four** layers |
|
||||
|
||||
All four layers keep `content: ""; position: fixed; inset: 0;
|
||||
z-index: -1; pointer-events: none`. `<html>` keeps the `var(--bg)`
|
||||
canvas and `<body>` stays transparent (the no-occlusion contract):
|
||||
`html` is the root stacking context, so its `z-index: -1`
|
||||
pseudo-elements paint above the canvas and below the transparent,
|
||||
non-stacking `<body>`'s content. The 26/34/42s periods with negative
|
||||
delays (LCM 4641s) keep the cycles out of phase — the composite
|
||||
pattern effectively never repeats within a viewing session.
|
||||
|
||||
## Acceptance criteria
|
||||
1. **No movement:** the grid is static (`body::before` computed
|
||||
`animationName: none`; no `bg-grid-drift` in
|
||||
`document.getAnimations()`; grid texture still painted) and —
|
||||
audited in real Chromium via `document.styleSheets` — **no `bg-*`
|
||||
keyframe animates anything but `opacity`** (the deterministic
|
||||
no-movement proof).
|
||||
2. **Three distinct bright spots** (`body::after`, `html::before`,
|
||||
`html::after`) run distinct slow opacity fades (26s/34s/42s,
|
||||
ease-in-out, infinite, pairwise distinct, out of phase); all three
|
||||
timelines advance; the layer's computed opacity AND a clipped
|
||||
screenshot of the bottom-left glow region measurably change within
|
||||
a few seconds (a real fade, not a frozen frame).
|
||||
3. **Contracts hold:** all four layers `position: fixed`, `z-index:
|
||||
-1`, `pointer-events: none`, full-viewport `inset: 0`; `<html>`
|
||||
keeps the `var(--bg)` canvas (`rgb(10, 14, 23)`) and `<body>` stays
|
||||
transparent (`rgba(0, 0, 0, 0)`) — no occlusion.
|
||||
4. **Reduced motion** stills all four layers
|
||||
(`animationName: none`), the static grid + spot images remain.
|
||||
5. No new horizontal overflow at 360px (the phase-07 pin).
|
||||
6. Pure CSS, zero JS, no `filter: blur`, no new assets (A11 + phase-08
|
||||
perf anchor); WCAG AA palette untouched (the layers carry no text).
|
||||
7. Regressions green in isolation:
|
||||
`tests/e2e/test_background_animation.py` (adapted to the phase-25
|
||||
contract), `tests/e2e/test_dark_tech_theme.py` (grid static +
|
||||
26/34/42s spots; reduced motion across all four layers),
|
||||
`tests/e2e/test_responsive_polish.py`.
|
||||
8. Unit + integration green, `app/` coverage >90%, story E2E green in
|
||||
isolation, ruff + pyright clean.
|
||||
|
||||
## UI Visualization & Structure
|
||||
- **Grid layer (`body::before`):** 44px cells, 1px lines at 60% of
|
||||
`--line` (`rgb(38 48 74 / 0.6)`), widened radial mask
|
||||
(`140% 110% at 50% 0%, black 40%, transparent 90%`) — a STATIC
|
||||
texture, no animation.
|
||||
- **Glow spot A (`body::after`):** indigo `rgb(109 120 242 / 0.14)`
|
||||
56rem circle at 12%/8% (phase-08 position/color); opacity-only fade
|
||||
0.25↔1 over 26s ease-in-out.
|
||||
- **Glow spot B (`html::before`):** cyan `rgb(34 211 238 / 0.10)` 60rem
|
||||
circle at 88%/92% (phase-08 position/color); fade 0.20↔1 over 34s,
|
||||
−12s delay.
|
||||
- **Glow spot C (`html::after`):** indigo `rgb(109 120 242 / 0.09)`
|
||||
52rem circle at 14%/86%; fade 0.15↔1 over 42s, −23s delay.
|
||||
- **Stacking / no occlusion:** the `<html>` canvas
|
||||
(`var(--bg)` = `#0a0e17`) sits under all four `z-index: -1` layers;
|
||||
`<body>` stays transparent and non-stacking, so nothing can paint
|
||||
over the layers — verified live, not assumed.
|
||||
- **Motion:** opacity-only keyframes (compositor-friendly); no
|
||||
`transform`, no `background-position`, no `filter` anywhere in the
|
||||
background; `prefers-reduced-motion: reduce` stills all four layers
|
||||
(the static background remains visible).
|
||||
|
||||
## Playwright Mapping Rule
|
||||
**Test Scenario → `tests/e2e/test_background_no_motion.py`** (the
|
||||
layers are CSS pseudo-elements — asserted via computed style + the Web
|
||||
Animations API + a live `document.styleSheets` audit; Chromium
|
||||
enumerates pseudo-element CSS animations in `document.getAnimations()`,
|
||||
not `document.body.getAnimations()`, and the `html` pseudo-layers'
|
||||
computed styles come from
|
||||
`getComputedStyle(document.documentElement, "::before"/"::after")`):
|
||||
1. `test_grid_layer_is_static` — computed `animationName` of
|
||||
`body::before` is `"none"`; no `bg-grid-drift` entry in
|
||||
`document.getAnimations()`; the grid `backgroundImage` is still
|
||||
present (the static texture survives).
|
||||
2. `test_three_glow_layers_run_distinct_fades` — `body::after` →
|
||||
`bg-glow-a` (26s), `documentElement::before` → `bg-glow-b` (34s),
|
||||
`documentElement::after` → `bg-glow-c` (42s); each `ease-in-out` +
|
||||
`infinite`, with a matching `playState === "running"` entry in the
|
||||
document animation list; the three durations are pairwise distinct.
|
||||
3. `test_no_motion_properties_in_background_keyframes` — walk
|
||||
`document.styleSheets`; for every `CSSRule.KEYFRAMES_RULE` whose
|
||||
name starts with `bg-`, collect the declared property names of
|
||||
every keyframe frame; the set across all frames is exactly
|
||||
`{"opacity"}` — the deterministic no-movement proof.
|
||||
4. `test_glow_timelines_advance` — poll until all three timelines
|
||||
report `currentTime > 0` (headless Chromium starts the document
|
||||
timeline ~1s after load), sample all three, wait ~500ms, each
|
||||
advanced ≥ 200ms.
|
||||
5. `test_background_light_actually_changes` — (a) the computed opacity
|
||||
of `body::after` changes by ≥ 0.05 within ~8s (a real fade, not a
|
||||
frozen frame); (b) two clipped screenshots ~4s apart of the
|
||||
bottom-left glow region (the `html::after` spot at 14%/86%) differ
|
||||
in bytes — the light visibly changes while nothing moves.
|
||||
6. `test_background_layers_contracts` — all four pseudo-layers:
|
||||
`position: fixed`, `z-index: -1`, `pointer-events: none`,
|
||||
top/right/bottom/left all `0px`; `documentElement` computed
|
||||
background is `rgb(10, 14, 23)` (canvas stays on `html`);
|
||||
`document.body` computed background is `rgba(0, 0, 0, 0)` (no
|
||||
occlusion).
|
||||
7. `test_reduced_motion_stills_all_layers` —
|
||||
`reduced_motion="reduce"` context: all four pseudo-layers report
|
||||
computed `animationName` `"none"` and still carry a
|
||||
`backgroundImage`.
|
||||
8. `test_no_horizontal_overflow_with_layers` — 360px viewport:
|
||||
`documentElement.scrollWidth <= clientWidth` (the phase-07 pin).
|
||||
Reference in New Issue
Block a user