Rebuild the frontend to the refit design

Space Grotesk for prose, IBM Plex Mono for every identifier, and the flat
hairline-bordered surfaces from the handoff. Green marks what you can act
on and amber only ever means a star count; nothing else is coloured.

Three changes to how the same data reads. Descriptions clamp to three lines
behind an expander, so a kernel CVE no longer pushes the links people came
for below the fold. Raw URLs become owner / repo rows carrying stars and
last-commit age, ranked best first, five at a time. And an empty search box
now keeps the trending PoCs on screen with a TRENDING / RECENT toggle
rather than showing an empty page.

The search grammar, ranking and negation are unchanged.
This commit is contained in:
0xMarcio committed 2026-08-30 21:53:31 +02:00
1 parent 576b0e4735
commit 066ddf4400
4 files changed
+775 -792

No files matched your search

+1
View File
@@ -1,6 +1,7 @@
data/
.venv/
docs/CVE_list.json
docs/repo_meta.json
docs/trending_poc.json
docs/api/v1/cve/
__pycache__/
+46 -35
View File
@@ -4,53 +4,64 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CVE PoC Hub</title>
<meta name="description" content="Search publicly available proof-of-concept exploits by CVE, vendor or product." />
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/style.css" />
</head>
<body class="color-no-search">
<main class="wrap">
<section class="hero" data-search-root>
<h1>CVE PoC Hub</h1>
<div class="pill-row tight">
<body>
<header class="masthead">
<div class="container">
<h1 class="wordmark">CVE PoC Hub</h1>
<div class="pills">
<span class="pill">Linked PoCs</span>
<span class="pill">MITRE CVE pages</span>
<span class="pill">Vendor / product filters</span>
<span class="pill">Supports negative terms (e.g. <strong>-windows</strong>)</span>
<span class="pill">Supports negative terms (e.g. -windows)</span>
</div>
<form class="searchForm" action="#">
<input type="text" class="search" placeholder="Search CVE, vendor, product, or keyword" autocomplete="off">
<form class="search" role="search" autocomplete="off">
<span class="search-glyph" aria-hidden="true">⌕</span>
<input type="text" spellcheck="false" data-search
placeholder="Search CVE, vendor, product, or keyword"
aria-label="Search CVE, vendor, product, or keyword" />
<span class="search-status" data-status aria-live="polite"></span>
</form>
<div class="search-results" data-results style="display:none">
<div class="header">
<h2>Results</h2>
</div>
<div class="noResults">No results yet.</div>
<div class="results-table hide">
<table class="results">
<thead>
<tr>
<td width="18%">CVE</td>
<td>Description / PoC links</td>
</tr>
</thead>
<tbody class="results"></tbody>
</table>
</div>
</div>
</section>
</div>
</header>
<section class="section" data-trending-section>
<h2>Recently updated Proof-of-Concepts</h2>
<div class="table-wrap" data-trending>
<table>
<thead><tr><th>Stars</th><th>Updated</th><th>Name</th><th>Description</th></tr></thead>
<tbody id="trending-body">
<tr><td colspan="4">No recent PoCs.</td></tr>
</tbody>
</table>
<main class="container content">
<section class="card results" data-results hidden></section>
<section data-trending>
<div class="trend-head">
<h2 data-trend-title>Trending Proof-of-Concepts</h2>
<div class="toggle" role="group" aria-label="Ranking">
<button type="button" data-mode="TRENDING" aria-pressed="true">TRENDING</button>
<button type="button" data-mode="RECENT" aria-pressed="false">RECENT</button>
</div>
<span class="trend-note" data-trend-note></span>
</div>
<div class="card trend-card">
<div class="col-head">
<span class="col-stars">STARS</span>
<span class="col-updated">UPDATED</span>
<span>NAME</span>
<span class="col-desc">DESCRIPTION</span>
</div>
<div data-trend-rows></div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container">
<span>sources: nvd · mitre · github</span>
<span data-refreshed></span>
</div>
</footer>
<script src="/logic.js"></script>
</body>
</html>
+448 -364
View File
@@ -1,423 +1,507 @@
const totalLimit = 10000;
const replaceStrings = ['HackTheBox - ', 'VulnHub - ', 'UHC - '];
const colorUpdate = document.body;
'use strict';
function getSearchRoot() {
return document.querySelector('[data-search-root]');
const REPLACE_STRINGS = ['HackTheBox - ', 'VulnHub - ', 'UHC - '];
const PAGE_SIZE = 50;
const POC_PREVIEW = 5;
const TREND_ROWS = 20;
const state = {
query: '',
mode: 'TRENDING',
shown: PAGE_SIZE,
descOpen: new Set(),
pocOpen: new Set(),
ready: false,
results: []
};
let dataset = [];
let repoMeta = {};
let trending = [];
let indexMeta = null;
/* ---- formatting -------------------------------------------------------- */
function escapeHTML(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, ch => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[ch]));
}
function getTrendingSection() {
return document.querySelector('[data-trending-section]');
function formatCount(n) {
return n.toLocaleString('en-US');
}
function getTrendingBody() {
return document.getElementById('trending-body');
function formatStars(n) {
return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
}
function escapeHTML(str) {
return str.replace(/[&<>"']/g, match => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[match]));
function hoursSince(iso) {
if (!iso) return null;
const then = Date.parse(iso.length === 10 ? iso + 'T00:00:00Z' : iso);
if (Number.isNaN(then)) return null;
return (Date.now() - then) / 36e5;
}
/** Long form for the trending table: "3 hours ago", "4 months ago". */
function longAge(hours) {
if (hours == null) return '';
const units = [
[8760, 'year'], [720, 'month'], [168, 'week'], [24, 'day'], [1, 'hour']
];
for (const [size, name] of units) {
if (hours >= size) {
const n = Math.floor(hours / size);
return `${n} ${name}${n === 1 ? '' : 's'} ago`;
}
}
const minutes = Math.max(1, Math.round(hours * 60));
return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
}
/** Compact age for the PoC columns: 3h, 3d, 2w, 5mo, 2y. */
function shortAge(hours) {
if (hours == null) return '';
if (hours < 24) return Math.max(1, Math.round(hours)) + 'h';
const days = hours / 24;
if (days < 14) return Math.round(days) + 'd';
if (days < 60) return Math.round(days / 7) + 'w';
if (days < 730) return Math.round(days / 30) + 'mo';
return Math.round(days / 365) + 'y';
}
function repoFromUrl(url) {
const match = /^https?:\/\/(?:www\.)?github\.com\/([^/#?]+)\/([^/#?]+)/i.exec(url || '');
if (!match) return null;
return { owner: match[1], repo: match[2].replace(/\.git$/i, '') };
}
/** Everything after the host, for links that are not GitHub repositories. */
function plainLinkLabel(url) {
try {
const parsed = new URL(url);
return parsed.host.replace(/^www\./, '') + parsed.pathname.replace(/\/$/, '');
} catch (err) {
return url;
}
}
/* ---- matching ---------------------------------------------------------- */
function normalizeToSpaces(value) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
}
function buildLooseRegex(value) {
const compact = value.toLowerCase().replace(/[^a-z0-9]+/g, '');
if (compact.length < 4) {
return null;
}
const escaped = compact.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = escaped.split('').join('[^a-z0-9]*');
return new RegExp(pattern);
const compact = value.toLowerCase().replace(/[^a-z0-9]+/g, '');
if (compact.length < 4) return null;
const escaped = compact.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(escaped.split('').join('[^a-z0-9]*'));
}
function buildMatcher(term) {
const raw = term.toLowerCase().trim();
if (!raw) return null;
const isPhrase = /\s/.test(raw);
const normalized = raw.replace(/[^a-z0-9]+/g, '');
return {
raw,
normalized,
isPhrase,
phrase: isPhrase ? normalizeToSpaces(raw) : '',
loose: !isPhrase && normalized.length >= 4 ? buildLooseRegex(raw) : null
};
}
function linkItem(link) {
const safe = escapeHTML(link);
return `<li><a target="_blank" rel="noopener" href="${safe}">${safe}</a></li>`;
}
function convertLinksToList(links) {
if (links.length === 0) {
return '';
}
let htmlOutput = `<div class="poc-container"><ul>`;
const displayLimit = 5;
links.slice(0, displayLimit).forEach(link => {
htmlOutput += linkItem(link);
});
htmlOutput += `</ul>`;
if (links.length > displayLimit) {
htmlOutput += `
<ul class="dropdown" style="display:none;">
${links.slice(displayLimit).map(linkItem).join('')}
</ul>
<button class="dropdown-btn" onclick="toggleDropdown(this)">Show More</button>`;
}
htmlOutput += `</div>`;
return htmlOutput;
}
function toggleDropdown(button) {
const dropdown = button.previousElementSibling;
if (dropdown.style.display === "none") {
dropdown.style.display = "block";
button.textContent = "Show Less";
} else {
dropdown.style.display = "none";
button.textContent = "Show More";
}
}
window.toggleDropdown = toggleDropdown;
function getCveLink(cveId) {
return `<a href="https://nvd.nist.gov/vuln/detail/${cveId}" target="_blank"><b>${cveId}</b></a>`;
}
function pocText(entry) {
if (entry._pocText === undefined) {
entry._pocText = (entry.poc || []).join(' ').toLowerCase();
}
return entry._pocText;
}
function pocSpace(entry) {
if (entry._pocSpace === undefined) {
entry._pocSpace = normalizeToSpaces(pocText(entry));
}
return entry._pocSpace;
const raw = term.toLowerCase().trim();
if (!raw) return null;
const isPhrase = /\s/.test(raw);
const normalized = raw.replace(/[^a-z0-9]+/g, '');
return {
raw,
isPhrase,
phrase: isPhrase ? normalizeToSpaces(raw) : '',
loose: !isPhrase && normalized.length >= 4 ? buildLooseRegex(raw) : null
};
}
// Plain substring matching made "rce" hit "open source" and "sudo" hit
// "pseudo". Require the term to start a word; matching into the end of one is
// still allowed so that "word" finds "wordpress".
function wordStart(text, term) {
let at = text.indexOf(term);
while (at > 0) {
const code = text.charCodeAt(at - 1);
if (!((code > 96 && code < 123) || (code > 47 && code < 58))) return at;
at = text.indexOf(term, at + 1);
}
return at;
let at = text.indexOf(term);
while (at > 0) {
const code = text.charCodeAt(at - 1);
if (!((code > 96 && code < 123) || (code > 47 && code < 58))) return at;
at = text.indexOf(term, at + 1);
}
return at;
}
// A term in the opening of an advisory is what it is about; one buried deep in
// the prose is usually incidental. Coarse buckets so that a few characters of
// difference never outweighs the newest-first tie-break.
function leadBonus(index) {
if (index < 0) return 0;
if (index < 80) return 80;
if (index < 300) return 40;
return 0;
if (index < 0) return 0;
if (index < 80) return 80;
if (index < 300) return 40;
return 0;
}
function pocText(entry) {
if (entry._pocText === undefined) {
entry._pocText = (entry.poc || []).join(' ').toLowerCase();
}
return entry._pocText;
}
function pocSpace(entry) {
if (entry._pocSpace === undefined) {
entry._pocSpace = normalizeToSpaces(pocText(entry));
}
return entry._pocSpace;
}
function descSpace(entry) {
if (entry._descSpace === undefined) {
entry._descSpace = normalizeToSpaces(entry._descText);
if (entry._descSpace === undefined) {
entry._descSpace = normalizeToSpaces(entry._descText);
}
return entry._descSpace;
}
function scoreEntry(entry, matcher) {
if (!matcher || !matcher.raw) return 0;
if (matcher.isPhrase) {
const phrase = matcher.phrase;
if (!phrase) return 0;
const at = wordStart(descSpace(entry), phrase);
if (at >= 0) return 200 + leadBonus(at);
if (wordStart(pocSpace(entry), phrase) >= 0) return 80;
return 0;
}
const raw = matcher.raw;
if (entry._cveText.includes(raw)) return 600;
const at = wordStart(entry._descText, raw);
if (at >= 0) return 240 + leadBonus(at);
if (wordStart(pocText(entry), raw) >= 0) return 80;
if (matcher.loose) {
if (matcher.loose.test(entry._descText)) return 160;
if (matcher.loose.test(pocText(entry))) return 60;
}
return 0;
}
function runSearch(query) {
const terms = query.match(/-?"[^"]+"|-?\S+/g) || [];
const cleaned = terms.map(t => t.replace(/^(-?)"/, '$1').replace(/"$/, ''));
const positive = cleaned.filter(t => t && t[0] !== '-');
const matchers = positive.map(buildMatcher).filter(Boolean);
// Unquoted words are matched independently, so "active directory" also hits a
// description holding "active session" and "working directory". Reward the
// words appearing together to keep those below real matches.
const adjacent = positive.length > 1 ? positive.join(' ').toLowerCase() : '';
const negatives = cleaned
.filter(t => t && t[0] === '-')
.map(t => t.slice(1))
.filter(Boolean)
.map(buildMatcher)
.filter(Boolean);
const results = [];
for (const entry of dataset) {
let score = 0;
let matched = true;
for (const matcher of matchers) {
const termScore = scoreEntry(entry, matcher);
if (termScore === 0) { matched = false; break; }
score += termScore;
}
return entry._descSpace;
if (!matched) continue;
if (negatives.some(m => scoreEntry(entry, m) > 0)) continue;
if (adjacent) {
const at = wordStart(entry._descText, adjacent);
if (at >= 0) score += 300 + leadBonus(at);
else if (wordStart(pocText(entry), adjacent) >= 0) score += 100;
}
entry._score = score;
results.push(entry);
}
results.sort((a, b) => {
if (b._score !== a._score) return b._score - a._score;
if (b._year !== a._year) return b._year - a._year;
return b._num - a._num;
});
return results;
}
function prepareDataset(raw) {
if (!Array.isArray(raw)) return [];
const dataset = [];
for (const entry of raw) {
const cve = (entry.cve || '').trim();
if (!cve || !Array.isArray(entry.poc) || entry.poc.length === 0) continue;
const parts = cve.split('-');
entry._cveText = cve.toLowerCase();
entry._descText = replaceStrings
.reduce((desc, str) => desc.replace(str, ''), entry.desc || '')
.toLowerCase();
entry._year = parseInt(parts[1], 10) || 0;
entry._num = parseInt(parts[2], 10) || 0;
dataset.push(entry);
}
return dataset;
if (!Array.isArray(raw)) return [];
const out = [];
for (const entry of raw) {
const cve = (entry.cve || '').trim();
if (!cve || !Array.isArray(entry.poc) || entry.poc.length === 0) continue;
const parts = cve.split('-');
entry._cveText = cve.toLowerCase();
entry._descText = REPLACE_STRINGS
.reduce((desc, str) => desc.replace(str, ''), entry.desc || '')
.toLowerCase();
entry._year = parseInt(parts[1], 10) || 0;
entry._num = parseInt(parts[2], 10) || 0;
out.push(entry);
}
return out;
}
const controls = {
oldColor: '',
displayResults(results, resultsTableHideable) {
results.style.display = '';
resultsTableHideable.classList.remove('hide');
},
hideResults(results, resultsTableHideable) {
results.style.display = 'none';
resultsTableHideable.classList.add('hide');
},
scoreEntry(entry, matcher) {
if (!matcher || !matcher.raw) return 0;
/* ---- rendering --------------------------------------------------------- */
if (matcher.isPhrase) {
const phrase = matcher.phrase;
if (!phrase) return 0;
const at = wordStart(descSpace(entry), phrase);
if (at >= 0) return 200 + leadBonus(at);
if (wordStart(pocSpace(entry), phrase) >= 0) return 80;
return 0;
}
const raw = matcher.raw;
if (entry._cveText.includes(raw)) return 600;
const at = wordStart(entry._descText, raw);
if (at >= 0) return 240 + leadBonus(at);
if (wordStart(pocText(entry), raw) >= 0) return 80;
if (matcher.loose) {
if (matcher.loose.test(entry._descText)) return 160;
if (matcher.loose.test(pocText(entry))) return 60;
}
return 0;
},
doSearch(match, dataset) {
const terms = match.match(/-?"[^"]+"|-?\S+/g) || [];
const cleaned = terms.map(term => term.replace(/^(-?)"/, '$1').replace(/"$/, ''));
const positive = cleaned.filter(term => term && term[0] !== '-');
const posmatch = positive.map(buildMatcher).filter(Boolean);
// Unquoted words are matched independently, so "active directory" also
// hits a description holding "active session" and "working directory".
// Reward the words appearing together to keep those below real matches.
const adjacent = positive.length > 1 ? positive.join(' ').toLowerCase() : '';
const negmatch = cleaned
.filter(term => term && term[0] === '-')
.map(term => term.substring(1))
.filter(Boolean)
.map(buildMatcher)
.filter(Boolean);
const results = [];
for (const entry of dataset) {
let score = 0;
let matched = true;
for (const matcher of posmatch) {
const termScore = this.scoreEntry(entry, matcher);
if (termScore === 0) {
matched = false;
break;
}
score += termScore;
}
if (!matched) continue;
const hasNegative = negmatch.some(matcher => this.scoreEntry(entry, matcher) > 0);
if (hasNegative) continue;
if (adjacent) {
const at = wordStart(entry._descText, adjacent);
if (at >= 0) {
score += 300 + leadBonus(at);
} else if (wordStart(pocText(entry), adjacent) >= 0) {
score += 100;
}
}
entry._score = score;
results.push(entry);
}
results.sort((a, b) => {
if (b._score !== a._score) return b._score - a._score;
if (b._year !== a._year) return b._year - a._year;
return b._num - a._num;
});
return results;
},
updateResults(loc, results, noResults, resultsTableHideable) {
if (results.length === 0) {
noResults.style.display = '';
noResults.textContent = 'No results found — try another vendor, product, or CVE id.';
resultsTableHideable.classList.add('hide');
} else if (results.length > totalLimit) {
noResults.style.display = '';
resultsTableHideable.classList.add('hide');
noResults.textContent = 'Error: ' + results.length + ' results were found, try being more specific';
this.setColor(colorUpdate, 'too-many-results');
} else {
loc.innerHTML = '';
noResults.style.display = 'none';
resultsTableHideable.classList.remove('hide');
const html = results.map(r =>
'<tr><td class="cveNum">' + getCveLink(r.cve) +
'</td><td class="desc">' + escapeHTML(r.desc || '') + ' ' +
convertLinksToList(r.poc || []) + '</td></tr>'
).join('');
loc.innerHTML = html;
}
},
setColor(loc, indicator) {
if (this.oldColor === indicator) return;
loc.className = loc.className.replace(/\bcolor-\S+/g, '');
loc.classList.add('color-' + indicator);
this.oldColor = indicator;
}
const el = {
input: document.querySelector('[data-search]'),
status: document.querySelector('[data-status]'),
results: document.querySelector('[data-results]'),
trending: document.querySelector('[data-trending]'),
trendTitle: document.querySelector('[data-trend-title]'),
trendNote: document.querySelector('[data-trend-note]'),
trendRows: document.querySelector('[data-trend-rows]'),
refreshed: document.querySelector('[data-refreshed]')
};
window.controls = controls;
function pocRow(url) {
const parsed = repoFromUrl(url);
const href = escapeHTML(url);
if (!parsed) {
return `<div class="poc-row"><span class="poc-name">` +
`<a href="${href}" target="_blank" rel="noopener">${escapeHTML(plainLinkLabel(url))}</a>` +
`</span><span class="poc-stars"></span><span class="poc-age"></span></div>`;
}
const key = (parsed.owner + '/' + parsed.repo).toLowerCase();
const meta = repoMeta[key];
const stars = meta ? meta[0] : null;
const hours = meta ? hoursSince(meta[1]) : null;
const starClass = stars != null && stars >= 500 ? 'poc-stars is-popular' : 'poc-stars';
return `<div class="poc-row">` +
`<span class="poc-name"><span class="poc-owner">${escapeHTML(parsed.owner)}</span>` +
`<span class="poc-sep"> / </span>` +
`<a href="${href}" target="_blank" rel="noopener">${escapeHTML(parsed.repo)}</a></span>` +
`<span class="${starClass}">${stars == null ? '' : formatStars(stars) + ' ★'}</span>` +
`<span class="poc-age">${escapeHTML(shortAge(hours))}</span></div>`;
}
document.addEventListener('DOMContentLoaded', () => {
const root = getSearchRoot();
const trendingSection = getTrendingSection();
const trendingBody = getTrendingBody();
if (!root) return;
/** Best-known repositories first, so the five shown by default are the five
* worth opening. Links without a star count keep their original order behind
* them; references from the CVE record have no repository to rank. */
function rankedLinks(entry) {
if (entry._ranked) return entry._ranked;
const scored = (entry.poc || []).map((url, index) => {
const parsed = repoFromUrl(url);
const meta = parsed && repoMeta[(parsed.owner + '/' + parsed.repo).toLowerCase()];
return { url, index, stars: meta ? meta[0] : -1 };
});
scored.sort((a, b) => (b.stars - a.stars) || (a.index - b.index));
entry._ranked = scored.map(item => item.url);
return entry._ranked;
}
const results = root.querySelector('[data-results]');
const searchValue = root.querySelector('input.search');
const form = root.querySelector('form.searchForm');
const resultsTableHideable = root.querySelector('.results-table');
const resultsTable = root.querySelector('tbody.results');
const noResults = root.querySelector('div.noResults');
function resultRow(entry) {
const id = entry.cve;
const open = state.descOpen.has(id);
const all = state.pocOpen.has(id);
const links = rankedLinks(entry);
const visible = all ? links : links.slice(0, POC_PREVIEW);
const more = all
? '↑ show fewer'
: (links.length > POC_PREVIEW
? `+ ${formatCount(links.length - POC_PREVIEW)} more repositories`
: 'all repositories shown');
document.body.classList.add('fade');
const dates = [];
if (entry.published) dates.push(['published', entry.published]);
if (entry.modified) dates.push(['modified', entry.modified]);
const dateHtml = dates.length
? `<dl class="result-dates">${dates.map(([label, value]) =>
`<div class="result-date"><dt>${label}</dt><dd>${escapeHTML(value)}</dd></div>`).join('')}</dl>`
: '';
if (!results || !searchValue || !form || !resultsTableHideable || !resultsTable || !noResults) {
console.warn('Search container missing expected elements');
return;
}
return `<div class="result-row">
<div class="result-meta">
<a class="result-id" href="https://nvd.nist.gov/vuln/detail/${encodeURIComponent(id)}" target="_blank" rel="noopener">${escapeHTML(id)}</a>
<div class="result-pocs">${formatCount(links.length)} linked PoC${links.length === 1 ? '' : 's'}</div>
${dateHtml}
<a class="mitre" href="https://www.cve.org/CVERecord?id=${encodeURIComponent(id)}" target="_blank" rel="noopener">MITRE ↗</a>
</div>
<div class="result-body">
<p class="result-desc${open ? ' is-open' : ''}">${escapeHTML(entry.desc || '')}</p>
<button type="button" class="expander" data-toggle-desc="${escapeHTML(id)}">${open ? '↑ collapse' : '↓ full description'}</button>
<div class="poc-list">
${visible.map(pocRow).join('')}
<button type="button" class="poc-more" data-toggle-poc="${escapeHTML(id)}">${more}</button>
</div>
</div>
</div>`;
}
let currentSet = [];
function renderResults(elapsed) {
const results = state.results;
el.trending.hidden = true;
el.results.hidden = false;
let datasetReady = false;
let debounceTimer;
if (!results.length) {
el.results.innerHTML = `<div class="empty">
<div class="empty-head">No CVE matched <b>${escapeHTML(state.query)}</b></div>
<div class="empty-hint">Try a CVE ID, a vendor, or a product name. Prefix a term with <code>-</code> to exclude it.</div>
</div>`;
return;
}
function renderTrending(items) {
if (!trendingBody) return;
if (!items || items.length === 0) {
trendingBody.innerHTML = '<tr><td colspan="4">No recent PoCs.</td></tr>';
return;
}
const rows = items.slice(0, 20).map(item => {
const stars = item.stars ?? '';
const updated = escapeHTML(item.updated || '');
const name = escapeHTML(item.name || '');
const url = item.url || '#';
const desc = escapeHTML(item.desc || '');
return `<tr><td>${stars}⭐</td><td>${updated}</td><td><a href="${url}" target="_blank">${name}</a></td><td class="mono">${desc}</td></tr>`;
}).join('');
trendingBody.innerHTML = rows;
}
const shown = results.slice(0, state.shown);
const pocTotal = results.reduce((sum, r) => sum + (r.poc || []).length, 0);
const remaining = results.length - shown.length;
const footer = remaining > 0
? `<button type="button" class="poc-more" data-more-results>+ ${formatCount(remaining)} more matching CVEs</button>`
: '';
async function loadTrending() {
if (!trendingBody) return;
try {
const res = await fetch('/trending_poc.json', { cache: 'no-store' });
if (!res.ok) {
throw new Error(`Failed to load trending (${res.status})`);
}
const data = await res.json();
const items = Array.isArray(data) ? data : (data.items || []);
renderTrending(items);
} catch (err) {
console.warn(err.message);
}
}
el.results.innerHTML = `<div class="card-head">
<h2>Results</h2>
<span class="card-count">${formatCount(results.length)} CVE${results.length === 1 ? '' : 's'} · ${formatCount(pocTotal)} PoCs</span>
</div>
<div class="col-head"><span>CVE</span><span>DESCRIPTION / POC LINKS</span></div>
${shown.map(resultRow).join('')}${footer}`;
function doSearch(event) {
const val = searchValue.value.trim();
if (elapsed != null) {
el.status.textContent = `matched in ${Math.max(1, Math.round(elapsed))}ms`;
}
}
if (val !== '') {
controls.displayResults(results, resultsTableHideable);
if (trendingSection) {
trendingSection.style.display = 'none';
}
// Typing before the CVE list lands would otherwise report no results.
if (!datasetReady) {
resultsTableHideable.classList.add('hide');
noResults.style.display = '';
noResults.textContent = 'Loading CVE data…';
if (event && event.type === 'submit') {
event.preventDefault();
}
return;
}
currentSet = window.controls.doSearch(val, window.dataset || []);
function trendRow(item) {
const popular = item.stars >= 500 ? ' is-popular' : '';
return `<div class="trend-row">
<span class="trend-stars${popular}">${formatStars(item.stars)} ★</span>
<span class="trend-age">${escapeHTML(longAge(item._pushed))}</span>
<a class="trend-name" href="${escapeHTML(item.url)}" target="_blank" rel="noopener">${escapeHTML(item.name)}</a>
<span class="trend-desc">${escapeHTML(item.desc || '')}</span>
</div>`;
}
if (currentSet.length < totalLimit) {
window.controls.setColor(colorUpdate, currentSet.length === 0 ? 'no-results' : 'results-found');
}
function renderTrending() {
el.results.hidden = true;
el.trending.hidden = false;
window.controls.updateResults(resultsTable, currentSet, noResults, resultsTableHideable);
} else {
controls.hideResults(results, resultsTableHideable);
window.controls.setColor(colorUpdate, 'no-search');
noResults.style.display = 'none';
if (trendingSection) {
trendingSection.style.display = '';
}
}
const ranked = trending.slice();
if (state.mode === 'TRENDING') {
// Stars weighted against how long the repository has existed, so a
// fast-climbing new PoC outranks an older one with a bigger absolute count.
const rank = r => r.stars / Math.pow((r._created == null ? 8760 : r._created) + 6, 0.45);
ranked.sort((a, b) => rank(b) - rank(a));
} else {
ranked.sort((a, b) => (a._pushed == null ? Infinity : a._pushed) - (b._pushed == null ? Infinity : b._pushed));
}
const rows = ranked.slice(0, TREND_ROWS);
if (event && event.type === 'submit') {
event.preventDefault();
}
}
el.trendTitle.textContent = state.mode === 'TRENDING'
? 'Trending Proof-of-Concepts'
: 'Recently updated Proof-of-Concepts';
const cveListCandidates = [
new URL('/CVE_list.json', window.location.origin).href,
new URL('CVE_list.json', window.location.href).href,
new URL('../CVE_list.json', window.location.href).href
];
el.trendNote.textContent = state.mode === 'TRENDING'
? 'stars gained relative to repository age'
: `newest commit first · ${formatCount(trending.length)} repositories`;
(async () => {
for (const url of cveListCandidates) {
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to load ${url} (${res.status})`);
}
const data = await res.json();
window.dataset = prepareDataset(data);
currentSet = window.dataset;
datasetReady = true;
if (searchValue.value.trim() !== '') {
doSearch();
} else {
controls.hideResults(results, resultsTableHideable);
noResults.style.display = 'none';
window.controls.setColor(colorUpdate, 'no-search');
}
return;
} catch (err) {
console.warn(err.message);
}
}
window.dataset = [];
datasetReady = true;
noResults.textContent = 'Unable to load CVE list';
noResults.style.display = '';
controls.setColor(colorUpdate, 'no-results');
})();
el.trendRows.innerHTML = rows.length
? rows.map(trendRow).join('')
: '<div class="trend-row"><span class="trend-desc">No recent PoCs.</span></div>';
form.addEventListener('submit', doSearch);
document.querySelectorAll('.toggle button').forEach(button => {
button.setAttribute('aria-pressed', String(button.dataset.mode === state.mode));
});
}
searchValue.addEventListener('input', event => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => doSearch(event), 200);
});
function idleStatus() {
if (!indexMeta) return '';
return `${formatCount(indexMeta.total_cves)} CVEs · ${formatCount(indexMeta.with_pocs)} with PoCs`;
}
loadTrending();
function render() {
if (!state.query) {
el.status.textContent = state.ready ? idleStatus() : 'loading index…';
renderTrending();
return;
}
if (!state.ready) {
el.results.hidden = false;
el.trending.hidden = true;
el.status.textContent = 'loading index…';
el.results.innerHTML = `<div class="empty">
<div class="empty-head">Loading the CVE index…</div>
<div class="empty-hint">The full index is a single download; results appear as soon as it lands.</div>
</div>`;
return;
}
const started = performance.now();
state.results = runSearch(state.query);
renderResults(performance.now() - started);
}
/* ---- wiring ------------------------------------------------------------ */
el.input.addEventListener('input', () => {
state.query = el.input.value.trim();
state.shown = PAGE_SIZE;
render();
});
document.querySelector('.search').addEventListener('submit', event => event.preventDefault());
document.querySelector('.toggle').addEventListener('click', event => {
const button = event.target.closest('button[data-mode]');
if (!button) return;
state.mode = button.dataset.mode;
renderTrending();
});
el.results.addEventListener('click', event => {
const desc = event.target.closest('[data-toggle-desc]');
const poc = event.target.closest('[data-toggle-poc]');
const more = event.target.closest('[data-more-results]');
if (desc) {
const id = desc.dataset.toggleDesc;
state.descOpen.has(id) ? state.descOpen.delete(id) : state.descOpen.add(id);
} else if (poc) {
const id = poc.dataset.togglePoc;
state.pocOpen.has(id) ? state.pocOpen.delete(id) : state.pocOpen.add(id);
} else if (more) {
state.shown += PAGE_SIZE;
} else {
return;
}
renderResults(null);
});
async function loadJSON(url, options) {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`${url} (${response.status})`);
return response.json();
}
(async () => {
try {
const meta = await loadJSON('/trending_poc.json', { cache: 'no-store' });
indexMeta = meta;
trending = (meta.items || []).map(item => Object.assign({}, item, {
_pushed: hoursSince(item.pushed),
_created: hoursSince(item.created)
}));
const minutes = Math.max(0, Math.round((Date.now() - Date.parse(meta.generated)) / 60000));
el.refreshed.textContent = minutes < 60
? `index refreshed ${minutes} minute${minutes === 1 ? '' : 's'} ago`
: `index refreshed ${Math.round(minutes / 60)} hour${Math.round(minutes / 60) === 1 ? '' : 's'} ago`;
} catch (err) {
console.warn(err.message);
}
render();
// The repository metadata is optional: without it the PoC rows simply lose
// their star and age columns, which is the documented fallback.
loadJSON('/repo_meta.json').then(data => {
repoMeta = data || {};
for (const entry of dataset) entry._ranked = null;
if (state.query && state.ready) renderResults(null);
}).catch(err => console.warn(err.message));
try {
dataset = prepareDataset(await loadJSON('/CVE_list.json'));
state.ready = true;
render();
} catch (err) {
console.warn(err.message);
el.status.textContent = 'index unavailable';
}
})();
+280 -393
View File
@@ -1,436 +1,323 @@
@import url("https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;600&display=swap");
/* CVE PoC Hub — design tokens from the frontend refit handoff. */
:root {
--bg: #04060f;
--bg-2: #0a0f24;
--panel: rgba(255, 255, 255, 0.04);
--panel-strong: rgba(255, 255, 255, 0.1);
--text: #e7ecf5;
--muted: #94a3c7;
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.16);
--accent: #7af0c1;
--accent-2: #6fb3ff;
--danger: #ff8a8a;
--success: #7ee0ff;
font-family: 'Sora', 'IBM Plex Mono', system-ui, -apple-system, sans-serif;
--page: #0a1114;
--header-top: #102026;
--header-bottom: #0d191e;
--surface: #0d171b;
--surface-sunken: #08100f;
--table-header: #0b1519;
--footer: #0b1418;
--row-hover: #101d21;
--pill: #0e1b20;
--border-strong: #23353a;
--border: #1d2c31;
--border-faint: #162327;
--border-hairline: #142023;
--text-primary: #f2f7f6;
--text: #eaf3f1;
--text-muted: #9fb3b0;
--prose: #8fa3a0;
--text-dim: #7f9491;
--text-faint: #5c6f6c;
--text-faintest: #4a5c59;
--separator: #3d4d4a;
--accent: #58e39b;
--accent-hover: #7df0b3;
--star: #e8b84b;
--mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--sans: 'Space Grotesk', system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
*, *::before, *::after { box-sizing: border-box; }
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
html, body { margin: 0; padding: 0; background: var(--page); min-height: 100%; }
body {
margin: 0;
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
min-height: 100vh;
background: radial-gradient(circle at 18% 20%, rgba(122, 240, 193, 0.08), transparent 26%),
radial-gradient(circle at 82% 12%, rgba(111, 179, 255, 0.12), transparent 22%),
linear-gradient(130deg, #04060f 0%, #070d1c 55%, #040712 100%);
color: var(--text);
font-family: 'Sora', 'IBM Plex Mono', system-ui, -apple-system, sans-serif;
line-height: 1.6;
overflow-x: hidden;
transition: background 0.6s ease;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-2); }
a:hover { color: var(--accent-hover); }
.wrap { width: min(1180px, 94vw); margin: 0 auto; padding: 0 0 2rem; }
.site-header {
position: sticky;
top: 0;
z-index: 10;
background: rgba(8, 12, 26, 0.88);
border-bottom: 1px solid var(--border);
backdrop-filter: blur(12px);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 2px;
}
.site-header .wrap {
.container { max-width: 1180px; margin: 0 auto; padding: 0 40px; width: 100%; }
/* ---- header ------------------------------------------------------------ */
.masthead {
background: linear-gradient(180deg, var(--header-top) 0%, var(--header-bottom) 100%);
border-bottom: 1px solid var(--border);
padding: 44px 0 38px;
}
.wordmark {
margin: 0;
font: 700 46px/1 var(--sans);
color: var(--text-primary);
letter-spacing: -.035em;
}
.pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
.pill {
font: 400 12px/1 var(--sans);
color: var(--text-muted);
border: 1px solid var(--border-strong);
background: var(--pill);
border-radius: 20px;
padding: 7px 13px;
}
.search {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 0.5rem;
gap: 12px;
margin-top: 22px;
background: var(--surface-sunken);
border: 1px solid var(--border-strong);
border-radius: 8px;
padding: 15px 18px;
}
.brand a {
font-weight: 700;
color: #fff;
font-size: 1.2rem;
letter-spacing: -0.02em;
.search-glyph { font: 400 14px/1 var(--mono); color: var(--text-faintest); }
.search input {
flex: 1;
min-width: 0;
background: transparent;
border: 0;
outline: 0;
padding: 0;
font: 400 15px/1 var(--mono);
color: var(--text);
}
.brand span {
display: inline-block;
padding: 0.15rem 0.5rem;
margin-left: 0.35rem;
background: rgba(126, 224, 255, 0.15);
.search input::placeholder { color: var(--text-faintest); }
.search:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.search-status {
font: 400 11.5px/1 var(--mono);
color: var(--text-faintest);
white-space: nowrap;
}
/* ---- content ----------------------------------------------------------- */
.content { flex: 1; padding-top: 34px; padding-bottom: 56px; }
[hidden] { display: none !important; }
.card {
border: 1px solid var(--border);
border-radius: 10px;
font-size: 0.8rem;
border-radius: 9px;
overflow: hidden;
background: var(--surface);
}
.card-head {
display: flex;
align-items: baseline;
gap: 11px;
padding: 15px 20px;
border-bottom: 1px solid var(--border);
}
.card-head h2 { margin: 0; font: 600 15px/1 var(--sans); color: var(--text); }
.card-count { font: 400 12px/1 var(--mono); color: var(--text-faint); }
.col-head {
display: grid;
padding: 9px 20px;
background: var(--table-header);
border-bottom: 1px solid var(--border);
font: 500 9.5px/1 var(--mono);
letter-spacing: .11em;
color: var(--text-faint);
}
/* ---- state A: results -------------------------------------------------- */
.results .col-head, .result-row { grid-template-columns: 230px 1fr; }
.result-row { display: grid; border-bottom: 1px solid var(--border-faint); }
.result-row:last-child { border-bottom: 0; }
.result-meta { padding: 20px; border-right: 1px solid var(--border-faint); }
.result-id {
font: 600 14.5px/1.2 var(--mono);
color: var(--accent);
letter-spacing: -.01em;
}
.result-pocs { font: 500 11.5px/1 var(--mono); color: var(--text-dim); margin-top: 11px; }
.result-dates { display: flex; flex-direction: column; gap: 5px; margin-top: 14px; }
.result-date { display: flex; gap: 8px; font: 400 10.5px/1.4 var(--mono); }
.result-date dt { width: 62px; color: var(--text-faintest); margin: 0; }
.result-date dd { color: var(--prose); margin: 0; }
.mitre {
display: inline-block;
margin-top: 16px;
font: 500 10.5px/1 var(--mono);
color: var(--text-dim);
border: 1px solid var(--border-strong);
border-radius: 4px;
padding: 6px 9px;
}
.mitre:hover { color: var(--text); border-color: #3a5158; }
.result-body { padding: 20px 20px 22px; min-width: 0; }
.result-desc {
margin: 0;
font: 400 13px/1.6 var(--mono);
color: var(--prose);
max-height: 4.7em;
overflow: hidden;
}
.result-desc.is-open { max-height: none; }
.expander {
margin-top: 9px;
background: transparent;
border: 0;
padding: 0;
cursor: pointer;
font: 500 11px/1 var(--mono);
color: var(--accent);
}
nav {
display: flex;
align-items: center;
gap: 0.75rem;
}
nav a {
color: var(--text);
text-decoration: none;
padding: 0.5rem 0.8rem;
border-radius: 10px;
border: 1px solid transparent;
font-weight: 600;
letter-spacing: 0.01em;
transition: all 0.2s ease;
}
nav a:hover {
border-color: var(--border);
background: var(--panel);
color: #fff;
}
.expander:hover { color: var(--accent-hover); }
main { padding-top: 1.5rem; }
.hero {
display: grid;
gap: 1rem;
margin-top: 2rem;
padding: 1.25rem 1.25rem 1.5rem;
border-radius: 18px;
background: linear-gradient(150deg, rgba(122, 240, 193, 0.08), rgba(111, 179, 255, 0.08));
border: 1px solid var(--border-strong);
box-shadow: 0 24px 70px rgba(0,0,0,0.4);
position: relative;
.poc-list {
margin-top: 18px;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.hero::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 78% 18%, rgba(111,179,255,0.14), transparent 36%),
radial-gradient(circle at 14% 86%, rgba(122,240,193,0.15), transparent 34%);
pointer-events: none;
}
.hero > * { position: relative; z-index: 1; }
.hero-signal {
background: linear-gradient(135deg, rgba(8, 15, 32, 0.95) 0%, rgba(6, 12, 26, 0.9) 65%);
border: 1px solid var(--border);
}
.hero-meta { display: grid; gap: 0.25rem; }
.hero .eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.8rem;
color: var(--muted);
margin: 0;
}
.hero h1 {
margin: 0.2rem 0 0.2rem;
font-size: clamp(2.2rem, 4vw, 2.9rem);
letter-spacing: -0.03em;
color: #fff;
}
.hero .lede {
margin: 0 0 0.8rem;
color: var(--muted);
max-width: 820px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.75rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border);
color: var(--text);
font-size: 0.9rem;
}
.pill strong { color: #fff; }
.searchForm {
width: 100%;
}
.searchForm input[type="text"],
.searchForm input[type="search"] {
width: 100%;
padding: 1.1rem 1.25rem;
border-radius: 14px;
border: 1px solid var(--border-strong);
background: rgba(6, 10, 22, 0.9);
color: #fff;
font-size: 1.05rem;
font-family: 'IBM Plex Mono', monospace;
outline: none;
box-shadow: inset 0 0 0 1px rgba(126, 224, 255, 0.05), 0 16px 32px rgba(0,0,0,0.35);
}
.searchForm input::placeholder { color: #7c8aab; }
.search-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
color: var(--muted);
}
.search-results {
margin-top: 1rem;
border-radius: 14px;
border: 1px solid var(--border);
background: rgba(8, 12, 26, 0.8);
overflow: hidden;
}
.search-results .header {
padding: 0.75rem 1.1rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
.search-results .header h2 {
margin: 0;
font-size: 1rem;
letter-spacing: 0.02em;
color: #fff;
}
.search-results table.results {
width: 100%;
border-collapse: collapse;
table-layout: auto;
}
.search-results table.results thead td {
font-weight: 700;
color: var(--muted);
padding: 0.75rem 1rem;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.85rem;
}
.search-results table.results tbody tr {
border-top: 1px solid var(--border);
}
.search-results table.results td {
padding: 0.9rem 1rem;
vertical-align: top;
color: var(--text);
word-break: break-word;
overflow-wrap: anywhere;
}
.search-results table.results td.desc {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.95rem;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
.cveNum { font-weight: 700; font-size: 1.05rem; white-space: nowrap; }
.poc-container { margin-top: 0.4rem; }
.poc-container ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.3rem; }
.poc-container li a { word-break: break-word; overflow-wrap: anywhere; color: var(--accent); }
.dropdown-btn {
margin-top: 0.4rem;
background: transparent;
color: #fff;
border: 1px solid var(--border);
border-radius: 50px;
padding: 0.35rem 0.8rem;
cursor: pointer;
font-family: inherit;
}
.results-table.hide { display: none; }
.noResults {
padding: 0.8rem 1rem;
color: var(--muted);
font-weight: 600;
}
.card-grid {
.poc-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.card {
background: linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.03) 100%);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1rem;
box-shadow: 0 16px 40px rgba(0,0,0,0.4);
display: grid;
gap: 0.35rem;
}
.card-title { font-weight: 700; color: #fff; font-size: 1rem; }
.card-meta { color: var(--muted); font-size: 0.95rem; }
.card p { margin: 0; color: #c6d1ea; }
.badge {
display: inline-flex;
grid-template-columns: 1fr 84px 58px;
gap: 14px;
align-items: center;
padding: 0.25rem 0.55rem;
border-radius: 999px;
background: rgba(126, 224, 255, 0.12);
color: var(--text);
font-size: 0.85rem;
margin-right: 0.3rem;
border: 1px solid var(--border);
padding: 10px 14px;
border-bottom: 1px solid var(--border-hairline);
}
.muted { color: var(--muted); }
.section {
margin: 2rem 0;
}
.section h1, .section h2 {
margin: 0 0 0.25rem;
color: #fff;
letter-spacing: -0.01em;
}
.section p { margin: 0; color: var(--muted); }
.table-responsive {
margin-top: 1rem;
border-radius: 14px;
.poc-row:hover { background: var(--row-hover); }
.poc-name {
min-width: 0;
overflow: hidden;
border: 1px solid var(--border);
background: rgba(8, 12, 26, 0.9);
box-shadow: 0 16px 40px rgba(0,0,0,0.35);
text-overflow: ellipsis;
white-space: nowrap;
font: 400 12.5px/1 var(--mono);
}
.table-responsive table {
.poc-owner { color: var(--text-faint); }
.poc-sep { color: var(--separator); }
.poc-name a { font-weight: 500; }
.poc-stars { font: 400 12px/1 var(--mono); color: var(--text-faint); text-align: right; }
.poc-stars.is-popular { color: var(--star); }
.poc-age { font: 400 11.5px/1 var(--mono); color: var(--text-faintest); text-align: right; }
.poc-more {
display: block;
width: 100%;
border-collapse: collapse;
}
.table-responsive th,
.table-responsive td {
padding: 0.85rem 0.9rem;
border-bottom: 1px solid var(--border);
text-align: left;
background: transparent;
border: 0;
padding: 9px 14px;
cursor: pointer;
font: 500 11px/1 var(--mono);
color: var(--text-dim);
}
.poc-more:hover { color: var(--accent); }
/* ---- state B: nothing matched ------------------------------------------ */
.empty { padding: 52px 20px; text-align: center; }
.empty-head { font: 400 14px/1 var(--mono); color: var(--prose); }
.empty-head b { color: var(--text); font-weight: 400; }
.empty-hint { font: 400 13px/1.6 var(--sans); color: var(--text-faint); margin-top: 10px; }
.empty-hint code { font-family: var(--mono); color: var(--text-dim); }
/* ---- state C: trending ------------------------------------------------- */
.trend-head { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.trend-head h2 {
margin: 0;
font: 700 23px/1 var(--sans);
color: var(--text);
letter-spacing: -.02em;
}
.table-responsive th {
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
letter-spacing: 0.03em;
.toggle { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.toggle button {
border: 0;
border-right: 1px solid var(--border);
padding: 7px 13px;
cursor: pointer;
font: 500 11px/1 var(--mono);
background: transparent;
color: var(--text-dim);
}
.table-responsive tr:nth-child(odd) td { background: rgba(255, 255, 255, 0.02); }
.filter {
width: 100%;
padding: 0.75rem 0.9rem;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(7, 12, 24, 0.85);
color: #fff;
.toggle button:last-child { border-right: 0; }
.toggle button[aria-pressed="true"] { background: var(--accent); color: var(--surface-sunken); }
.trend-note { font: 400 12px/1 var(--mono); color: var(--text-faint); }
.trend-card { margin-top: 16px; }
.trend-card .col-head, .trend-row {
grid-template-columns: 80px 92px 300px 1fr;
gap: 18px;
}
.trend-row {
display: grid;
align-items: center;
padding: 11px 20px;
border-bottom: 1px solid var(--border-hairline);
}
.trend-row:last-child { border-bottom: 0; }
.trend-row:hover { background: var(--row-hover); }
.col-stars { text-align: right; }
.trend-stars {
font: 500 12.5px/1 var(--mono);
color: var(--text-faint);
text-align: right;
white-space: nowrap;
}
.trend-stars.is-popular { color: var(--star); }
.trend-age { font: 400 12px/1 var(--mono); color: var(--text-dim); white-space: nowrap; }
.trend-name {
font: 500 12.5px/1 var(--mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.trend-desc {
font: 400 12.5px/1.45 var(--sans);
color: var(--prose);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ---- footer ------------------------------------------------------------ */
.site-footer {
border-top: 1px solid var(--border);
padding: 1rem 0 1.5rem;
color: var(--muted);
text-align: center;
font-size: 0.9rem;
background: rgba(8, 12, 26, 0.8);
backdrop-filter: blur(10px);
padding: 16px 0;
background: var(--footer);
}
.site-footer a { color: var(--accent); }
.site-footer .wrap { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; align-items: center; }
.color-no-search { background: linear-gradient(145deg, #0b1021 0%, #0d172e 60%, #0a0f1e 100%); }
.color-no-results { background: radial-gradient(circle at 20% 20%, rgba(255,138,138,0.15), transparent 35%), var(--bg); }
.color-results-found { background: radial-gradient(circle at 15% 20%, rgba(110,231,183,0.12), transparent 32%), var(--bg); }
.color-too-many-results { background: radial-gradient(circle at 30% 10%, rgba(255,188,99,0.12), transparent 28%), var(--bg); }
.subtle-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.75rem;
}
.stat-row {
.site-footer .container {
display: flex;
gap: 0.75rem;
gap: 20px;
flex-wrap: wrap;
font: 400 11.5px/1.6 var(--mono);
color: var(--text-faintest);
}
.stat {
padding: 0.75rem 0.9rem;
border-radius: 12px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
min-width: 180px;
}
.stat strong { display: block; color: #fff; font-size: 1.2rem; }
.stat span { color: var(--muted); font-size: 0.9rem; }
.site-footer span:last-child { margin-left: auto; }
.detail-card {
margin-top: 0.4rem;
padding: 1rem 1.1rem;
border-radius: 14px;
border: 1px solid var(--border);
background: linear-gradient(160deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
box-shadow: 0 18px 40px rgba(0,0,0,0.35);
}
.detail-card p { margin: 0; color: var(--text); }
/* ---- narrow viewports -------------------------------------------------- */
.pill-row.tight { gap: 0.25rem; }
.topbar {
position: sticky;
top: 0;
z-index: 9;
background: rgba(8, 12, 26, 0.92);
border-bottom: 1px solid var(--border);
backdrop-filter: blur(12px);
}
.topbar .wrap {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.9rem 0.5rem;
}
.topbar nav { display: flex; gap: 0.6rem; }
.topbar nav a {
color: var(--text);
padding: 0.45rem 0.7rem;
border-radius: 10px;
border: 1px solid transparent;
transition: all 0.2s ease;
}
.topbar nav a:hover { border-color: var(--border); background: var(--panel); color: #fff; }
.dot { color: var(--accent); margin-right: 0.35rem; }
.small { font-size: 0.9rem; }
.footer { padding: 1.2rem 0; border-top: 1px solid var(--border); color: var(--muted); background: rgba(8,12,26,0.85); }
.footer-inner { display: flex; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.section-header { display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem; }
.section-header h1, .section-header h2 { margin: 0; }
.input {
width: 100%;
padding: 0.8rem 1rem;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(7, 12, 24, 0.85);
color: #fff;
}
.table-wrap {
margin-top: 1rem;
border-radius: 14px;
border: 1px solid var(--border);
background: rgba(8,12,26,0.9);
overflow: hidden;
box-shadow: 0 16px 40px rgba(0,0,0,0.35);
}
.table-wrap table { width: 100%; border-collapse: collapse; }
.table-wrap th, .table-wrap td {
padding: 0.85rem 0.9rem;
border-bottom: 1px solid var(--border);
text-align: left;
}
.table-wrap th { background: rgba(255,255,255,0.03); color: var(--muted); }
.cve-cell a { white-space: nowrap; font-variant-numeric: tabular-nums; }
.mono { font-family: 'IBM Plex Mono', monospace; color: #c7d2ea; font-size: 0.95rem; }
@media screen and (max-width: 720px) {
nav { gap: 0.35rem; }
nav a { padding: 0.45rem 0.55rem; font-size: 0.95rem; }
.hero { padding: 1.15rem; }
.search-results table.results td { word-break: break-word; }
@media (max-width: 900px) {
.container { padding: 0 20px; }
.wordmark { font-size: 34px; }
.results .col-head { display: none; }
.result-row { grid-template-columns: 1fr; }
.result-meta { border-right: 0; border-bottom: 1px solid var(--border-faint); }
.trend-card .col-head, .trend-row { grid-template-columns: 70px 88px 1fr; }
.col-desc, .trend-desc { display: none; }
}
ul { list-style: none; padding: 0; margin: 0; }
@media (max-width: 620px) {
.trend-card .col-head, .trend-row { grid-template-columns: 70px 1fr; }
.col-updated, .trend-age { display: none; }
}