stage-31 polish: featured banner header + clickable news cards w/ hover

Two enhancements from the screenshot annotations:

1. The featured card now has a proper banner header above the hero —
   gradient strip with "⌖ Featured Threat" label, a pulsing severity
   dot, severity/TLP badges, source + ingest time. The header is
   sev-themed (red border + glow for CRITICAL, amber for HIGH), and an
   "Open case →" CTA at the bottom of the hero with an arrow that
   nudges right on hover.

2. News items are now full-card clickable (transparent overlay link)
   with rich severity-tinted hover transitions:
   - Lifts (translateY -1px), tinted background + border that matches
     severity (red CRITICAL, amber HIGH, pale MEDIUM/LOW), per-kind
     accents for enforced/submitted/rejected/failed items.
   - A right-side arrow (→) slides in on hover signaling "click to open".
   - Case glyph SVG gets a subtle drop-shadow + 1.04x scale on hover.
   - 180ms ease transitions across background/border/transform/shadow.
   - Respects prefers-reduced-motion (turns animations off).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m17hr1l
2026-05-25 19:12:55 +02:00
parent f51e672ad3
commit 5cf7cb5655
2 changed files with 119 additions and 13 deletions

View File

@@ -927,3 +927,100 @@ body.wide .net-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr
.news-item.sev-low { border-left-color: var(--muted); }
.news-item .news-icon { background: transparent; border: none; padding: 0; }
.news-item .news-icon .case-glyph-svg { display: block; border: 1px solid var(--border); border-radius: 7px; }
/* ── featured section header (banner above the hero) ───────── */
.featured-section { margin: 0 0 22px; }
.featured-section-head {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; flex-wrap: wrap;
padding: 10px 16px; margin-bottom: 1px;
background: linear-gradient(90deg, rgba(30,200,255,0.10), rgba(15,17,21,0.4));
border: 1px solid var(--border); border-bottom: none;
border-radius: 14px 14px 0 0;
position: relative;
}
.featured-section-head.sev-critical { background: linear-gradient(90deg, rgba(248,113,113,0.16), rgba(15,17,21,0.4)); border-color: var(--red); }
.featured-section-head.sev-high { background: linear-gradient(90deg, rgba(251,191,36,0.14), rgba(15,17,21,0.4)); border-color: var(--amber); }
.featured-section-title { display: inline-flex; align-items: center; gap: 10px; }
.featured-bracket {
font-family: var(--font-display); color: var(--accent); font-size: 18px;
text-shadow: 0 0 12px var(--accent-glow);
}
.featured-section-head.sev-critical .featured-bracket { color: var(--red); text-shadow: 0 0 12px rgba(248,113,113,0.5); }
.featured-section-head.sev-high .featured-bracket { color: var(--amber); text-shadow: 0 0 12px rgba(251,191,36,0.5); }
.featured-section-label {
font-family: var(--font-display); font-size: 13px; font-weight: 600;
letter-spacing: 0.22em; text-transform: uppercase; color: #eaf6ff;
}
.featured-section-pulse {
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
background: var(--accent); box-shadow: 0 0 10px var(--accent);
animation: featured-pulse 1.8s ease-in-out infinite;
}
.featured-section-head.sev-critical .featured-section-pulse { background: var(--red); box-shadow: 0 0 10px var(--red); }
.featured-section-head.sev-high .featured-section-pulse { background: var(--amber); box-shadow: 0 0 10px var(--amber); }
@keyframes featured-pulse { 0%,100% { opacity: 0.5; transform: scale(1); } 50% { opacity: 1; transform: scale(1.3); } }
.featured-section-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 12px; }
.featured-section-meta .muted { color: var(--muted); font-family: ui-monospace, Menlo, monospace; }
.featured { border-radius: 0 0 14px 14px; margin: 0; }
.featured-cta {
display: inline-flex; align-items: center; gap: 6px; margin-top: 8px;
font-family: var(--font-display); font-size: 11px; letter-spacing: 0.16em;
color: var(--accent); text-transform: uppercase;
opacity: 0.85; transition: gap 0.2s, opacity 0.2s;
}
.featured-link:hover .featured-cta, .featured:hover .featured-cta { opacity: 1; gap: 12px; }
.featured-arrow { transition: transform 0.25s ease; }
.featured:hover .featured-arrow { transform: translateX(4px); }
/* ── news-item: clickable card with severity-tinted hover ─── */
.news-item {
position: relative;
display: grid; grid-template-columns: 44px 1fr auto; gap: 12px;
padding: 12px 14px; padding-left: 14px;
margin: 6px 0;
background: rgba(28,34,48,0.0);
border: 1px solid transparent;
border-left: 3px solid transparent;
border-radius: 8px;
transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease, box-shadow 0.22s ease;
}
.news-item.is-link { cursor: pointer; }
/* full-card click target (sits under the content, lets text + chip be selectable on real <a>'s) */
.news-cardlink {
position: absolute; inset: 0; border-radius: inherit;
z-index: 1; text-indent: -9999px; overflow: hidden;
}
.news-item > .news-icon, .news-item > .news-body, .news-item > .news-arrow { position: relative; z-index: 2; pointer-events: none; }
.news-item .news-meta a { pointer-events: auto; } /* explicit links stay clickable */
.news-arrow {
align-self: center; font-family: var(--font-display);
color: var(--accent); font-size: 18px;
opacity: 0; transform: translateX(-6px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.news-item.is-link:hover .news-arrow { opacity: 1; transform: translateX(0); }
/* severity tints on hover */
.news-item:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0,0,0,0.25); }
.news-item.sev-critical:hover { background: rgba(248,113,113,0.06); border-color: rgba(248,113,113,0.55); box-shadow: 0 6px 22px rgba(248,113,113,0.18); }
.news-item.sev-high:hover { background: rgba(251,191,36,0.06); border-color: rgba(251,191,36,0.55); box-shadow: 0 6px 22px rgba(251,191,36,0.16); }
.news-item.sev-medium:hover { background: rgba(253,230,138,0.05); border-color: rgba(253,230,138,0.42); }
.news-item.sev-low:hover { background: rgba(125,133,151,0.06); border-color: var(--border); }
.news-item.news-kind-enforced:hover { background: rgba(30,200,255,0.06); border-color: var(--accent); box-shadow: 0 6px 22px var(--accent-glow); }
.news-item.news-kind-submitted:hover { background: rgba(74,222,128,0.05); border-color: rgba(74,222,128,0.4); }
.news-item.news-kind-rejected:hover, .news-item.news-kind-failed:hover { background: rgba(248,113,113,0.05); border-color: rgba(248,113,113,0.4); }
/* the glyph svg inside the icon — subtle highlight on hover */
.news-item:hover .case-glyph-svg { filter: drop-shadow(0 0 8px var(--accent-glow)); transform: scale(1.04); transition: filter 0.18s, transform 0.18s; }
.news-icon-glyph {
display: grid; place-items: center; width: 36px; height: 36px;
background: var(--panel-2); border: 1px solid var(--border); border-radius: 7px;
font-size: 16px; color: var(--muted);
}
.news-case-id { color: var(--muted); font-family: ui-monospace, Menlo, monospace; }
@media (prefers-reduced-motion: reduce) {
.news-item, .news-arrow, .featured-arrow, .featured-cta, .case-glyph-svg, .featured-section-pulse { transition: none; animation: none; }
}

View File

@@ -38,18 +38,24 @@
</section>
{% if featured %}
<section class="featured sev-{{ featured.classification.severity.value }}">
<a class="featured-link" href="/cases/{{ featured.case_id }}">
<section class="featured-section">
<header class="featured-section-head sev-{{ featured.classification.severity.value }}">
<div class="featured-section-title">
<span class="featured-bracket"></span>
<span class="featured-section-label">Featured Threat</span>
<span class="featured-section-pulse"></span>
</div>
<div class="featured-section-meta">
<span class="sev-badge">{{ featured.classification.severity.value if featured.classification.severity else '—' }}</span>
<span class="tlp-badge tlp-{{ featured.classification.tlp.value }}">{{ featured.classification.tlp.value }}</span>
<span class="muted">{{ featured.source_metadata.feed or 'unknown' }} · {{ featured.ingested_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
</header>
<a class="featured sev-{{ featured.classification.severity.value }}" href="/cases/{{ featured.case_id }}">
<div class="featured-hero">{{ featured_hero|safe }}</div>
<div class="featured-overlay">
<div class="featured-tag">⌖ FEATURED THREAT</div>
<h2 class="featured-title">{{ featured.summary }}</h2>
<div class="featured-meta">
<span class="sev-badge">{{ featured.classification.severity.value if featured.classification.severity else '—' }}</span>
<span class="tlp-badge tlp-{{ featured.classification.tlp.value }}">{{ featured.classification.tlp.value }}</span>
<span class="muted">from {{ featured.source_metadata.feed or 'unknown' }}</span>
<span class="muted">ingested {{ featured.ingested_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
<h3 class="featured-title">{{ featured.summary }}</h3>
<div class="featured-cta">Open case <span class="featured-arrow"></span></div>
</div>
</a>
</section>
@@ -71,12 +77,14 @@
<h3 class="bucket-head">{{ b.label }} <span class="bucket-count">{{ b.items|length }}</span></h3>
<ol class="news-list">
{% for i in b.items %}
<li class="news-item news-kind-{{ i.kind }}{% if i.severity %} sev-{{ i.severity }}{% endif %}">
{% set has_link = i.case_id %}
<li class="news-item news-kind-{{ i.kind }}{% if i.severity %} sev-{{ i.severity }}{% endif %}{% if has_link %} is-link{% endif %}">
{% if has_link %}<a class="news-cardlink" href="/cases/{{ i.case_id }}" aria-label="Open {{ i.case_id }}"></a>{% endif %}
<div class="news-icon">
{% if i.kind == 'case' and i.case_id and case_glyphs.get(i.case_id) %}
{{ case_glyphs[i.case_id]|safe }}
{% else %}
{{ i.icon }}
<span class="news-icon-glyph">{{ i.icon }}</span>
{% endif %}
</div>
<div class="news-body">
@@ -87,9 +95,10 @@
<div class="news-sub">{{ i.body }}</div>
<div class="news-meta">
<time>{{ i.timestamp.strftime('%H:%M' if b.label == 'Today' else '%Y-%m-%d %H:%M') }}</time>
{% if i.case_id %} · <a href="/cases/{{ i.case_id }}">{{ i.case_id }}</a>{% endif %}
{% if i.case_id %} · <span class="news-case-id">{{ i.case_id }}</span>{% endif %}
</div>
</div>
{% if has_link %}<span class="news-arrow" aria-hidden="true"></span>{% endif %}
</li>
{% endfor %}
</ol>