From 43c7c199c3db1cc6744f726936df42b9c9079367 Mon Sep 17 00:00:00 2001 From: m17hr1l Date: Mon, 25 May 2026 19:38:31 +0200 Subject: [PATCH] =?UTF-8?q?stage-31=20polish:=20featured=20hero=20?= =?UTF-8?q?=E2=80=94=20particles=20sync=20with=20sweep,=20stat=20chips,=20?= =?UTF-8?q?radar=20emblem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Particles now blink in time with the scan column. Each / in the case hero SVG carries class="hero-particle" and a CSS animation-delay equal to -(arrival-time-of-sweep-at-its-x), so as the sweep marches left-to-right the particles light up in sequence — a real "the scanner just touched this point" effect. The sweep is now linear / non-alternating on a 12s cycle so the phase stays predictable. Drop-shadow + opacity flash at the peak. 2. Stat chips added under the featured title. Up to five compact chips showing incident type, total IOC count with per-type breakdown (U/D/I/H/C), victim country if mapped, confidence level, malware family. Each chip is color-coded. 3. Radar emblem in the top-right of the hero. Pure SVG: three concentric range rings, cross-hairs, a translucent sweep arm that spins every 4.5s, a center dot. The whole emblem inherits the cycling-hero hue cycle so it changes color with the grid. Smaller + dimmer on mobile; honors prefers-reduced-motion. Co-Authored-By: Claude Opus 4.7 --- src/psyc/cockpit/case_visuals.py | 30 +++++++++--- src/psyc/cockpit/static/cockpit.css | 73 ++++++++++++++++++++++++++-- src/psyc/cockpit/templates/home.html | 36 ++++++++++++++ 3 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/psyc/cockpit/case_visuals.py b/src/psyc/cockpit/case_visuals.py index c86c2bd..2b53697 100644 --- a/src/psyc/cockpit/case_visuals.py +++ b/src/psyc/cockpit/case_visuals.py @@ -82,9 +82,20 @@ def case_hero_svg(case: Case, width: int = 880, height: int = 220) -> str: for y in range(0, height, 44): parts.append(f'') - # Particle field - n = 40 - pts = [(rng() * width, rng() * height, 1 + rng() * 3, 0.3 + rng() * 0.55) for _ in range(n)] + # Particle field — each particle gets a CSS animation-delay so it blinks + # exactly when the sweep column passes over its x-position. Sweep cycle is + # 12s left-to-right; arrival time at x = 12 * (px*100 + 20) / 140 sec. + n = 42 + pts = [] + for _ in range(n): + x = rng() * width + y = rng() * height + s = 1.2 + rng() * 3 + op = 0.3 + rng() * 0.55 + # negative delay so the cycle is already at the right phase when the + # page loads (otherwise every particle would flash in unison at t=0). + delay = -round(12.0 * ((x / width) * 100 + 20) / 140, 2) + pts.append((x, y, s, op, delay)) # Connect close particles for i in range(n): @@ -99,13 +110,18 @@ def case_hero_svg(case: Case, width: int = 880, height: int = 220) -> str: f'stroke="{accent}" stroke-width="0.6" stroke-opacity="{op:.2f}"/>' ) - for x, y, s, op in pts: + for x, y, s, op, d in pts: + style = f'animation-delay: {d}s;' if rng() < 0.7: - parts.append(f'') + parts.append( + f'' + ) else: parts.append( - f'' + f'' ) # HUD corner brackets diff --git a/src/psyc/cockpit/static/cockpit.css b/src/psyc/cockpit/static/cockpit.css index 05d58a4..c097b83 100644 --- a/src/psyc/cockpit/static/cockpit.css +++ b/src/psyc/cockpit/static/cockpit.css @@ -904,11 +904,12 @@ body.wide .net-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr will-change: color, background-position; } .featured-grid::after { - /* a soft glowing column that sweeps across the hero */ + /* glowing column that sweeps left-to-right; non-alternating so the + particle pulse delays line up the same way every cycle */ content: ""; position: absolute; inset: 0; - background: radial-gradient(220px 100% at 0% 50%, currentColor 0%, transparent 65%); + background: radial-gradient(180px 100% at 0% 50%, currentColor 0%, transparent 60%); opacity: 0.55; mix-blend-mode: screen; - animation: hero-sweep 12s ease-in-out infinite alternate; + animation: hero-sweep 12s linear infinite; } @keyframes hero-hue-cycle { 0%, 100% { color: #1ec8ff; } /* cyan */ @@ -922,8 +923,26 @@ body.wide .net-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr to { background-position: 37px 37px; } } @keyframes hero-sweep { - 0% { transform: translateX(-20%); } - 100% { transform: translateX(120%); } + 0% { transform: translateX(-20%); opacity: 0.55; } + 85% { transform: translateX(120%); opacity: 0.55; } + 90% { opacity: 0; } + 99% { transform: translateX(-20%); opacity: 0; } + 100% { transform: translateX(-20%); opacity: 0.55; } +} + +/* Particles in the hero SVG blink when the sweep column passes over them. + Each particle has animation-delay = -arrival-time (negative), set inline + by case_visuals so the cycle phase matches its x-position from t=0. */ +.featured-hero .hero-particle { + animation: hero-particle-pulse 12s linear infinite; + transform-box: fill-box; transform-origin: center; +} +@keyframes hero-particle-pulse { + /* peak at 0% = the moment the sweep arrives; quick decay to baseline */ + 0% { fill-opacity: 1; filter: drop-shadow(0 0 5px currentColor); } + 8% { fill-opacity: 0.35; filter: none; } + 92% { fill-opacity: 0.35; } + 100% { fill-opacity: 1; } } /* case-specific particles layered on top of the grid */ @@ -1095,3 +1114,47 @@ body.wide .net-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr @media (prefers-reduced-motion: reduce) { .news-item, .news-arrow, .featured-arrow, .featured-cta, .case-glyph-svg, .featured-section-pulse { transition: none; animation: none; } } + +/* ── featured card: radar emblem + stat chips ──────────────── */ +.featured-radar { + position: absolute; top: 16px; right: 18px; + width: 130px; height: 130px; + color: #1ec8ff; + pointer-events: none; + opacity: 0.62; + animation: hero-hue-cycle 18s ease-in-out infinite; + filter: drop-shadow(0 0 12px currentColor); +} +.radar-sweep-arm { + transform-origin: 50px 50px; + animation: radar-spin 4.5s linear infinite; +} +@keyframes radar-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } +@media (max-width: 720px) { + .featured-radar { width: 78px; height: 78px; top: 10px; right: 10px; opacity: 0.45; } +} +@media (prefers-reduced-motion: reduce) { + .radar-sweep-arm, .featured-radar { animation: none; } +} + +.featured-stats { + display: flex; flex-wrap: wrap; gap: 6px; + margin: 8px 0 10px; +} +.stat-chip { + display: inline-flex; align-items: center; gap: 4px; + padding: 3px 9px; + font-family: ui-monospace, Menlo, monospace; font-size: 11px; + background: rgba(15,17,21,0.55); + border: 1px solid rgba(125,133,151,0.35); + border-radius: 4px; + color: var(--text); + backdrop-filter: blur(2px); + transition: border-color 0.18s, color 0.18s, background 0.18s; +} +.stat-chip:hover { border-color: var(--accent); color: var(--accent); } +.stat-incident { border-color: rgba(30,200,255,0.45); color: var(--accent); } +.stat-iocs { border-color: rgba(74,222,128,0.4); color: var(--green); } +.stat-country { border-color: rgba(251,191,36,0.4); color: var(--amber); } +.stat-confidence { border-color: rgba(167,139,250,0.4); color: #c4b5fd; } +.stat-family { border-color: rgba(248,113,113,0.4); color: var(--red); } diff --git a/src/psyc/cockpit/templates/home.html b/src/psyc/cockpit/templates/home.html index b08e129..9e87006 100644 --- a/src/psyc/cockpit/templates/home.html +++ b/src/psyc/cockpit/templates/home.html @@ -55,8 +55,44 @@