"""Unit: the phase-22 animated-background contract (source pins). The owner report (2026-08-24, roadmap A3): the phase-08 background "just blinks". The diagnosis (`.agent/reports/22_background_animation/`) found both layers *were* animating with no occlusion — the grid simply wasn't perceptible: 1px lines at 35% `--line` alpha (≈10-18/255 over the page bg), masked to the top ~25% of the viewport, drifting 0.73px/s. Only the glow's 0.65↔1 opacity swing was visible, and it read as a blink. The fix (styles.css, pure CSS, zero JS, no `filter: blur`): - grid lines 35% → 60% `--line` alpha; - mask widened: `120% 90% … black 25%, transparent 78%` → `140% 110% … black 40%, transparent 90%` (grid now readable across most of the viewport, fading to the corners); - glow breathe narrowed 0.65↔1 → 0.85↔1 (breathing, not pulsing). Durations are the owner-confirmed phase-08 design and stay pinned at 60s (one-cell seamless drift) and 14s — changing them would also break the phase-08 story gate (`tests/e2e/test_dark_tech_theme.py` pins the live durations). This file pins the FINAL values so a silent regression (weaker alpha, shrunken mask, wider opacity swing, re-occluded layer) is caught without a browser. Browser behavior (visible motion, no jank) is E2E-covered by tests/e2e/test_background_animation.py (task 02). """ from __future__ import annotations import re from pathlib import Path STYLES_CSS = ( Path(__file__).resolve().parents[2] / "frontend" / "assets" / "styles.css" ) def _css() -> str: return STYLES_CSS.read_text(encoding="utf-8") def _css_no_comments() -> str: """styles.css with /* … */ comments stripped — for functional anchor checks (filter/blur) that must not trip on explanatory comments.""" return re.sub(r"/\*[\s\S]*?\*/", "", _css()) def _rule_block(css: str, selector: str) -> str: """Body of the first `selector { ... }` rule (top-level, no nesting).""" rule = re.search( r"(?m)^" + re.escape(selector) + r"\s*\{([\s\S]*?)\n\}", css ) assert rule, f"styles.css must define a {selector} rule" return rule.group(1) def _grid_rule(css: str) -> str: return _rule_block(css, "body::before") def _glow_rule(css: str) -> str: return _rule_block(css, "body::after") # -------------------------------------------------------------------------- # Layer plumbing — the no-occlusion contract (phase 08) must survive # -------------------------------------------------------------------------- def test_both_layers_are_fixed_zminus1_noninteractive() -> None: """Both background layers stay behind the content and can never intercept input: fixed, full-viewport, z-index -1, pointer-events none (UI Structure Check: layers behind content, no 360px overflow).""" for name, block in (("body::before", _grid_rule(_css())), ("body::after", _glow_rule(_css()))): assert "position: fixed" in block, f"{name} must stay position:fixed" assert "inset: 0" in block, f"{name} must stay full-viewport (inset: 0)" assert "z-index: -1" in block, f"{name} must stay z-index:-1" assert "pointer-events: none" in block, f"{name} must stay click-through" assert "content: \"\"" in block, f"{name} must keep its pseudo content" def test_html_owns_bg_and_body_stays_transparent() -> None: """The no-occlusion contract: the visible page background lives on ; must remain transparent and non-stacking, or the z-index:-1 layers are painted over (the phase-08 recipe).""" html_block = _rule_block(_css(), "html") assert "background: var(--bg)" in html_block, ( "html must keep background: var(--bg) (the page canvas)" ) body_block = _rule_block(_css(), "body") assert "background: transparent" in body_block, ( "body must keep background: transparent so the layers show" ) # body must not gain a z-index/transform/opacity that would turn it # into a stacking context trapping the negative-z-index layers. for prop in ("z-index", "transform", "opacity", "filter"): assert prop + ":" not in body_block, ( f"body must not create a stacking context (found {prop})" ) # -------------------------------------------------------------------------- # Grid layer — the phase-22 final values # -------------------------------------------------------------------------- def test_grid_animates_seamless_one_cell_drift() -> None: """body::before runs bg-grid-drift 60s linear infinite — the owner-confirmed 60s one-cell loop (seamless, delta == 44px).""" block = _grid_rule(_css()) assert "animation: bg-grid-drift 60s linear infinite" in block def test_grid_keyframes_move_exactly_one_cell() -> None: """The drift delta must equal one 44px cell (0 0 → 44px 44px) for a seamless loop — if the speed ever changes, only the duration may move.""" keyframes = re.search( r"@keyframes bg-grid-drift\s*\{([\s\S]*?)\n\}", _css() ) assert keyframes, "styles.css must define @keyframes bg-grid-drift" body = keyframes.group(1) assert "background-position: 0 0, 0 0" in body assert "background-position: 44px 44px, 44px 44px" in body def test_grid_cells_and_line_contrast() -> None: """44px cells with 1px lines at the phase-22 fixed 60% --line alpha (phase-08's 35% measured imperceptible at 0.73px/s — see module docstring).""" block = _grid_rule(_css()) assert "background-size: 44px 44px" in block line = "linear-gradient(to right, rgb(38 48 74 / 0.6) 1px, transparent 1px)" assert line in block, "grid must keep horizontal 1px lines at 60% --line" assert ( "linear-gradient(to bottom, rgb(38 48 74 / 0.6) 1px, transparent 1px)" in block ), "grid must keep vertical 1px lines at 60% --line" assert "0.35" not in block, "the too-faint 35% line alpha must not return" def test_grid_mask_widened_and_prefixed() -> None: """The phase-22 mask: 140%×110% ellipse, fully visible to 40% of the radius, faded out by 90% — the grid must read across most of the viewport (phase-08's 120%×90%/25%/78% masked it to the top ~25%). The -webkit- and standard mask-image must stay in lockstep.""" block = _grid_rule(_css()) mask = "radial-gradient(140% 110% at 50% 0%, black 40%, transparent 90%)" assert f"-webkit-mask-image: {mask};" in block assert f"mask-image: {mask};" in block # -------------------------------------------------------------------------- # Glow layer — the phase-22 final values # -------------------------------------------------------------------------- def test_glow_animates_breathe_not_blink() -> None: """body::after runs bg-glow-breathe 14s ease-in-out infinite alternate — the owner-confirmed 14s breathing period (untouched).""" block = _glow_rule(_css()) assert "animation: bg-glow-breathe 14s ease-in-out infinite alternate" in block def test_glow_keyframes_narrowed_opacity_swing() -> None: """The opacity swing is narrowed to 0.85↔1 (phase-08's 0.65↔1 was the only visible motion and read as a blink). The gentle scale (1↔1.05) stays.""" keyframes = re.search( r"@keyframes bg-glow-breathe\s*\{([\s\S]*?)\n\}", _css() ) assert keyframes, "styles.css must define @keyframes bg-glow-breathe" body = keyframes.group(1) assert re.search(r"opacity:\s*0\.85", body), "glow low must be 0.85" assert re.search(r"opacity:\s*1;?", body), "glow high must be 1" assert "0.65" not in body, "the blinking 0.65 low must not return" assert re.search(r"scale\(1\)", body) assert re.search(r"scale\(1\.05\)", body) def test_glow_colors_and_radii_untouched() -> None: """Phase-22 is a perception fix, not a redesign: the two glow gradients (indigo top-left, cyan bottom-right) keep phase-08's colors and radii.""" block = _glow_rule(_css()) assert ( "radial-gradient(circle 56rem at 12% 8%, rgb(109 120 242 / 0.14), " "transparent 62%)" in block ) assert ( "radial-gradient(circle 60rem at 88% 92%, rgb(34 211 238 / 0.10), " "transparent 62%)" in block ) # -------------------------------------------------------------------------- # Phase-08 anchor: pure CSS, zero JS, no blur # -------------------------------------------------------------------------- def test_no_blur_no_js_in_background_layers() -> None: """The phase-08 performance anchor: no `filter: blur` (or any filter) on either layer, and the animation is CSS-only (both layers carry an `animation:` shorthand; nothing in styles.css references a script).""" for name, block in (("body::before", _grid_rule(_css())), ("body::after", _glow_rule(_css()))): assert "filter" not in block, f"{name} must not use any filter" assert "animation:" in block, f"{name} must be CSS-animated" assert "blur" not in _css_no_comments(), ( "no filter: blur anywhere in styles.css (phase-08 perf anchor)" )