Standardize on the .agents/ directory (shared with project skills): phases/, user_stories/, reports/, screenshots/, validate.sh, and phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves history; runtime artifacts move alongside). Updates every reference in AGENTS.md, README.md, .gitignore, app docstrings, and test story headers. Historical KB content in data/ and the runtime pipeline.log transcript are left untouched.
10 KiB
Phase 25 — Background: No Motion, Only Fading Light
Source: Owner report (2026-08-25, chat): the background "jitters down
and to the right every second and it slowly blinks brighter and darker. It
should be smooth, fluxuating, dimming and brightening, but not moving.
Different bright spots should slowly fade in and out."
Story: .agents/user_stories/background-no-motion.md (created by task 02)
Context: frontend/assets/styles.css — the background block:
body::before (44px grid, 1px lines at 60% --line alpha, widened radial
mask, animation: bg-grid-drift 60s linear infinite → 0 0 →
44px 44px ≈ 0.73px/s down-right) and body::after (indigo + cyan radial
glows, animation: bg-glow-breathe 14s ease-in-out infinite alternate →
opacity 0.85↔1 + scale 1↔1.05). Both are
position: fixed; inset: 0; z-index: -1; pointer-events: none; <html>
owns the var(--bg) canvas, <body> stays transparent (no-occlusion
contract). Pins to adapt: tests/unit/test_background_animation.py,
tests/e2e/test_background_animation.py,
tests/e2e/test_dark_tech_theme.py (test_animated_background pins
60s/14s; test_reduced_motion_honored pins the two body pseudo-layers).
Objective
Stop the background from moving entirely, and replace the uniform whole-layer "blink" with different bright spots that slowly fade in and out: the grid becomes a static texture, and three soft glow spots (phase-08 colors/positions, plus a third spot) each run their own slow, opacity-only fade cycle at a different period, so the background's brightness fluxuates smoothly and irregularly — no blink, no jitter, no motion.
Root cause (found from the code, 2026-08-25)
- "Jitters down and to the right every second" =
bg-grid-drift: 44px/60s (≈0.73px/s) in the diagonal44px 44pxdirection (exactly down-right). A 1px grid line translated sub-pixel-by-sub-pixel is rasterized with per-frame stepping/shimmer — perceived as a once-per- second jitter, not smooth drift. Phase 22 made the drift visible; that is precisely why it now reads as jitter. - "Slowly blinks brighter and darker" =
bg-glow-breathe: a uniform whole-layer opacity swing 0.85↔1 over 14s (alternate) plusscale(1)↔scale(1.05)— the entire background pulses in unison (the scale adds a faint zoom). One synchronized pulse 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 now supersedes that design intent — this is an
owner revision of a design comment, not of any LOCKED anchor: A1–A17
are untouched, and the phase-08 anchors this phase must still honor are
pure CSS / zero JS / no CDN / no new assets / no filter: blur (A11 +
the block's perf note).
Owner direction (2026-08-25, verbatim)
"It should be smooth, fluxuating, dimming and brightening, but not moving. Different bright spots should slowly fade in and out."
- No movement — no grid drift, no
scale/transform, nobackground-positionanimation, anywhere in the background. - Fluxuating brightness — overall page brightness varies smoothly and irregularly (not one synchronized pulse).
- Different bright spots — multiple glow spots, each fading in and out on its own slow cycle.
- 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.)
Design (pure CSS, zero JS — A11 anchor)
-
body::before— grid: remove theanimation; delete@keyframes bg-grid-drift. Keep the 44px cells, 60%--linealpha lines, and widened radial mask (the static texture). -
Three glow-spot layers, one soft radial gradient each, opacity-only keyframes (
0%,100%low →50%1,ease-in-out,infinite), with different durations + negative delays so the cycles are out of phase (periods 26/34/42s → LCM 4641s, the composite pattern effectively never repeats within a viewing session):layer spot (gradient) keyframes cycle body::afterindigo rgb(109 120 242 / 0.14), circle 56rem at 12% 8% (phase-08)bg-glow-a26s, low opacity 0.25 html::beforecyan rgb(34 211 238 / 0.10), circle 60rem at 88% 92% (phase-08)bg-glow-b34s, delay −12s, low 0.20 html::afterindigo rgb(109 120 242 / 0.09), circle 52rem at 14% 86%bg-glow-c42s, delay −23s, low 0.15 -
html::before/html::afterjoinbody::afteras background layers:<html>is the root stacking context — itsz-index: -1pseudo-elements paint above thevar(--bg)canvas and below the transparent, non-stacking<body>'s content, so the no-occlusion contract holds unchanged (verify in the E2E, not just assume). -
All four layers keep:
content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; -
prefers-reduced-motion: reducestills all four layers (animation: none). -
No
filter(phase-08 no-blur perf anchor), no JS, no new assets; the WCAG palette and every text contrast pair are untouched (the layers carry no text). Opacity-only keyframes stay compositor-friendly.
Dependencies
08_story_dark_tech_theme(complete) — the layers, the palette, the pure-CSS / no-blur / no-CDN anchors, and the 60s/14s pins intests/e2e/test_dark_tech_theme.pythis phase adapts.22_background_animation(complete) — the current implementation and the unit/E2E pins this phase supersedes.07_story_responsive_polish(complete) — the 360px overflow pin (the layers arefixed; inset: 0— they must add no width).
Tasks
01_still_background_css.md— the CSS redesign (static grid + three opacity-only glow fades) + unit source pins (newtests/unit/test_background_no_motion.py; the phase-22 pins intests/unit/test_background_animation.pyadapted).02_e2e_story_suite_commit.md—tests/e2e/test_background_no_motion.py(the story gate, isolated), the regression suites adapted (test_background_animation.py,test_dark_tech_theme.py), the story file, the phase report + screenshots, final validation, the single atomic commit, phase move tocomplete/.
Locked decisions
- A11 untouched — vanilla HTML/CSS/JS in git, no CDN, zero JS, system
fonts; the whole change is CSS. No
filter/blur(phase-08 perf anchor). No new assets. No anchor changed — the superseded spec is the phase-08 design intent (grid drift + whole-layer breathe), not a LOCKED decision; the owner's 2026-08-25 direction is recorded above as the revision. A16 honored — one new story E2E suite + adapted regressions. A17 honored — one atomic--no-gpg-signcommit.
Testing & Quality
- Unit (source-level, repo source-pin pattern):
- New
tests/unit/test_background_no_motion.py— pins the full new contract (task 01, step 6). - Adapted
tests/unit/test_background_animation.py— the phase-22 drift/breathe pins flip to the new contract; the generic layer-plumbing and no-blur/no-JS sections stay (task 01, step 7).
- New
- Integration: none (no
app/changes). - Coverage: frontend-only; the >90%
app/gate is unaffected (TOTAL must stay ≥ the pre-change number). - E2E:
tests/e2e/test_background_no_motion.py(task 02), green in isolation (prereqpodman compose up -d db); regressions green in isolation after adaptation:test_background_animation.py,test_dark_tech_theme.py,test_responsive_polish.py. - Lint/types:
uv run ruff check . && uv run pyrightclean.
Completion Criteria
- No movement: the grid is static (
bg-grid-driftgone, noanimationonbody::before) and nobg-*keyframe animates anything butopacity— audited in a real Chromium viadocument.styleSheets(E2E test 3). - Three distinct bright spots (
body::after,html::before,html::after) run distinct slow opacity fades (26s/34s/42s, out of phase); the timelines advance; the layer opacity and a clipped screenshot of the glow region measurably change within a few seconds (E2E tests 2, 4, 5). - Contracts hold: all four layers
fixed; inset: 0; z-index: -1; pointer-events: none;<html>keeps thevar(--bg)canvas and<body>stays transparent (no occlusion);prefers-reduced-motionstills all four; no horizontal overflow at 360px (E2E tests 6–8). .agents/reports/25_background_no_motion/documents the two root causes with before/after screenshot pairs (.agents/screenshots/25_background_no_motion/).uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missingTOTAL ≥ pre-change number (gate >90%).uv run pytest tests/e2e/test_background_no_motion.py -v --no-covgreen in isolation;test_background_animation.py,test_dark_tech_theme.py,test_responsive_polish.pygreen in isolation after adaptation.uv run ruff check . && uv run pyrightclean.- UI Structure Check (AGENTS.md rule 5): layers stay behind content, no text/contrast impact, no 360px overflow.
.agents/user_stories/background-no-motion.mdexists; the oldbackground-animation.mdstory carries a supersession note.- One
--no-gpg-signcommit staging only this phase's files (the unrelated dirtyTODO.mdmust NOT be staged);.agents/phases/todo/25_background_no_motion/moved to.agents/phases/complete/.
Commit
git add -f .agents/phases/complete/25_background_no_motion \
.agents/user_stories/background-no-motion.md \
.agents/user_stories/background-animation.md \
.agents/reports/25_background_no_motion \
.agents/screenshots/25_background_no_motion
git add frontend/assets/styles.css tests/unit tests/e2e
git commit --no-gpg-sign -m "fix(ui): background no longer moves — static grid, three glow spots fading in and out on their own slow cycles (owner 2026-08-25)"