From 066ddf4400923cec4b0d4d4b0115ad912fdaa333 Mon Sep 17 00:00:00 2001 From: 0xMarcio Date: Sun, 30 Aug 2026 21:53:31 +0200 Subject: [PATCH] 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. --- .gitignore | 1 + docs/index.html | 81 ++--- docs/logic.js | 812 ++++++++++++++++++++++++++---------------------- docs/style.css | 673 +++++++++++++++++---------------------- 4 files changed, 775 insertions(+), 792 deletions(-) diff --git a/.gitignore b/.gitignore index 8d15f2d4004..a68746181ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ data/ .venv/ docs/CVE_list.json +docs/repo_meta.json docs/trending_poc.json docs/api/v1/cve/ __pycache__/ diff --git a/docs/index.html b/docs/index.html index 43879e86c68..642cb5eccc8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,53 +4,64 @@ CVE PoC Hub + + + + - -
-
-

CVE PoC Hub

-
+ +
+
+

CVE PoC Hub

+
Linked PoCs MITRE CVE pages Vendor / product filters - Supports negative terms (e.g. -windows) + Supports negative terms (e.g. -windows)
-
- + + + +
- -
+ + -
-

Recently updated Proof-of-Concepts

-
- - - - - -
StarsUpdatedNameDescription
No recent PoCs.
+
+ + +
+
+

Trending Proof-of-Concepts

+
+ + +
+ +
+
+
+ STARS + UPDATED + NAME + DESCRIPTION +
+
+ +
+
+ sources: nvd · mitre · github + +
+
+ diff --git a/docs/logic.js b/docs/logic.js index c17a6da735b..e7049e3b836 100644 --- a/docs/logic.js +++ b/docs/logic.js @@ -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 => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' + }[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 => ({ - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }[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 `
  • ${safe}
  • `; -} - -function convertLinksToList(links) { - if (links.length === 0) { - return ''; - } - let htmlOutput = `
      `; - const displayLimit = 5; - links.slice(0, displayLimit).forEach(link => { - htmlOutput += linkItem(link); - }); - htmlOutput += `
    `; - if (links.length > displayLimit) { - htmlOutput += ` - - `; - } - htmlOutput += `
    `; - 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 `${cveId}`; -} - -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 => - '' + getCveLink(r.cve) + - '' + escapeHTML(r.desc || '') + ' ' + - convertLinksToList(r.poc || []) + '' - ).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 ``; + } + 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 `
    ` + + `${escapeHTML(parsed.owner)}` + + ` / ` + + `${escapeHTML(parsed.repo)}` + + `${stars == null ? '' : formatStars(stars) + ' ★'}` + + `${escapeHTML(shortAge(hours))}
    `; +} -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 + ? `
    ${dates.map(([label, value]) => + `
    ${label}
    ${escapeHTML(value)}
    `).join('')}
    ` + : ''; - if (!results || !searchValue || !form || !resultsTableHideable || !resultsTable || !noResults) { - console.warn('Search container missing expected elements'); - return; - } + return `
    +
    + ${escapeHTML(id)} +
    ${formatCount(links.length)} linked PoC${links.length === 1 ? '' : 's'}
    + ${dateHtml} + MITRE ↗ +
    +
    +

    ${escapeHTML(entry.desc || '')}

    + +
    + ${visible.map(pocRow).join('')} + +
    +
    +
    `; +} - 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 = `
    +
    No CVE matched ${escapeHTML(state.query)}
    +
    Try a CVE ID, a vendor, or a product name. Prefix a term with - to exclude it.
    +
    `; + return; + } - function renderTrending(items) { - if (!trendingBody) return; - if (!items || items.length === 0) { - trendingBody.innerHTML = 'No recent PoCs.'; - 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 `${stars}⭐${updated}${name}${desc}`; - }).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 + ? `` + : ''; - 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 = `
    +

    Results

    + ${formatCount(results.length)} CVE${results.length === 1 ? '' : 's'} · ${formatCount(pocTotal)} PoCs +
    +
    CVEDESCRIPTION / POC LINKS
    + ${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 `
    + ${formatStars(item.stars)} ★ + ${escapeHTML(longAge(item._pushed))} + ${escapeHTML(item.name)} + ${escapeHTML(item.desc || '')} +
    `; +} - 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('') + : '
    No recent PoCs.
    '; - 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 = `
    +
    Loading the CVE index…
    +
    The full index is a single download; results appear as soon as it lands.
    +
    `; + 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'; + } +})(); diff --git a/docs/style.css b/docs/style.css index 9a37e6ca389..83aeba02c56 100644 --- a/docs/style.css +++ b/docs/style.css @@ -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; } +}