diff --git a/README.md b/README.md index e724c34cb..6f272be6b 100644 --- a/README.md +++ b/README.md @@ -479,7 +479,7 @@ the `--check` mode. ## Website -The public site at is an Astro +The public site at is an Astro Starlight package in `docs/`; every number it shows is rendered from the generated `results/results.json`, never hand-authored. Develop locally with: diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 4edf905c2..a184e4dac 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -4,6 +4,7 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import { snapshots } from './src/data/snapshots'; import { remarkHeadingIds } from './src/plugins/remark-heading-ids.mjs'; +import { rehypeTableScroll } from './src/plugins/rehype-table-scroll.mjs'; // --------------------------------------------------------------------------- // Snapshot navigation. @@ -58,12 +59,21 @@ const previousLabel = previousSnapshot : ''; // Production deployment target for GitHub Pages. +const site = 'https://dataflowbench.slopcop.com'; +const base = '/'; +/** The published root, for the absolute URLs social cards need. */ +const siteRoot = new URL(base.replace(/\/?$/, '/'), site); +const socialCard = new URL('og-image.png', siteRoot).href; + export default defineConfig({ - site: 'https://dataflowbench.brokk.ai', + site, + base, markdown: { // `## Heading {#legacy-id}` — explicit anchors for headings whose ids are // citable and must not follow the auto-slugger. See the plugin's comment. remarkPlugins: [remarkHeadingIds], + // Each table in a scroll box, so a wide one pans instead of overflowing. + rehypePlugins: [rehypeTableScroll], }, redirects: { // Explicit current-snapshot pointer alongside versioned snapshot URLs. @@ -75,7 +85,44 @@ export default defineConfig({ favicon: '/favicon.svg', description: 'Analyzer-neutral benchmark for data-flow analysis, published exclusively from immutable freeze evidence.', - customCss: ['./src/styles/custom.css'], + // The shared foundation first (identical in the UsageBench repository), + // then this site's accent, palette and layout on top of it. + customCss: ['./src/styles/foundation.css', './src/styles/custom.css'], + logo: { src: './src/assets/mark.svg', alt: '' }, + head: [ + { tag: 'meta', attrs: { property: 'og:image', content: socialCard } }, + { tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } }, + { tag: 'meta', attrs: { property: 'og:image:height', content: '630' } }, + { + tag: 'meta', + attrs: { + property: 'og:image:alt', + content: 'DataFlowBench: how well do static analyzers follow data flow?', + }, + }, + { tag: 'meta', attrs: { name: 'twitter:image', content: socialCard } }, + { + tag: 'script', + attrs: { type: 'application/ld+json' }, + content: JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'DataFlowBench', + url: siteRoot.href, + publisher: { '@type': 'Organization', name: 'SlopCop', url: 'https://slopcop.com' }, + }), + }, + ], + // Archived snapshot pages get their release named in their search + // description, so no two pages share one. See src/routeData.ts. + routeMiddleware: './src/routeData.ts', + expressiveCode: { + styleOverrides: { + borderRadius: '0px', + codeFontFamily: "'JetBrains Mono', ui-monospace, monospace", + uiFontFamily: "'Apfel Grotezk', ui-sans-serif, system-ui, sans-serif", + }, + }, components: { // Starlight's own footer, with the floating "back to top" control // appended. The footer is the one slot that renders exactly once on diff --git a/docs/public/CNAME b/docs/public/CNAME index af10f63da..2b2629ea3 100644 --- a/docs/public/CNAME +++ b/docs/public/CNAME @@ -1 +1 @@ -dataflowbench.brokk.ai +dataflowbench.slopcop.com diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg index 182f96962..f45ab4a4c 100644 --- a/docs/public/favicon.svg +++ b/docs/public/favicon.svg @@ -1,14 +1,15 @@ - - + + - - - + + + - + diff --git a/docs/public/og-image.png b/docs/public/og-image.png new file mode 100644 index 000000000..143d8b8a0 Binary files /dev/null and b/docs/public/og-image.png differ diff --git a/docs/src/assets/mark.svg b/docs/src/assets/mark.svg new file mode 100644 index 000000000..f45ab4a4c --- /dev/null +++ b/docs/src/assets/mark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/docs/src/components/AnalyzerLandscape.astro b/docs/src/components/AnalyzerLandscape.astro index 4c0f44bd4..93954b3df 100644 --- a/docs/src/components/AnalyzerLandscape.astro +++ b/docs/src/components/AnalyzerLandscape.astro @@ -12,6 +12,7 @@ import { vendorOrder, } from '../data/snapshots'; import { analyzerLanguageSupport, type AnalyzerLanguageSupport } from '../data/analyzer-language-support'; +import { percentAxis, placeLabels } from '../data/scatter-layout'; import ScatterTooltip from './ScatterTooltip.astro'; interface AnalyzerPoint { @@ -67,28 +68,28 @@ const formatPercent = (value: number) => `${value.toFixed(1)}%`; const width = 820; const height = 470; -const padLeft = 66; -const padRight = 30; -const padTop = 34; -const padBottom = 62; +const padLeft = 60; +const padRight = 24; +const padTop = 28; +const padBottom = 56; const plotWidth = width - padLeft - padRight; const plotHeight = height - padTop - padBottom; const languageAxisMax = 14; +// The vertical axis starts at the tens below the lowest analyzer rather than +// at 0, so the plot spends its height on the range the analyzers occupy. +const axis = percentAxis(points.map((point) => percent(point.correct, point.covered))); const x = (languages: number) => padLeft + languages * plotWidth / languageAxisMax; -const y = (correct: number, covered: number) => padTop + (100 - percent(correct, covered)) * plotHeight / 100; +const y = (value: number) => padTop + (axis.max - value) * plotHeight / (axis.max - axis.min); const xTicks = [0, 2, 4, 6, 8, 10, 12, 14]; -const yTicks = [50, 60, 70, 80, 90, 100]; -// Small, stable offsets keep labels readable without changing point geometry. -const labelOffsets: Record = { - bifrost: { x: -9, y: -11, anchor: 'end' }, - codeql: { x: -9, y: 17, anchor: 'end' }, - joern: { x: -9, y: -11, anchor: 'end' }, - semgrep: { x: -9, y: 17, anchor: 'end' }, - opentaint: { x: -10, y: -11, anchor: 'end' }, - infer: { x: 9, y: -11 }, - flowdroid: { x: 9, y: 17 }, - pysa: { x: 9, y: 17 }, -}; +const plotted = points.map((point) => ({ + ...point, + px: x(point.languageSupport.languages.length), + py: y(percent(point.correct, point.covered)), +})); +const labels = placeLabels( + plotted.map((point) => ({ x: point.px, y: point.py, text: point.name })), + { bounds: { left: padLeft + 4, top: 4, right: width - 4, bottom: height - padBottom - 4 }, radius: 8, fontSize: 13, charWidth: 0.58 }, +); ---

Accuracy and language coverage — current snapshot

@@ -108,33 +109,26 @@ const labelOffsets: Record Current analyzer accuracy by supported data-flow languages Scatter plot of all analyzers in DataFlowBench {currentSnapshot.version}. The horizontal axis is the documented number of languages in which each analyzer performs data-flow analysis. The vertical axis is decisive-correct assertions divided by every assertion in the benchmark kernels that analyzer covers, including non-answers in the denominator. - {yTicks.map((tick) => { - const tickY = padTop + (100 - tick) * plotHeight / 100; - return <> - - {tick}% - ; - })} - {xTicks.map((tick) => { - const tickX = x(tick); - return <> - - {tick} - ; - })} + {axis.ticks.map((tick) => <> + + {tick}% + )} + {xTicks.map((tick) => <> + + {tick} + )} - languages with documented data-flow support - accuracy within covered kernels - {points.map((point) => { - const pointX = x(point.languageSupport.languages.length); - const pointY = y(point.correct, point.covered); - const offset = labelOffsets[point.tool] ?? { x: 9, y: -9 }; + Languages with documented data-flow support + Accuracy within covered kernels + {plotted.map((point, index) => { + const label = labels[index]; const accuracy = percent(point.correct, point.covered); const tooltip = `${point.name} — x: ${point.languageSupport.languages.length} supported languages · y: ${formatPercent(accuracy)} accuracy`; return - - {point.name} + {label.leader && } + + {point.name} ; })} @@ -148,15 +142,15 @@ const labelOffsets: Record Exact figures -
+
{points.map((point) => - - + + )}
AnalyzerScopeSupported languagesBenchmark kernelsAccuracyCorrectWrongIncomplete
{point.name} {point.cohort} {point.languageSupport.languages.length}{point.languageSupport.languages.join(', ')}{point.kernels}/{kernelCount} ({formatPercent(percent(point.kernels, kernelCount))}){point.correct}/{point.covered} ({formatPercent(percent(point.correct, point.covered))}){point.kernels}/{kernelCount} ({formatPercent(percent(point.kernels, kernelCount))}){point.correct}/{point.covered} ({formatPercent(percent(point.correct, point.covered))}) {point.correct}{point.wrong}{point.incomplete}
@@ -164,26 +158,32 @@ const labelOffsets: Record diff --git a/docs/src/components/AnalyzerProfiles.astro b/docs/src/components/AnalyzerProfiles.astro index a51a64129..e44a47224 100644 --- a/docs/src/components/AnalyzerProfiles.astro +++ b/docs/src/components/AnalyzerProfiles.astro @@ -300,17 +300,19 @@ const cards = model.profiles.map((profile) => { } .profile-card { margin: 0; - border: 1px solid var(--sl-color-hairline); - border-radius: 0.75rem; - padding: 0.75rem 0.75rem 0.6rem; - background: var(--sl-color-bg-sidebar); + border: 1px solid var(--ds-line); + border-radius: var(--ds-cut-control); + corner-shape: bevel; + padding: 0.95rem 1rem 0.85rem; + background: var(--ds-surface); } .profile-name { display: flex; align-items: center; - font-weight: 600; + font-weight: 500; font-size: var(--sl-text-base); - color: var(--sl-color-white); + letter-spacing: -0.01em; + color: var(--ds-text); margin: 0 0 0.25rem; } .vendor-dot { @@ -318,7 +320,7 @@ const cards = model.profiles.map((profile) => { width: 0.55rem; height: 0.55rem; border-radius: 50%; - margin-right: 0.4rem; + margin-right: 0.45rem; background: var(--vendor-rule, var(--sl-color-accent)); } .profile-radar { @@ -328,11 +330,11 @@ const cards = model.profiles.map((profile) => { } .profile-radar .ring { fill: none; - stroke: var(--sl-color-hairline); + stroke: var(--ds-line); stroke-width: 1; } .profile-radar .spoke { - stroke: var(--sl-color-hairline); + stroke: var(--ds-line-soft); stroke-width: 1; } .profile-radar .shape { @@ -346,9 +348,10 @@ const cards = model.profiles.map((profile) => { fill: var(--vendor-rule, var(--sl-color-accent)); } .profile-radar .axis-label { - font-family: var(--__sl-font-mono); - font-size: 11px; - fill: var(--sl-color-gray-3); + font-family: var(--ds-font); + font-size: 11.5px; + font-weight: 500; + fill: var(--ds-text-3); } .readings { display: grid; @@ -358,26 +361,25 @@ const cards = model.profiles.map((profile) => { font-size: var(--sl-text-xs); } .readings dt { - font-family: var(--__sl-font-mono); - color: var(--sl-color-gray-3); + color: var(--ds-text-3); } .readings dd { margin: 0; display: flex; flex-direction: column; line-height: 1.3; - color: var(--sl-color-gray-2); + color: var(--ds-text-2); } .readings .basis { - color: var(--sl-color-gray-3); + color: var(--ds-text-3); } .axis-notes { font-size: var(--sl-text-sm); - color: var(--sl-color-gray-3); + color: var(--ds-text-3); } .caption { font-size: var(--sl-text-sm); - color: var(--sl-color-gray-3); + color: var(--ds-text-3); } /* The same fixed vendor→colour identity map every other section uses. */ .v0 { diff --git a/docs/src/components/BackToTop.astro b/docs/src/components/BackToTop.astro index 431758f54..84b88bfa6 100644 --- a/docs/src/components/BackToTop.astro +++ b/docs/src/components/BackToTop.astro @@ -68,13 +68,14 @@ justify-content: center; width: 2.5rem; height: 2.5rem; - border: 1px solid var(--sl-color-gray-5); - border-radius: 999px; + border: 1px solid var(--ds-line-strong); + border-radius: var(--ds-cut-chip); + corner-shape: bevel; /* The nav's own background: this floats over the page, so it should read as chrome rather than as another card. */ background: var(--sl-color-bg-nav); - color: var(--sl-color-gray-2); - box-shadow: var(--sl-shadow-md); + color: var(--ds-text-2); + box-shadow: var(--ds-shadow-float); /* Hidden until the reader has scrolled. `visibility` rather than opacity alone so the anchor leaves the tab order while it is not on offer. */ @@ -94,12 +95,12 @@ } .to-top:hover { - color: var(--sl-color-white); - border-color: var(--sl-color-accent); + color: var(--ds-text); + border-color: var(--ds-accent); } .to-top:focus-visible { - outline: 2px solid var(--sl-color-accent-high); + outline: 2px solid var(--ds-accent); outline-offset: 2px; } diff --git a/docs/src/components/CaseEvidence.astro b/docs/src/components/CaseEvidence.astro index 1103b9fdb..e8d157784 100644 --- a/docs/src/components/CaseEvidence.astro +++ b/docs/src/components/CaseEvidence.astro @@ -14,7 +14,7 @@ interface Props { const { snapshot, tier } = Astro.props; --- -
+
@@ -30,17 +30,17 @@ const { snapshot, tier } = Astro.props; { tier.cases.map((result) => ( - - - +
+ {result.template_id.replace('dfb-template-', '')} + {result.case_id} {result.polarity} {result.outcome} {result.classification}{result.classification} {shortDigest(result.raw_evidence.sha256)}… @@ -57,4 +57,16 @@ const { snapshot, tier } = Astro.props; .table-scroll { overflow-x: auto; } + /* Template and case ids wrap at their hyphens, two or three pieces to a + line; below that the table pans instead of stacking one piece per line. + A classification such as true-negative is one term. */ + .template { + min-width: 9rem; + } + .case { + min-width: 11rem; + } + .classification { + white-space: nowrap; + } diff --git a/docs/src/components/DecisionLandscape.astro b/docs/src/components/DecisionLandscape.astro index cf5467bed..aed02e409 100644 --- a/docs/src/components/DecisionLandscape.astro +++ b/docs/src/components/DecisionLandscape.astro @@ -11,6 +11,7 @@ import { vendorName, vendorOrder, } from '../data/snapshots'; +import { percentAxis, placeLabels, type LabelPlacement, type PercentAxis } from '../data/scatter-layout'; import ScatterTooltip from './ScatterTooltip.astro'; interface Point { @@ -30,6 +31,13 @@ interface Panel { points: Point[]; } +interface PlottedPoint extends Point { + decided: number; + px: number; + py: number; + label: LabelPlacement; +} + const populations = coreKernelPopulations(currentSnapshot.results); const totalAssertions = populations.reduce((sum, population) => sum + population.cases, 0); @@ -73,36 +81,49 @@ const panels: Panel[] = [ const width = 820; const height = 470; -const padLeft = 66; -const padRight = 30; -const padTop = 34; -const padBottom = 62; +const padLeft = 60; +const padRight = 24; +const padTop = 28; +const padBottom = 56; const plotWidth = width - padLeft - padRight; const plotHeight = height - padTop - padBottom; const percent = (part: number, whole: number) => whole === 0 ? 0 : (100 * part) / whole; const formatPercent = (value: number) => `${value.toFixed(1)}%`; -const x = (decided: number, denominator: number) => padLeft + percent(decided, denominator) * plotWidth / 100; -const y = (correct: number, decided: number) => padTop + (100 - percent(correct, decided)) * plotHeight / 100; const xTicks = [0, 25, 50, 75, 100]; -const yTicks = [50, 60, 70, 80, 90, 100]; -const labelOffsets: Record = { - bifrost: { x: -9, y: -11, anchor: 'end' }, - codeql: { x: -9, y: 17, anchor: 'end' }, - joern: { x: -9, y: -11, anchor: 'end' }, - semgrep: { x: 9, y: 17 }, - opentaint: { x: -10, y: -11, anchor: 'end' }, - infer: { x: 9, y: -11 }, - flowdroid: { x: 9, y: 17 }, - pysa: { x: -9, y: 17, anchor: 'end' }, -}; +// Blind answering — the same verdict on both halves of every pair — is right +// exactly half the time, so 50% correctness is the floor worth marking. +const baseline = 50; + +// Each view gets a vertical axis fitted to its own points, trimmed from 0 to +// the tens below the lowest one but always keeping the baseline clear of the +// floor: most views share the same 40–100% axis, and one that dips below it +// says so on its ticks. +// Labels are placed per view, since which points crowd depends on the view. +const layouts = panels.map((panel) => { + const axis: PercentAxis = percentAxis( + panel.points.map((point) => percent(point.correct, point.correct + point.wrong)), + baseline - 10, + ); + const x = (value: number) => padLeft + value * plotWidth / 100; + const y = (value: number) => padTop + (axis.max - value) * plotHeight / (axis.max - axis.min); + const located = panel.points.map((point) => { + const decided = point.correct + point.wrong; + return { ...point, decided, px: x(percent(decided, panel.denominator)), py: y(percent(point.correct, decided)) }; + }); + const labels = placeLabels( + located.map((point) => ({ x: point.px, y: point.py, text: point.name })), + { bounds: { left: padLeft + 4, top: 4, right: width - 4, bottom: height - padBottom - 4 }, radius: 8, fontSize: 13, charWidth: 0.58 }, + ); + const points: PlottedPoint[] = located.map((point, index) => ({ ...point, label: labels[index] })); + return { panel, axis, x, y, points }; +}); const toggleCss = panels.map((panel) => ` #decision-view-${panel.id}:checked ~ .decision-ribbon label[for='decision-view-${panel.id}'] { - background: var(--sl-color-accent-low); color: var(--sl-color-white); - box-shadow: inset 0 0 0 1px var(--sl-color-accent); + border-color: var(--ds-accent); background: var(--ds-accent-low); color: var(--ds-text); } #decision-view-${panel.id}:focus-visible ~ .decision-ribbon label[for='decision-view-${panel.id}'] { - outline: 2px solid var(--sl-color-accent-high); outline-offset: 2px; + outline: 2px solid var(--ds-accent); outline-offset: 2px; } #decision-view-${panel.id}:checked ~ .decision-panels > #decision-panel-${panel.id} { display: block; } `).join('\n'); @@ -111,35 +132,34 @@ const toggleCss = panels.map((panel) => `
{panels.map((panel, index) => )}
- {panels.map((panel) => )} + {panels.map((panel, index) => )}
- {panels.map((panel) =>
+ {layouts.map(({ panel, axis, x, y, points }) =>
{panel.label} analyzer decisiveness and correctness - Scatter plot for {panel.label}. The horizontal axis is decisive answers divided by {panel.denominator} assertions. The vertical axis is correct answers divided by decisive answers. - {yTicks.map((tick) => { - const tickY = padTop + (100 - tick) * plotHeight / 100; - return <>{tick}%; - })} - {xTicks.map((tick) => { - const tickX = padLeft + tick * plotWidth / 100; - return <>{tick}%; - })} + Scatter plot for {panel.label}. The horizontal axis is decisive answers divided by {panel.denominator} assertions. The vertical axis is correct answers divided by decisive answers, from {axis.min}% to 100%; a line marks the {baseline}% a blind answerer would score. + {axis.ticks.map((tick) => <> + + {tick}% + )} + {xTicks.map((tick) => <> + + {tick}% + )} + + blind baseline - decisiveness — share of {panel.denominator} assertions answered - correctness among decisive answers - {panel.points.map((point) => { - const decided = point.correct + point.wrong; - const pointX = x(decided, panel.denominator); - const pointY = y(point.correct, decided); - const offset = labelOffsets[point.tool] ?? { x: 9, y: -9 }; - const tooltip = `${point.name} — x: ${formatPercent(percent(decided, panel.denominator))} decisiveness (${decided}/${panel.denominator}) · y: ${formatPercent(percent(point.correct, decided))} correctness (${point.correct}/${decided})`; + Decisiveness: share of {panel.denominator} assertions answered + Correctness among decisive answers + {points.map((point) => { + const tooltip = `${point.name} — x: ${formatPercent(percent(point.decided, panel.denominator))} decisiveness (${point.decided}/${panel.denominator}) · y: ${formatPercent(percent(point.correct, point.decided))} correctness (${point.correct}/${point.decided})`; return - - {point.name} + {point.label.leader && } + + {point.name} ; })} @@ -152,16 +172,16 @@ const toggleCss = panels.map((panel) => `

Exact figures -
+
{panel.kernelId && }{panel.points.map((point) => { const decided = point.correct + point.wrong; return - - + + - {panel.kernelId && } + {panel.kernelId && } ; })}
AnalyzerDecisivenessCorrectness when decisiveUnansweredEvidence
{point.name}{decided}/{panel.denominator} ({formatPercent(percent(decided, panel.denominator))}){point.correct}/{decided} ({formatPercent(percent(point.correct, decided))}){decided}/{panel.denominator} ({formatPercent(percent(decided, panel.denominator))}){point.correct}/{decided} ({formatPercent(percent(point.correct, decided))}) {panel.denominator - decided}
@@ -172,30 +192,38 @@ const toggleCss = panels.map((panel) => ` diff --git a/docs/src/components/DimensionRates.astro b/docs/src/components/DimensionRates.astro index ad96ffb89..cb254563c 100644 --- a/docs/src/components/DimensionRates.astro +++ b/docs/src/components/DimensionRates.astro @@ -18,7 +18,7 @@ function fraction(rate: RateFraction | null): string { } --- -
+
@@ -50,10 +50,10 @@ function fraction(rate: RateFraction | null): string { - - - - + + + + )) } @@ -65,4 +65,8 @@ function fraction(rate: RateFraction | null): string { .table-scroll { overflow-x: auto; } + /* A rate reads as one figure; its header wraps over it instead. */ + .rate { + white-space: nowrap; + } diff --git a/docs/src/components/LandingResults.astro b/docs/src/components/LandingResults.astro index df33f336f..9a8f8bb3b 100644 --- a/docs/src/components/LandingResults.astro +++ b/docs/src/components/LandingResults.astro @@ -469,54 +469,25 @@ const nativeDecidingAdapters = [ const snapshotPage = `/snapshots/${snapshot.slug}`; --- -

- Immutable evaluation · DataFlowBench {snapshot.version} -

- -{/* The subtitle the page title used to be. The h1 names the benchmark, which - is what a reader arriving from a citation is looking for; the motivation - belongs directly under it, stated rather than implied. */} -

- BrokkAi's benchmark for semantic data flow: Bifrost measured against {analyzerCount - 1} reference analyzers, from immutable - freeze evidence. -

- -{/* Starlight's "On this page" nav is built from markdown headings and cannot - see the h2s this component emits, so the landing page carries its own - section strip. The ids it targets are permanent deep-link anchors: chosen - once, never renamed. */} - - -{/* What the benchmark is, ahead of what this particular freeze found. These - two describe the dataset and the conflict of interest behind it, so they - belong above the verdict rather than inside it — a reader meeting the page - for the first time needs them before the numbers mean anything. Wrapped - because they are adjacent: two paragraphs each holding their own pair of - columns read in the wrong order (see `.prose-columns` in custom.css). */} -
-

- DataFlowBench is BrokkAi's own - benchmark: we build it to measure where Bifrost stands against other analyzers, and to keep ourselves honest while doing - it. The methodology is deliberately analyzer-neutral — every number below is - generated from digest-bound freeze evidence that we cannot edit after the - fact, misses and crashes included — but the motivation is not neutral, and - we would rather say so. +{/* The verdict answers the page title, so it follows it directly, across the + full column above the two paragraphs that frame it: how the kernels are + read, and who builds the benchmark and why. A reader meeting the page for + the first time needs both before the numbers mean anything. */} +

+ Nobody is perfect. No analyzer answers a whole core correctly in any of the + {kernels.length} languages{spotless.length === 0 ? ( + <>, and every one of the {analyzerCount} in the field makes at least one + decisive mistake. + ) : ( + <>, and of the {analyzerCount} in the field only{' '} + {spotless.map((tool) => tool.name).join(' and ')} makes no decisive + mistake at all — bought by declining{' '} + {spotless.reduce((sum, tool) => sum + tool.incomplete, 0)} of the{' '} + {coreAssertions} core assertions rather than guessing at them. + )}

+

Each kernel poses one positive and one negative assertion per semantic template, and each language's kernel is its own population — cores of @@ -529,24 +500,39 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; dialog. A blank analyzer on a kernel means no report in this freeze: no extractor, no frontend, or no adapter, which is coverage rather than a score.

+

+ DataFlowBench is SlopCop's own + benchmark for semantic data flow: we build it to measure where Bifrost stands against {analyzerCount - 1} reference analyzers, and to keep + ourselves honest while doing it. The methodology is deliberately + analyzer-neutral — every number here is generated from digest-bound freeze + evidence that we cannot edit after the fact, misses and crashes included — + but the motivation is not neutral, and we would rather say so. +

- +
+
+
Frozen case results, across three populations
+
{caseCount.toLocaleString('en-US')}
+
+
+
Kernel languages, {templateSizes} templates each
+
{kernels.length}
+
+
+
Languages in the direct-flow breadth baseline
+
{breadth.length}
+
+
+
Immutable evidence release
+
{snapshot.version}
+
+
+
+

How decisive, and how correct

@@ -873,25 +878,6 @@ const snapshotPage = `/snapshots/${snapshot.slug}`;

-
-
-

{caseCount}

-

frozen case results, across three populations

-
-
-

{breadth.length}

-

languages in the breadth baseline

-
-
-

{kernels.length}

-

kernel languages ({templateSizes} templates each)

-
-
-

{snapshot.version}

-

immutable evidence release

-
-
-

Modeling matrix — is the model surface load-bearing?

@@ -915,16 +901,12 @@ const snapshotPage = `/snapshots/${snapshot.slug}`;

- {card.name} {card.toolVersion} + {card.name} + {card.toolVersion}

{card.rows.map((row) => (
-

- {row.language} - - {row.scored} scored · {row.unsupported} declined of {row.total} - -

+ {row.language} -

- {tierVerdict(row)} - {row.inconclusive > 0 ? ` · ${row.inconclusive} inconclusive` : ''} +

+ + {tierVerdict(row)} + {row.inconclusive > 0 ? ` · ${row.inconclusive} inconclusive` : ''} + + + {row.scored} scored · {row.unsupported} declined of {row.total} +

))} @@ -996,16 +983,12 @@ const snapshotPage = `/snapshots/${snapshot.slug}`;

- {card.name} {card.toolVersion} + {card.name} + {card.toolVersion}

{card.rows.map((row) => (
-

- {row.language} - - {row.scored} scored · {row.unsupported} declined of {row.total} - -

+ {row.language} -

- {tierVerdict(row)} - {row.falsePositives > 0 ? ` · ${row.falsePositives} FP` : ''} - {row.falseNegatives > 0 ? ` · ${row.falseNegatives} FN` : ''} +

+ + {tierVerdict(row)} + {row.falsePositives > 0 ? ` · ${row.falsePositives} FP` : ''} + {row.falseNegatives > 0 ? ` · ${row.falseNegatives} FN` : ''} + + + {row.scored} scored · {row.unsupported} declined of {row.total} +

))} @@ -1100,10 +1088,20 @@ const snapshotPage = `/snapshots/${snapshot.slug}`;
{kernels.map((kernel) => (
-

- {kernel.language} - {kernel.cases} assertions -

+
+

+ {kernel.language} + {kernel.cases} assertions +

+ +
{kernel.vendors.map((vendor, index) => (

@@ -1128,21 +1126,11 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; class="seg declined" style={`width:${barWidth(vendor.unsupported, vendor.total)}`}>

-

+

{vendor.correct}/{vendor.total}

))} -

- -

))}
@@ -1242,11 +1230,13 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; width: min(64rem, calc(100vw - 2rem)); max-height: calc(100vh - 4rem); overflow-y: auto; - border: 1px solid var(--sl-color-accent); - border-radius: 0.75rem; - padding: 1.25rem 1.5rem 1.5rem; - background: var(--sl-color-bg-sidebar); + border: 1px solid var(--ds-line-strong); + border-radius: var(--ds-cut-panel); + corner-shape: bevel; + padding: 1.5rem 1.75rem 1.75rem; + background: var(--ds-surface); color: var(--sl-color-text); + box-shadow: var(--ds-shadow-panel); } /* Starlight's content flow adds margin-top to every sibling block; the dialog lays out its own spacing. */ @@ -1254,7 +1244,7 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; margin-top: 0; } #kernel-dialog::backdrop { - background: rgb(0 0 0 / 0.6); + background: var(--sl-color-backdrop-overlay); } #kernel-dialog .dialog-head { display: flex; @@ -1263,39 +1253,48 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; gap: 1rem; } #kernel-dialog .label { - font-family: var(--__sl-font-mono); font-size: var(--sl-text-xs); - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--sl-color-accent-high); + font-weight: 500; + color: var(--ds-text-3); margin: 0; } #kernel-dialog h2 { font-size: var(--sl-text-2xl); - line-height: 1.2; - margin: 0.25rem 0 0; - color: var(--sl-color-white); + font-weight: 500; + letter-spacing: -0.03em; + line-height: 1.15; + margin: 0.3rem 0 0; + color: var(--ds-text); border: 0; padding: 0; } #kernel-dialog .caption { font-size: var(--sl-text-sm); - color: var(--sl-color-gray-3); - margin: 0.25rem 0 0; + color: var(--ds-text-3); + margin: 0.3rem 0 0; } #kernel-dialog .dialog-close { flex: 0 0 auto; - font-size: 1.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + font-size: 1.25rem; line-height: 1; - padding: 0.25rem 0.6rem; - border: 1px solid var(--sl-color-hairline); - border-radius: 0.5rem; - background: transparent; - color: var(--sl-color-text); + border: 1px solid var(--ds-line-strong); + border-radius: var(--ds-cut-chip); + corner-shape: bevel; + background: var(--ds-control); + color: var(--ds-text-2); cursor: pointer; + transition: + border-color var(--ds-duration) ease, + color var(--ds-duration) ease; } #kernel-dialog .dialog-close:hover { - background: var(--sl-color-gray-6); + border-color: var(--ds-accent-line); + color: var(--ds-text); } /* A grid rather than a wrapping flex row: with four analyzers the leftover panel on the second row keeps its column width instead of stretching to @@ -1304,57 +1303,78 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1rem; - margin-top: 1.25rem; + margin-top: 1.5rem; } #kernel-dialog .panel { - border: 1px solid var(--sl-color-hairline); - border-radius: 0.75rem; - padding: 0.75rem 1rem 1rem; + border: 1px solid var(--ds-line); + border-radius: var(--ds-cut-control); + corner-shape: bevel; + background: var(--ds-canvas); + padding: 1rem 1.1rem 1.1rem; + } + #kernel-dialog .panel-head { + padding-bottom: 0.6rem; + border-bottom: 1px solid var(--ds-line-soft); } #kernel-dialog .panel .who { - font-family: var(--__sl-font-mono); - font-size: var(--sl-text-xs); - letter-spacing: 0.1em; - text-transform: uppercase; + display: flex; + align-items: center; + gap: 0.45rem; + font-size: var(--sl-text-base); + font-weight: 500; + letter-spacing: -0.01em; margin: 0; - color: var(--vendor-color, var(--sl-color-accent-high)); - border-top: 3px solid var(--vendor-color, var(--sl-color-accent)); - padding-top: 0.25rem; + color: var(--ds-text); + } + #kernel-dialog .panel .who::before { + content: ''; + flex: none; + width: 0.55rem; + height: 0.55rem; + border-radius: 50%; + background: var(--vendor-rule, var(--ds-accent)); + } + #kernel-dialog .panel-head .caption { + font-family: var(--ds-font-mono); + font-size: var(--sl-text-2xs); } #kernel-dialog .matrix { width: 100%; - margin: 0.75rem 0 0; + margin: 0.85rem 0 0; border-collapse: collapse; font-size: var(--sl-text-sm); } #kernel-dialog .matrix th { - font-family: var(--__sl-font-mono); - font-size: var(--sl-text-xs); + font-family: var(--ds-font-mono); + font-size: var(--sl-text-2xs); font-weight: 400; - color: var(--sl-color-gray-3); + color: var(--ds-text-3); text-align: left; padding: 0.25rem 0.4rem; border: 0; background: transparent; } #kernel-dialog .matrix td { - border: 1px solid var(--sl-color-hairline); - padding: 0.4rem 0.5rem; + border: 1px solid var(--ds-line); + padding: 0.45rem 0.55rem; text-align: left; + background: transparent; } #kernel-dialog .matrix .matrix-corner { border: 0; } #kernel-dialog .matrix-count { - font-size: 1.5rem; - font-weight: 700; + font-family: var(--ds-font-mono); + font-variant-numeric: tabular-nums; + font-size: 1.375rem; + font-weight: 500; line-height: 1.1; margin-right: 0.35rem; } #kernel-dialog .matrix-tag { - font-family: var(--__sl-font-mono); - font-size: var(--sl-text-xs); - color: var(--sl-color-gray-3); + font-family: var(--ds-font-mono); + font-size: var(--sl-text-2xs); + color: var(--ds-text-3); } #kernel-dialog .matrix-cell.good .matrix-count { color: var(--sl-color-green-high); @@ -1364,34 +1384,32 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; } #kernel-dialog .rates { display: flex; - gap: 1.5rem; - margin-top: 0.75rem; + gap: 1.75rem; + margin-top: 0.85rem; } #kernel-dialog .rate-label { - font-family: var(--__sl-font-mono); font-size: var(--sl-text-xs); - letter-spacing: 0.1em; - color: var(--sl-color-gray-3); + font-weight: 500; + color: var(--ds-text-3); margin: 0; } #kernel-dialog .rate-value { - font-size: 1.25rem; - font-weight: 700; + font-family: var(--ds-font-mono); + font-variant-numeric: tabular-nums; + font-size: 1.2rem; + font-weight: 500; margin: 0; - color: var(--vendor-color, var(--sl-color-accent-high)); + color: var(--vendor-color, var(--ds-accent-text)); } #kernel-dialog .coverage { - margin-top: 0.75rem; - border: 1px dashed var(--sl-color-orange); - border-radius: 0.5rem; - padding: 0.5rem 0.75rem; - background: var(--sl-color-bg); + margin-top: 0.85rem; + border: 1px dashed var(--ds-line-strong); + padding: 0.55rem 0.75rem; + background: transparent; } #kernel-dialog .coverage-label { - font-family: var(--__sl-font-mono); font-size: var(--sl-text-xs); - letter-spacing: 0.08em; - text-transform: uppercase; + font-weight: 500; color: var(--sl-color-orange-high); margin: 0; } @@ -1407,21 +1425,24 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; margin: 0; } #kernel-dialog .coverage-count { - font-weight: 700; - margin-right: 0.25rem; + font-family: var(--ds-font-mono); + font-weight: 500; + color: var(--ds-text); + margin-right: 0.3rem; } #kernel-dialog .coverage-name { - font-family: var(--__sl-font-mono); - font-size: var(--sl-text-xs); - color: var(--sl-color-gray-3); + font-family: var(--ds-font-mono); + font-size: var(--sl-text-2xs); + color: var(--ds-text-3); } #kernel-dialog .scorecard { - font-family: var(--__sl-font-mono); - font-size: var(--sl-text-xs); + font-family: var(--ds-font-mono); + font-size: var(--sl-text-2xs); overflow-wrap: anywhere; } #kernel-dialog .dialog-foot { - margin-top: 1rem; + margin-top: 1.25rem; + max-width: 48rem; } #kernel-dialog .v0 { --vendor-color: var(--sl-color-accent-high); @@ -1456,330 +1477,59 @@ const snapshotPage = `/snapshots/${snapshot.slug}`; --vendor-color: var(--dfb-vendor-7); --vendor-rule: var(--dfb-vendor-7); } - #kernel-dialog .panel .who { - border-top-color: var(--vendor-rule, var(--sl-color-accent)); - } diff --git a/docs/src/components/LatencyRanking.astro b/docs/src/components/LatencyRanking.astro index e93fe6ca8..35f36d0fe 100644 --- a/docs/src/components/LatencyRanking.astro +++ b/docs/src/components/LatencyRanking.astro @@ -228,12 +228,14 @@ function layout(view: (typeof ranking.views)[number]) { }); y += mainRowHeight / 2; for (const phase of entry.phases) { + // A materialization phase says so on its own line rather than in a + // second, wider line under it; the figure notes say what it means. y += phaseRowHeight / 2; rows.push({ kind: 'phase', tool: entry.tool, - label: phase.phase, - sublabel: phase.includedInAnalyzerTotal ? '' : 'materialization; excluded', + label: phase.includedInAnalyzerTotal ? phase.phase : `${phase.phase} · excluded`, + sublabel: '', d: phase, warm: null, estimate: null, @@ -270,21 +272,19 @@ const panels = panelsToRender.map((view) => ({ // that silently rots the next time a kernel is added — so they are emitted // from the same array the panels are, and cannot drift from it. // -// The selected pill takes the accent rather than the evolution toggle's -// `--sl-color-gray-6`: with two options a subtle fill is enough to find, and -// in the dark theme gray-6 happens to equal the ribbon's own background, so -// the only cue left is the label's colour. With fourteen options that is not -// enough, and the accent reads against the ribbon in both themes. +// Each option is a chip, and the selected one takes the accent border and +// fill: with fourteen options, colour of the label alone is not enough to find +// the current one. const toggleCss = panels .map( (panel) => ` #${panel.radioId}:checked ~ .lat-ribbon label[for='${panel.radioId}'] { - background: var(--sl-color-accent-low); - color: var(--sl-color-white); - box-shadow: inset 0 0 0 1px var(--sl-color-accent); + border-color: var(--ds-accent); + background: var(--ds-accent-low); + color: var(--ds-text); } #${panel.radioId}:focus-visible ~ .lat-ribbon label[for='${panel.radioId}'] { - outline: 2px solid var(--sl-color-accent-high); + outline: 2px solid var(--ds-accent); outline-offset: 2px; } #${panel.radioId}:checked ~ .lat-panels > #${panel.panelId} { @@ -338,7 +338,7 @@ const belowThreshold = overheadPublished.filter( {kernelViews && (
{panels.map((panel) => ( - + ))}
)} @@ -413,7 +413,7 @@ const belowThreshold = overheadPublished.filter( y={panel.height - padBottom + 39} text-anchor="middle" > - wall-clock per invocation — logarithmic scale + Wall-clock per invocation, logarithmic scale {panel.rows.map((row) => { @@ -431,7 +431,7 @@ const belowThreshold = overheadPublished.filter( {row.label} @@ -473,7 +473,7 @@ const belowThreshold = overheadPublished.filter( y={row.y - boxHeight / 2} width={Math.max(1.5, x(row.d.q3) - x(row.d.q1))} height={boxHeight} - rx="2" + /> - measured warm marginal per case — the range its - retained repeats span + measured warm marginal per case — the range + its retained repeats span )} {drawsEstimateMarks && (
  • - estimated per-invocation overhead (trivial + estimated per-invocation overhead (trivial fixture, upper bound), drawn across the range its repeats spanned, where that range starts at or above{' '} - {Math.round(SIGNIFICANCE_SHARE * 100)}% of the row's median + {Math.round(SIGNIFICANCE_SHARE * 100)}% of the row's median
  • )}
    {/* The sentence directly below each chart is its caption. The longer reading - guidance is ordinary companion prose: title it explicitly so it does not - present as one enormous caption, while preserving its evidence and the - wrapper that gives adjacent paragraphs one coherent column flow. */} -
    -

    How to read this figure

    + guidance is companion prose under its own title, so it does not present as + one enormous caption. On the latency page it is open, since reading the + figure closely is what that page is for; on the landing page, where the + figure is one of several, it starts closed and is one click away. */} +
    +How to read this figure
    {warmMeasurements.length > 0 && (

    @@ -814,7 +815,7 @@ const belowThreshold = overheadPublished.filter(

    )}
    -
    + @@ -837,26 +838,20 @@ const belowThreshold = overheadPublished.filter( display: flex; align-items: center; flex-wrap: wrap; - gap: 0.25rem; - margin: 0 0 0.75rem; - padding: 0.2rem; - border: 1px solid var(--sl-color-hairline); - border-radius: 0.6rem; - background: var(--sl-color-bg-sidebar); + gap: 0.4rem; + margin: 0 0 1rem; } .lat-ribbon label { /* The content area's global sibling rule adds margin-top to successive - elements; inside the pill row that staggers the labels vertically. */ + elements; inside the chip row that staggers the labels vertically. */ margin: 0 !important; + min-height: 2rem; + padding-inline: 0.65rem; + font-size: var(--sl-text-xs); cursor: pointer; - border-radius: 0.45rem; - padding: 0.28rem 0.6rem; - font-size: var(--sl-text-sm); - color: var(--sl-color-gray-2); - line-height: 1.3; } - .lat-ribbon label:hover { - color: var(--sl-color-white); + .lat-ribbon label:first-child { + margin-right: 0.6rem !important; } .lat-panel { display: none; @@ -870,18 +865,14 @@ const belowThreshold = overheadPublished.filter( } .figure-notes { margin-top: 1.5rem; - padding-top: 1rem; - border-top: 1px solid var(--sl-color-hairline); } - .figure-notes > h3 { - margin: 0 0 0.75rem; - font-size: var(--sl-text-lg); + .figure-notes > .prose-columns { + padding: 0.5rem 0 0.75rem; } .chart-frame { - border: 1px solid var(--sl-color-hairline); - border-radius: 0.75rem; - padding: 0.75rem 0.5rem 0.25rem; - background: var(--sl-color-bg-sidebar); + border: 1px solid var(--ds-line); + padding: 1rem 0.75rem 0.5rem; + background: var(--ds-well); overflow-x: auto; } .ranking-chart { @@ -891,35 +882,41 @@ const belowThreshold = overheadPublished.filter( height: auto; } .ranking-chart .grid { - stroke: var(--sl-color-hairline); + stroke: var(--ds-line-soft); stroke-width: 1; } .ranking-chart .axis { - stroke: var(--sl-color-gray-4); - stroke-width: 1.5; + stroke: var(--ds-line-strong); + stroke-width: 1; } .ranking-chart .row-sublabel, .ranking-chart .phase-label, .ranking-chart .phase-value { - font-family: var(--__sl-font-mono); + font-family: var(--ds-font-mono); font-size: 10px; - fill: var(--sl-color-gray-3); + fill: var(--ds-text-3); } - .ranking-chart .tick, - .ranking-chart .axis-title { - font-family: var(--__sl-font-mono); + .ranking-chart .tick { + font-family: var(--ds-font-mono); font-size: 11px; - fill: var(--sl-color-gray-3); + fill: var(--ds-text-3); } - .ranking-chart .row-label { + .ranking-chart .axis-title { + font-family: var(--ds-font); font-size: 13px; - font-weight: 600; - fill: var(--sl-color-white); + font-weight: 500; + fill: var(--ds-text-2); + } + .ranking-chart .row-label { + font-family: var(--ds-font); + font-size: 13.5px; + font-weight: 500; + fill: var(--ds-text); } .ranking-chart .row-value { - font-family: var(--__sl-font-mono); + font-family: var(--ds-font-mono); font-size: 12px; - fill: var(--sl-color-gray-2); + fill: var(--ds-text-2); } .ranking-chart .row .whisker { stroke: var(--vendor-rule, var(--sl-color-accent)); @@ -988,28 +985,29 @@ const belowThreshold = overheadPublished.filter( gap: 0.4rem 1.25rem; margin: 0.75rem 0 0; padding: 0; - font-size: var(--sl-text-sm); - color: var(--sl-color-gray-2); + font-size: var(--sl-text-xs); + color: var(--ds-text-3); } + /* The label is one flex item, so a bold lead word wraps with its sentence + instead of being squeezed into a column of its own. */ .chart-legend li { margin: 0; display: flex; - align-items: center; + align-items: baseline; } .chart-legend .swatch { + flex: none; display: inline-block; width: 1.1rem; height: 0.7rem; margin-right: 0.45rem; - background: var(--sl-color-gray-3); + background: var(--ds-text-3); } .chart-legend .key-median .swatch { width: 0.2rem; - border-radius: 0.1rem; } .chart-legend .key-box .swatch { opacity: 0.42; - border-radius: 0.15rem; } .chart-legend .key-whisker .swatch { height: 0.15rem; @@ -1018,7 +1016,6 @@ const belowThreshold = overheadPublished.filter( .chart-legend .key-phase .swatch { height: 0.35rem; opacity: 0.28; - border-radius: 0.1rem; } .chart-legend .key-warm .swatch { width: 0.7rem; @@ -1029,13 +1026,13 @@ const belowThreshold = overheadPublished.filter( width: 1.1rem; height: 0.45rem; background: none; - border-top: 1.5px dashed var(--sl-color-gray-3); - border-left: 1.5px solid var(--sl-color-gray-3); - border-right: 1.5px solid var(--sl-color-gray-3); + border-top: 1.5px dashed var(--ds-text-3); + border-left: 1.5px solid var(--ds-text-3); + border-right: 1.5px solid var(--ds-text-3); } .caption { font-size: var(--sl-text-sm); - color: var(--sl-color-gray-3); + color: var(--ds-text-3); } /* The same fixed vendor→colour identity map every other section uses, keyed by tool rather than by row position. Slots v0–v3 keep the definitions they diff --git a/docs/src/components/LatencyTier.astro b/docs/src/components/LatencyTier.astro index c822a908c..cefe81351 100644 --- a/docs/src/components/LatencyTier.astro +++ b/docs/src/components/LatencyTier.astro @@ -173,7 +173,7 @@ interface Props { every number on this page.

    -
    +
    {dimension.counts.inconclusive} {dimension.counts.unsupported} {dimension.counts.runner_errors}{fraction(dimension.true_positive_rate)}{fraction(dimension.false_positive_rate)}{percentCell(dimension.template_macro.true_positive_rate_percent)}{percentCell(dimension.template_macro.false_positive_rate_percent)}{fraction(dimension.true_positive_rate)}{fraction(dimension.false_positive_rate)}{percentCell(dimension.template_macro.true_positive_rate_percent)}{percentCell(dimension.template_macro.false_positive_rate_percent)}
    @@ -235,7 +235,7 @@ interface Props { side by side as though they were.

    -
    +
    @@ -329,7 +329,7 @@ interface Props { Show the data table — analyzer-invocation wall-clock per adapter, with minima and maxima -
    +
    @@ -404,7 +404,7 @@ interface Props { adapter.phases.length } observable phases against its analyzer total`} -
    +
    @@ -507,7 +507,7 @@ interface Props {

    ) : ( <> -
    +
    @@ -611,7 +611,7 @@ interface Props { auditable if it is still legible beside the one that replaced it.

    -
    +
    @@ -707,7 +707,7 @@ interface Props { measurement.language } kernel, all ${measurement.runs.length} retained repeats`} -
    +
    @@ -798,7 +798,7 @@ interface Props { runtime or from another adapter's slope.

    -
    +
    @@ -816,7 +816,7 @@ interface Props { aria-hidden="true"> {vendorName(measurement.tool)} - + -
    Yes — measuredYes — measured A released multi-case batch that does the same per-case work, timed above. @@ -832,7 +832,7 @@ interface Props { aria-hidden="true"> {vendorName(decline.tool)} + {decline.verdict === 'deferred' ? ( Yes in the CLI — not measured here ) : ( @@ -913,7 +913,7 @@ interface Props { one, and never used to order a row.

    -
    +
    @@ -1076,7 +1076,7 @@ interface Props { selects.

    -
    +
    @@ -1185,7 +1185,7 @@ interface Props { adapter.slices.length === 1 ? 'slice' : 'slices' } for ${vendorName(adapter.tool)}`} -
    +
    @@ -1207,7 +1207,7 @@ interface Props { {adapter.slices.map((slice) => ( - +
    {slice.report.replace(/^reports\//, '').replace(/\.json$/, '')}{slice.language}{slice.language} {slice.modelProfile} {slice.timed} / {slice.cases} @@ -1243,7 +1243,7 @@ interface Props { accounted for:

    -
    +
    @@ -1306,21 +1306,35 @@ interface Props { )} diff --git a/docs/src/components/SnapshotArchive.astro b/docs/src/components/SnapshotArchive.astro index 55a4fcae3..873a735db 100644 --- a/docs/src/components/SnapshotArchive.astro +++ b/docs/src/components/SnapshotArchive.astro @@ -113,11 +113,12 @@ const entries = archived.map((snapshot) => ({ gap: 1rem; } .archive-entry { - border: 1px solid var(--sl-color-hairline); - border-inline-start: 4px solid var(--sl-color-gray-4); - border-radius: 0.5rem; - padding: 0.75rem 1rem 1rem; - background: var(--sl-color-bg-sidebar); + border: 1px solid var(--ds-line); + border-inline-start: 2px solid var(--ds-line-strong); + border-radius: 0 var(--ds-cut-control) var(--ds-cut-control) 0; + corner-shape: bevel; + padding: 0.9rem 1.1rem 1.1rem; + background: var(--ds-surface); } .archive-entry h3 { margin: 0; @@ -128,12 +129,14 @@ const entries = archived.map((snapshot) => ({ gap: 0.5rem; } .archived-pill { - padding: 0 0.5rem; - border-radius: 999px; + padding: 0.05rem 0.45rem; + border: 1px solid var(--ds-line-strong); + border-radius: var(--ds-cut-badge); + corner-shape: bevel; font-size: var(--sl-text-xs); - font-weight: 400; - background: var(--sl-color-gray-6); - color: var(--sl-color-gray-2); + font-weight: 500; + background: var(--ds-control); + color: var(--ds-text-2); } .summary { margin: 0.5rem 0 0; @@ -141,7 +144,7 @@ const entries = archived.map((snapshot) => ({ .scale { margin: 0.5rem 0 0; font-size: var(--sl-text-sm); - color: var(--sl-color-gray-2); + color: var(--ds-text-2); } .identity { margin: 0.75rem 0 0; @@ -151,8 +154,8 @@ const entries = archived.map((snapshot) => ({ font-size: var(--sl-text-sm); } .identity dt { - font-weight: 600; - color: var(--sl-color-gray-2); + font-weight: 500; + color: var(--ds-text-3); } .identity dd { margin: 0 0 0.4rem; diff --git a/docs/src/components/SnapshotBanner.astro b/docs/src/components/SnapshotBanner.astro index 4bf27ad47..7f5413eaf 100644 --- a/docs/src/components/SnapshotBanner.astro +++ b/docs/src/components/SnapshotBanner.astro @@ -34,11 +34,12 @@ const { benchmark, manifest, claim } = snapshot.results; diff --git a/docs/src/components/SnapshotEvolution.astro b/docs/src/components/SnapshotEvolution.astro index 087c6199c..ee69ff41c 100644 --- a/docs/src/components/SnapshotEvolution.astro +++ b/docs/src/components/SnapshotEvolution.astro @@ -377,10 +377,10 @@ const last = columns.at(-1)!; />
    {heading && ( - + )} - - + +
    {heading && ( @@ -500,7 +500,7 @@ const last = columns.at(-1)!; ))} - assertions + Assertions @@ -635,7 +635,7 @@ const last = columns.at(-1)!; ))} - % of its covered population + Share of its covered population @@ -669,8 +669,8 @@ const last = columns.at(-1)!; ))}
  • - marker size and the k/n label under it: kernels covered of the - snapshot's kernels + marker size and the k/n label under it: kernels covered + of the snapshot's kernels
  • @@ -690,7 +690,7 @@ const last = columns.at(-1)!; )}

    -
    +
    @@ -763,35 +763,11 @@ const last = columns.at(-1)!; diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index d0eb7f1f6..4dd4ce9a4 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -1,6 +1,6 @@ --- title: Current overview of DataFlowBench -description: BrokkAi's benchmark for data-flow analysis — Bifrost measured against reference analyzers, published exclusively from immutable freeze evidence. +description: SlopCop's benchmark for data-flow analysis — Bifrost measured against reference analyzers, published exclusively from immutable freeze evidence. tableOfContents: false --- diff --git a/docs/src/data/latency.ts b/docs/src/data/latency.ts index c2163d024..c9b753e45 100644 --- a/docs/src/data/latency.ts +++ b/docs/src/data/latency.ts @@ -735,8 +735,10 @@ export function kernelCorpusDistributions( } export function formatMs(value: number): string { + // A no-break space: a figure never parts from its unit, in a table cell or + // a sentence. const ms = Math.round(value); - if (ms < 1000) return `${ms} ms`; - if (ms < 10_000) return `${(Math.round(ms / 10) / 100).toFixed(2)} s`; - return `${(Math.round(ms / 100) / 10).toFixed(1)} s`; + if (ms < 1000) return `${ms}\u00a0ms`; + if (ms < 10_000) return `${(Math.round(ms / 10) / 100).toFixed(2)}\u00a0s`; + return `${(Math.round(ms / 100) / 10).toFixed(1)}\u00a0s`; } diff --git a/docs/src/data/scatter-layout.test.ts b/docs/src/data/scatter-layout.test.ts new file mode 100644 index 000000000..b559d66c9 --- /dev/null +++ b/docs/src/data/scatter-layout.test.ts @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { percentAxis, placeLabels } from './scatter-layout.ts'; + +test('the percent axis keeps 50% in view and trims empty space below', () => { + assert.deepEqual(percentAxis([78, 86, 100]), { min: 50, max: 100, ticks: [50, 60, 70, 80, 90, 100] }); + assert.equal(percentAxis([45, 90]).min, 40); + // A point just above a tick would sit on the axis; it gets the tick below. + assert.equal(percentAxis([41.2, 90]).min, 30); + // A value on a tick still gets room beneath it. + assert.equal(percentAxis([30]).min, 20); + assert.equal(percentAxis([]).min, 50); + assert.equal(percentAxis([-5]).min, 0); +}); + +const bounds = { left: 0, top: 0, right: 800, bottom: 400 }; + +function boxes(points: { x: number; y: number; text: string }[]) { + return placeLabels(points, { bounds, fontSize: 12, charWidth: 0.6 }).map((label, index) => { + const width = points[index].text.length * 7.2; + const left = label.anchor === 'start' ? label.x : label.anchor === 'end' ? label.x - width : label.x - width / 2; + return { left, right: left + width, top: label.y - 8.6, bottom: label.y + 2.6 }; + }); +} + +test('labels of a tight cluster do not overlap one another', () => { + const cluster = [ + { x: 200, y: 120, text: 'OpenTaint' }, + { x: 206, y: 124, text: 'FlowDroid' }, + { x: 212, y: 130, text: 'Semgrep CE' }, + { x: 222, y: 128, text: 'Infer' }, + ]; + const placed = boxes(cluster); + for (let a = 0; a < placed.length; a += 1) { + for (let b = a + 1; b < placed.length; b += 1) { + const overlaps = + placed[a].left < placed[b].right && + placed[b].left < placed[a].right && + placed[a].top < placed[b].bottom && + placed[b].top < placed[a].bottom; + assert.equal(overlaps, false, `${cluster[a].text} overlaps ${cluster[b].text}`); + } + } +}); + +test('labels stay inside the plot', () => { + const corners = [ + { x: 795, y: 5, text: 'Bifrost' }, + { x: 5, y: 395, text: 'Pysa' }, + ]; + for (const box of boxes(corners)) { + assert.ok(box.left >= bounds.left && box.right <= bounds.right, 'horizontal'); + assert.ok(box.top >= bounds.top && box.bottom <= bounds.bottom, 'vertical'); + } +}); + +test('an isolated point keeps its label beside it', () => { + const [label] = placeLabels([{ x: 400, y: 200, text: 'Joern' }], { bounds }); + assert.equal(label.leader, false); + assert.equal(label.anchor, 'start'); +}); diff --git a/docs/src/data/scatter-layout.ts b/docs/src/data/scatter-layout.ts new file mode 100644 index 000000000..c27575f24 --- /dev/null +++ b/docs/src/data/scatter-layout.ts @@ -0,0 +1,155 @@ +// Layout helpers shared by the scatter charts: a value axis fitted to the data +// it has to show, and point labels placed so that they do not collide. +// +// Everything here works in SVG user units and is deterministic, so a chart +// renders identically on every build and its geometry can be tested without +// a browser. + +export interface PercentAxis { + min: number; + max: number; + ticks: number[]; +} + +/** A 0–100 axis trimmed at the bottom to the lowest value it must show. + * + * The floor never rises above `ceiling` (so a reference line such as the + * 50% blind baseline stays inside the plot) and drops to the next multiple of + * `step` below the lowest value, so nothing sits on the bottom edge. */ +export function percentAxis(values: number[], ceiling = 50, step = 10): PercentAxis { + const min = + values.length === 0 + ? ceiling + : Math.max(0, Math.min(ceiling, Math.floor((Math.min(...values) - step / 4) / step) * step)); + const ticks: number[] = []; + for (let tick = min; tick <= 100; tick += step) ticks.push(tick); + return { min, max: 100, ticks }; +} + +export interface LabelInput { + x: number; + y: number; + text: string; +} + +export interface LabelPlacement { + x: number; + y: number; + anchor: 'start' | 'middle' | 'end'; + /** True when the label had to move far enough from its point to need a leader line. */ + leader: boolean; +} + +export interface LabelOptions { + /** The rectangle labels must stay inside. */ + bounds: { left: number; top: number; right: number; bottom: number }; + /** Point radius, including its stroke. */ + radius?: number; + /** Label font size in user units. */ + fontSize?: number; + /** Average advance per character as a fraction of the font size. */ + charWidth?: number; +} + +interface Box { + left: number; + top: number; + right: number; + bottom: number; +} + +const overlapArea = (a: Box, b: Box) => + Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left)) * + Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top)); + +/** Place one label per point, greedily, avoiding other labels, other points + * and the plot edge. + * + * Points are placed in order of how crowded their neighbourhood is, most + * crowded first, so the points with the fewest free positions choose before + * the open ones take them. Each label tries the eight compass positions at + * increasing distance; the first ring is preferred, and a label that has to go + * further out is flagged for a leader line back to its point. */ +export function placeLabels(points: LabelInput[], options: LabelOptions): LabelPlacement[] { + const radius = options.radius ?? 8; + const fontSize = options.fontSize ?? 12; + const charWidth = options.charWidth ?? 0.6; + const { bounds } = options; + const ascent = fontSize * 0.72; + const descent = fontSize * 0.22; + const pad = 2; + + const pointBoxes: Box[] = points.map((point) => ({ + left: point.x - radius, + top: point.y - radius, + right: point.x + radius, + bottom: point.y + radius, + })); + + const crowding = points.map((point) => + points.reduce((count, other) => count + (Math.hypot(other.x - point.x, other.y - point.y) < 90 ? 1 : 0), 0), + ); + const order = points.map((_, index) => index).sort((a, b) => crowding[b] - crowding[a] || a - b); + + const directions: { dx: number; dy: number; anchor: LabelPlacement['anchor'] }[] = [ + { dx: 1, dy: 0, anchor: 'start' }, + { dx: -1, dy: 0, anchor: 'end' }, + { dx: 0, dy: -1, anchor: 'middle' }, + { dx: 0, dy: 1, anchor: 'middle' }, + { dx: 1, dy: -1, anchor: 'start' }, + { dx: -1, dy: -1, anchor: 'end' }, + { dx: 1, dy: 1, anchor: 'start' }, + { dx: -1, dy: 1, anchor: 'end' }, + ]; + const rings = [radius + 4, radius + 18, radius + 34, radius + 54]; + + const placed: Box[] = []; + const result: LabelPlacement[] = new Array(points.length); + + for (const index of order) { + const point = points[index]; + const width = point.text.length * fontSize * charWidth; + let best: { score: number; placement: LabelPlacement; box: Box } | undefined; + + rings.forEach((distance, ring) => { + directions.forEach((direction, preference) => { + // Diagonals sit on the circle, not on the square around it. + const scale = direction.dx !== 0 && direction.dy !== 0 ? Math.SQRT1_2 : 1; + const anchorX = point.x + direction.dx * distance * scale; + // Vertical offsets are measured to the text's near edge, not its baseline. + const edgeY = point.y + direction.dy * distance * scale; + const baseline = + direction.dy < 0 ? edgeY - descent : direction.dy > 0 ? edgeY + ascent : point.y + (ascent - descent) / 2; + const left = + direction.anchor === 'start' ? anchorX : direction.anchor === 'end' ? anchorX - width : anchorX - width / 2; + const box: Box = { + left: left - pad, + top: baseline - ascent - pad, + right: left + width + pad, + bottom: baseline + descent + pad, + }; + + let score = ring * 1000 + preference; + for (const other of placed) score += overlapArea(box, other) * 100; + pointBoxes.forEach((other, otherIndex) => { + if (otherIndex !== index) score += overlapArea(box, other) * 60; + }); + const outside = + Math.max(0, bounds.left - box.left) + + Math.max(0, box.right - bounds.right) + + Math.max(0, bounds.top - box.top) + + Math.max(0, box.bottom - bounds.bottom); + score += outside * 5000; + + if (!best || score < best.score) { + best = { score, box, placement: { x: anchorX, y: baseline, anchor: direction.anchor, leader: ring > 1 } }; + } + }); + }); + + placed.push(best!.box); + result[index] = best!.placement; + } + + return result; +} diff --git a/docs/src/fonts/ApfelGrotezk-Fett.woff2 b/docs/src/fonts/ApfelGrotezk-Fett.woff2 new file mode 100644 index 000000000..73d6417fb Binary files /dev/null and b/docs/src/fonts/ApfelGrotezk-Fett.woff2 differ diff --git a/docs/src/fonts/ApfelGrotezk-Mittel.woff2 b/docs/src/fonts/ApfelGrotezk-Mittel.woff2 new file mode 100644 index 000000000..8440a76e8 Binary files /dev/null and b/docs/src/fonts/ApfelGrotezk-Mittel.woff2 differ diff --git a/docs/src/fonts/ApfelGrotezk-Regular.woff2 b/docs/src/fonts/ApfelGrotezk-Regular.woff2 new file mode 100644 index 000000000..bfb09859a Binary files /dev/null and b/docs/src/fonts/ApfelGrotezk-Regular.woff2 differ diff --git a/docs/src/fonts/JetBrainsMono-wght.woff2 b/docs/src/fonts/JetBrainsMono-wght.woff2 new file mode 100644 index 000000000..cd5102a44 Binary files /dev/null and b/docs/src/fonts/JetBrainsMono-wght.woff2 differ diff --git a/docs/src/fonts/OFL.txt b/docs/src/fonts/OFL.txt new file mode 100644 index 000000000..7e1f12541 --- /dev/null +++ b/docs/src/fonts/OFL.txt @@ -0,0 +1,100 @@ +Fonts bundled with this site +============================ + +Apfel Grotezk (ApfelGrotezk-Regular, -Mittel, -Fett) + Collletttivo — https://collletttivo.it + Licensed under the SIL Open Font License, Version 1.1. + +JetBrains Mono (JetBrainsMono-wght, Latin subset of the variable font) + Copyright 2020 The JetBrains Mono Project Authors + (https://github.com/JetBrains/JetBrainsMono) + Licensed under the SIL Open Font License, Version 1.1. + +The licence text follows and applies to each font above. + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/src/pages/robots.txt.ts b/docs/src/pages/robots.txt.ts new file mode 100644 index 000000000..a971be8d9 --- /dev/null +++ b/docs/src/pages/robots.txt.ts @@ -0,0 +1,8 @@ +import type { APIRoute } from 'astro'; + +// Every page is public; the sitemap Starlight generates lists them. Built +// from the configured site so a preview build points at its own sitemap. +export const GET: APIRoute = ({ site }) => { + const sitemap = new URL(`${import.meta.env.BASE_URL.replace(/\/?$/, '/')}sitemap-index.xml`, site); + return new Response(`User-agent: *\nAllow: /\n\nSitemap: ${sitemap.href}\n`); +}; diff --git a/docs/src/plugins/rehype-table-scroll.mjs b/docs/src/plugins/rehype-table-scroll.mjs new file mode 100644 index 000000000..144e4837d --- /dev/null +++ b/docs/src/plugins/rehype-table-scroll.mjs @@ -0,0 +1,99 @@ +// Markdown tables: a scroll box around each, and sensible break points in the +// long identifiers their cells hold. +// +// A table spans the content column, and one with more columns than the +// column has room for must pan rather than run under the contents list. A +// table element cannot do both: made a scrolling block (Starlight's default) +// it shrinks to its content, and made a full-width table it cannot scroll. So +// the table fills the column and a `
    ` wrapper does the +// scrolling; see "Tables" in foundation.css. +// +// Cells hold identifiers, paths and digests that have no spaces. Left alone +// they either force their column wide or, allowed to break anywhere, crumble +// a character at a time. Instead each long token gets a `` after its own +// separators — `_ - / . :` — and a long hex digest splits into even pieces of +// at most sixteen characters, so it wraps where a reader would split it. +// +// Kept identical in the UsageBench repository. + +/** Tokens shorter than this never need a break. */ +const LONG = 14; +const SEPARATOR = /[_\-/.:]/; +const HEX_RUN = /^[0-9a-f]{16,}$/i; + +/** Splits one long token into pieces, each ending where a break belongs. */ +function pieces(token) { + if (HEX_RUN.test(token)) { + // Even pieces of at most sixteen: a 40-character revision in three, a + // 64-character digest in four. + const size = Math.ceil(token.length / Math.ceil(token.length / 16)); + return token.match(new RegExp(`.{1,${size}}`, 'g')); + } + const out = []; + let start = 0; + for (let i = 0; i < token.length - 1; i++) { + if (SEPARATOR.test(token[i])) { + out.push(token.slice(start, i + 1)); + start = i + 1; + } + } + out.push(token.slice(start)); + return out; +} + +/** Text node → text and `` nodes, or null when nothing is long. */ +function breakText(value) { + const parts = value.split(/(\s+)/); + if (!parts.some((part) => part.length >= LONG && !/\s/.test(part))) return null; + const nodes = []; + let pending = ''; + for (const part of parts) { + if (part.length < LONG || /\s/.test(part)) { + pending += part; + continue; + } + const split = pieces(part); + split.forEach((piece, index) => { + pending += piece; + if (index < split.length - 1) { + nodes.push({ type: 'text', value: pending }); + nodes.push({ type: 'element', tagName: 'wbr', properties: {}, children: [] }); + pending = ''; + } + }); + } + if (pending) nodes.push({ type: 'text', value: pending }); + return nodes; +} + +/** @param {any} node */ +function addBreaks(node) { + if (!Array.isArray(node.children)) return; + node.children = node.children.flatMap((child) => { + if (child.type === 'text') return breakText(child.value) ?? [child]; + addBreaks(child); + return [child]; + }); +} + +/** @param {any} node */ +function wrapTables(node) { + if (!Array.isArray(node.children)) return; + node.children = node.children.map((child) => { + if (child.type === 'element' && child.tagName === 'table') { + addBreaks(child); + return { + type: 'element', + tagName: 'div', + properties: { className: ['ds-table'] }, + children: [child], + }; + } + wrapTables(child); + return child; + }); +} + +export function rehypeTableScroll() { + return (/** @type {any} */ tree) => wrapTables(tree); +} diff --git a/docs/src/routeData.ts b/docs/src/routeData.ts new file mode 100644 index 000000000..66618138d --- /dev/null +++ b/docs/src/routeData.ts @@ -0,0 +1,26 @@ +/// + +import { defineRouteMiddleware } from '@astrojs/starlight/route-data'; + +// Archived snapshot pages were written before each page named its release in +// its description, so an early freeze's analyzers page shares one sentence +// with three others and search results cannot tell them apart. The frozen +// pages stay as they were published; only the description their +// carries gains the release it belongs to. +const SNAPSHOT_PAGE = /^snapshots\/v(\d+)-(\d+)-(\d+)(?:\/|$)/; +const DESCRIPTION_TAGS = new Set(['description', 'og:description']); + +export const onRequest = defineRouteMiddleware((context) => { + const route = context.locals.starlightRoute; + const match = SNAPSHOT_PAGE.exec(route.entry.id); + const description = route.entry.data.description; + if (!match || !description) return; + const version = `v${match[1]}.${match[2]}.${match[3]}`; + if (description.includes(version)) return; + for (const tag of route.head) { + const name = tag.attrs?.name ?? tag.attrs?.property; + if (tag.tag === 'meta' && typeof name === 'string' && DESCRIPTION_TAGS.has(name)) { + tag.attrs!.content = `Snapshot ${version}: ${description}`; + } + } +}); diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css index 52171a0ad..693642aff 100644 --- a/docs/src/styles/custom.css +++ b/docs/src/styles/custom.css @@ -1,14 +1,35 @@ -/* DataFlowBench accent styling over Starlight defaults. */ +/* DataFlowBench's own layer over the shared foundation (foundation.css, which + is kept identical to UsageBench's copy). This file holds what is particular + to this site: its cyan accent, the analyzer palette, and the page widths its + charts need. */ + +/* ---- Accent --------------------------------------------------------------- + Two sets of names, on purpose. The `--sl-color-accent*` trio is Bifrost's + identity colour on every chart (vendor slot 0) as well as Starlight's + accent, so its values are the ones published since v0.1.0 and must not + move. The `--ds-accent*` tokens are the interface accent — focus rings, + selected controls, list ticks, links — tuned for contrast on the new + grounds from the same cyan. */ :root { --sl-color-accent-low: #10222b; --sl-color-accent: #0e7490; --sl-color-accent-high: #a5f3fc; + + --ds-accent: #2aa7c2; + --ds-accent-text: #8fe3f2; + --ds-accent-low: #0c1d22; + --ds-accent-line: #235d6b; } :root[data-theme='light'] { --sl-color-accent-low: #cffafe; --sl-color-accent: #0e7490; --sl-color-accent-high: #155e75; + + --ds-accent: #0e7490; + --ds-accent-text: #0b5f76; + --ds-accent-low: #dcf0f3; + --ds-accent-line: #7eb8c5; } /* Vendor colour slots 4-7, added for the four analyzers v0.6.0 introduced. @@ -33,139 +54,64 @@ :root[data-theme='light'] { --dfb-vendor-6: hsl(150, 90%, 28%); --dfb-vendor-7: hsl(310, 80%, 42%); -} -table code { - white-space: nowrap; + /* Starlight's light orange and green (lightness 60% and 46%) sit near 1.6:1 + on the light grounds, too faint for the chart labels, counts and legend + text set in them. The hues — and so the identities a reader learned — + stay; only the lightness drops, to about 4:1 or better on every ground. + Red and purple come down with them so no slot reads paler than the rest. + The dark theme, the primary one, is untouched. */ + --sl-color-orange: hsl(38, 95%, 33%); + --sl-color-green: hsl(101, 75%, 30%); + --sl-color-red: hsl(339, 75%, 46%); + --sl-color-purple: hsl(281, 70%, 48%); } :root { - /* Roughly 75 characters at the body size: the measure the prose on these - pages was written against. */ - --dfb-measure: 46rem; + /* The prose measure: roughly 75 characters at the body size. */ + --dfb-measure: var(--ds-measure); } -/* Desktop content width. - ---------------------------------------------------------------------- - Starlight publishes a 45rem content column, which is the right measure for - a page of prose and the wrong one for these pages: almost everything here - is a grid of cards, a bar panel or a viewBox chart, and at 45rem those stack - into a column several screens tall while two thirds of a desktop display - sits empty. - - The fix is the one variable the whole layout is built from rather than a - per-component breakout, so every grid, chart and table on every page gets - the room at once. The grids are already `auto-fit minmax(...)`, so they - spend the extra width on columns without any further help. - - The width is a clamp, not a step, so it grows with the viewport and never - shrinks below what Starlight published: - - the lower bound is 45rem, so nothing below the 72rem breakpoint — every - phone and tablet — is touched, and neither is a narrow desktop window; - - the middle term leaves room for the chrome that is actually on the page: - the nav sidebar always, plus the table-of-contents sidebar on the pages - that have one (Starlight gives the two sidebars equal width), and 6rem - of gutter; - - the upper bound is 80rem, past which a wider column stops buying - columns and starts buying whitespace. - - `screen and` keeps this out of print, where Starlight sets its own - full-bleed width on `.main-pane`. */ -@media screen and (min-width: 72rem) { - :root[data-has-sidebar] { - --sl-content-width: clamp( - 45rem, - calc(100vw - var(--sl-sidebar-width) - 6rem), - 80rem - ); - } - - /* A page with a table of contents pays for a second sidebar, and Starlight - splits whatever is left over between the content pane and the contents - pane — so every rem of gutter asked for here costs the column two. The - reservation is halved to match: the same gutter, half the price. */ - :root[data-has-sidebar][data-has-toc] { - --sl-content-width: clamp( - 45rem, - calc(100vw - (2 * var(--sl-sidebar-width)) - 3rem), - 80rem - ); - } - - /* A wider column must widen the grids and the charts, not the sentences. - Running text keeps a fixed measure and everything else — grids, tables, - charts, card strips, the components' own containers — takes the full - width. - - The rule reaches paragraphs at any depth, because the prose that would - otherwise run to 130 characters is as often a chart's explanatory - paragraph inside a component as it is a top-level one. It is a - `max-width`, so on everything already narrower than the measure — every - paragraph inside a card, a bar row or a dialog panel — it does nothing. - Lists that carry a class are layout (legends, chip strips, coverage - lists), not prose, and are left alone. */ - .sl-markdown-content :where(p, ul:not([class]), ol:not([class]), blockquote, dl) { - max-width: var(--dfb-measure); - } +/* A wider column widens the grids and the charts, never the sentences. + Running text keeps one fixed measure and everything else — grids, tables, + charts, card strips, the components' own containers — takes the full width. - /* On the landing (recognized by its decision landscape), top-level explanatory - paragraphs flow as two newspaper columns instead of a capped single - measure that leaves the right third of the wide column dead. Each column - is ~half the content width, i.e. the same readable measure the cap - enforces — the width is spent on flow, not on line length. Short - paragraphs simply occupy the first column; the eyebrow label stays as - is. Browsers without :has() keep the capped single column. + The rule reaches paragraphs at any depth, because the prose that would + otherwise run to 130 characters is as often a chart's explanatory paragraph + inside a component as it is a top-level one. It is a `max-width`, so on + everything already narrower than the measure — every paragraph inside a + card, a bar row or a dialog panel — it does nothing. Lists that carry a + class are layout (legends, chip strips, coverage lists), not prose, and are + left alone. - This applies to a paragraph *on its own*, where the two columns are one - continuous read: down the left, then down the right, and nothing else is - interleaved. It must never be given to two adjacent paragraphs, which - would put the tail of the first at the top of the right column and the - tail of the second directly beneath it — two half-paragraphs stacked in - reading order, which is not the order they are meant to be read in. Runs - of adjacent paragraphs use `.prose-columns` below, which flows the whole - run through one multicol box instead. */ - .sl-markdown-content:has(.decision-landscape) > p:not(.eyebrow):not(.subtitle) { - columns: 2; - column-gap: 2.5rem; - max-width: none; - } + Earlier versions flowed long runs of prose as two newspaper columns to use + the width. On a screen that means reading down one column, scrolling back + up and reading down the next, so the prose now keeps one column and the + width goes to the figures. `.prose-columns` remains as a plain wrapper. */ +.sl-markdown-content :where(p, ul:not([class]), ol:not([class]), blockquote, dl) { + max-width: var(--dfb-measure); +} +.sl-markdown-content .prose-columns > p { + max-width: var(--dfb-measure); +} - /* A run of consecutive paragraphs as one newspaper block: every paragraph - of the run flows down the left column before any of it appears on the - right. The wrapper is what carries the columns, so the paragraphs inside - are ordinary block children and the rule above no longer reaches them. */ - .sl-markdown-content:has(.decision-landscape) .prose-columns { - columns: 2; - column-gap: 2.5rem; - max-width: none; - } - .sl-markdown-content:has(.decision-landscape) .prose-columns > p { - max-width: none; - } - /* Paragraphs may straddle the column break, and should: the balancer needs - to split something, and a run of two or three long paragraphs that each - refuse to break leaves one column half-empty. A continuation is legible - here in a way it was not under the old per-paragraph rule — the right - column carries one text in reading order, so the reader arrives at the - top of it from the bottom of the left. Orphans and widows are held at - three lines so a break can never strand one line at either end. */ - .sl-markdown-content:has(.decision-landscape) .prose-columns > * { - orphans: 3; - widows: 3; - } +/* Tables a step below the prose, as on UsageBench: the evidence tables here + hold ids and figures, and the smaller size keeps their identifier columns + to two or three lines inside the desktop column instead of one piece to a + line. */ +.sl-markdown-content table:not(:where(.not-content *)) { + font-size: 0.875rem; } /* The two viewBox charts scale with their container in both axes, so an unbounded column would spend the vertical space the grids just gave back on taller pictures. Past roughly 64rem the labels have stopped gaining legibility and the plot has stopped gaining resolution, so that is where - they stop growing, centred in whatever column they are given. Semantics, - geometry and every number inside the SVG are untouched — this is the - rendered box only. */ + they stop growing. Semantics, geometry and every number inside the SVG are + untouched — this is the rendered box only. */ .evolution-chart, .ranking-chart { max-width: 64rem; - margin-inline: auto; } /* Collapsed data tables. @@ -182,70 +128,67 @@ table code { latency ribbon use. Those switch between views of one region; there is no competing view here, only a table that may be shown or not, and inventing a second radio state for that would be a toggle with one meaningful - setting. */ -details.data-table { + setting. + + The same look serves every "Exact figures" / "Show the data table" + disclosure on the site, whatever its class: a hairline row with a plus that + turns into a cross. */ +:is(details.data-table, details.decision-data, details.landscape-data, details.historical-progress, details.figure-notes) { margin-top: 1rem; - border: 1px solid var(--sl-color-hairline); - border-radius: 0.75rem; - background: var(--sl-color-bg-sidebar); + border-top: 1px solid var(--ds-line); + border-bottom: 1px solid var(--ds-line); + background: transparent; } -details.data-table > summary { +:is(details.data-table, details.decision-data, details.landscape-data, details.historical-progress, details.figure-notes) > summary { cursor: pointer; list-style: none; - padding: 0.45rem 0.75rem; + padding: 0.6rem 0; font-size: var(--sl-text-sm); - color: var(--sl-color-gray-2); - border-radius: 0.75rem; + font-weight: 500; + color: var(--ds-text-2); display: flex; align-items: center; - gap: 0.5rem; + justify-content: space-between; + gap: 0.75rem; + transition: color var(--ds-duration) ease; } /* Safari still needs the WebKit pseudo-element to drop the native triangle. */ -details.data-table > summary::-webkit-details-marker { +:is(details.data-table, details.decision-data, details.landscape-data, details.historical-progress, details.figure-notes) > summary::-webkit-details-marker { display: none; } -details.data-table > summary::before { - content: ''; +:is(details.data-table, details.decision-data, details.landscape-data, details.historical-progress, details.figure-notes) > summary::after { + content: '+'; flex: none; - width: 0; - height: 0; - border-top: 0.3rem solid transparent; - border-bottom: 0.3rem solid transparent; - border-left: 0.42rem solid currentColor; - transition: transform 0.15s ease; + font-family: var(--ds-font-mono); + font-size: 1.05rem; + line-height: 1; + color: var(--ds-text-3); + transition: transform var(--ds-duration) ease; } -details.data-table[open] > summary::before { - transform: rotate(90deg); +:is(details.data-table, details.decision-data, details.landscape-data, details.historical-progress, details.figure-notes)[open] > summary::after { + transform: rotate(45deg); } -@media (prefers-reduced-motion: reduce) { - details.data-table > summary::before { - transition: none; - } -} - -details.data-table > summary:hover { - color: var(--sl-color-white); +:is(details.data-table, details.decision-data, details.landscape-data, details.historical-progress, details.figure-notes) > summary:hover { + color: var(--ds-text); } -details.data-table > summary:focus-visible { - outline: 2px solid var(--sl-color-accent-high); +:is(details.data-table, details.decision-data, details.landscape-data, details.historical-progress, details.figure-notes) > summary:focus-visible { + outline: 2px solid var(--ds-accent); outline-offset: 2px; } details.data-table[open] > summary { - border-bottom: 1px solid var(--sl-color-hairline); - border-end-start-radius: 0; - border-end-end-radius: 0; + border-bottom: 1px solid var(--ds-line-soft); } details.data-table > .table-scroll { overflow-x: auto; - padding: 0 0.75rem 0.25rem; + padding: 0 0 0.75rem; } /* The content area's global sibling rule adds a top margin to every element @@ -255,6 +198,13 @@ details.data-table > .table-scroll > table { margin-top: 0.75rem; } +/* Numbers in the site's tables line up: the mono's figures are tabular. */ +.sl-markdown-content :is(td, th) :is(.num, .ds-num) { + font-family: var(--ds-font-mono); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + /* In-page anchor ergonomics. ---------------------------------------------------------------------- The landing page's section strip and the snapshot pages' "On this page" diff --git a/docs/src/styles/foundation.css b/docs/src/styles/foundation.css new file mode 100644 index 000000000..bbddb4c6b --- /dev/null +++ b/docs/src/styles/foundation.css @@ -0,0 +1,1278 @@ +/* ========================================================================== + Benchmark site foundation + -------------------------------------------------------------------------- + Shared, byte for byte, by the DataFlowBench and UsageBench documentation + sites: edit it in one repository and copy it to the other. Everything + site-specific — the accent colour, the analyzer palette, page layout — lives + in each site's own stylesheet, which loads after this one and sets the + `--ds-accent*` tokens this file reads. + + The language is borrowed from the Brokk SlopCop design system and turned + down for reading: a near-black ground, bone-white type, hairline rules and + chamfered panels, with one accent per site and nothing rounded, glowing or + decorative. Separation comes from hairlines, not from fills. SlopCop's + display face and its red are deliberately not carried over: these are + reference pages, read for minutes at a time, not a landing page. + + Starlight's own styles live in cascade layers, so every rule here — which is + unlayered — wins over them without `!important`. + ========================================================================== */ + +/* ---- Type --------------------------------------------------------------- + Apfel Grotezk for everything a reader reads; JetBrains Mono for evidence + only — identifiers, digests, paths, code and the numbers in tables and + charts (Apfel's figures are proportional and it has no `tnum` feature, so + any column of numbers is set in the mono). Both are OFL; see + `src/fonts/OFL.txt`. */ +@font-face { + font-family: 'Apfel Grotezk'; + src: url('../fonts/ApfelGrotezk-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Apfel Grotezk'; + src: url('../fonts/ApfelGrotezk-Mittel.woff2') format('woff2'); + font-weight: 500 600; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Apfel Grotezk'; + src: url('../fonts/ApfelGrotezk-Fett.woff2') format('woff2'); + font-weight: 700 900; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('../fonts/JetBrainsMono-wght.woff2') format('woff2-variations'); + font-weight: 100 800; + font-style: normal; + font-display: swap; +} + +/* ---- Tokens: dark (the primary theme) ------------------------------------ */ +:root { + color-scheme: dark; + + /* Grounds, darkest out. They differ by a few percent on purpose. */ + --ds-canvas: #07080a; + --ds-well: #0b0c0e; + --ds-surface: #0e1012; + --ds-control: #131517; + --ds-surface-hover: #17191c; + + /* Hairlines. */ + --ds-line-soft: #1f2124; + --ds-line: #2e3134; + --ds-line-strong: #45484b; + + /* Text. All three clear 4.5:1 on every ground above. */ + --ds-text: #f2f0eb; + --ds-text-2: #c2c0bb; + --ds-text-3: #9a9da0; + + /* The accent is the site's. These are neutral fallbacks only. */ + --ds-accent: #8a8d90; + --ds-accent-text: #d6d4cf; + --ds-accent-low: #16181a; + --ds-accent-line: #5d6063; + + /* Figures and code. */ + --ds-code-text: #d4d1cb; + + --ds-shadow-panel: 0 18px 40px rgb(0 0 0 / 0.32); + --ds-shadow-float: 0 12px 28px rgb(0 0 0 / 0.45); + + --ds-font: 'Apfel Grotezk', ui-sans-serif, system-ui, -apple-system, + 'Segoe UI', sans-serif; + --ds-font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, + Consolas, monospace; + + --ds-duration: 160ms; + --ds-measure: 45rem; + + /* One spacing scale for everything the components lay out. Paragraphs keep + Starlight's 1rem flow; a figure, grid or statement sits `--ds-space-lg` + from its neighbours; a caption sits `--ds-space-sm` under its figure; the + inside of a card is padded `--ds-space-md`–`lg` and separated in `sm`s. */ + --ds-space-2xs: 0.25rem; + --ds-space-xs: 0.5rem; + --ds-space-sm: 0.75rem; + --ds-space-md: 1rem; + --ds-space-lg: 1.5rem; + --ds-space-xl: 2.5rem; + --ds-card-pad: 1.25rem; +} + +/* ---- Tokens: light ------------------------------------------------------ + Warm paper rather than white, ink rather than black, the same hairline + structure. The site stylesheet sets light accents alongside. */ +:root[data-theme='light'] { + color-scheme: light; + + --ds-canvas: #f7f6f2; + --ds-well: #efeee9; + --ds-surface: #fbfaf7; + --ds-control: #f1f0eb; + --ds-surface-hover: #ebeae4; + + --ds-line-soft: #e4e2dc; + --ds-line: #d2d0c9; + --ds-line-strong: #a9a7a0; + + --ds-text: #15171a; + --ds-text-2: #3a3d41; + --ds-text-3: #5c5f63; + + --ds-accent: #5c5f63; + --ds-accent-text: #2b2d30; + --ds-accent-low: #ebeae4; + --ds-accent-line: #a9a7a0; + + --ds-code-text: #2a2c2f; + + --ds-shadow-panel: 0 14px 32px rgb(40 36 28 / 0.08); + --ds-shadow-float: 0 10px 24px rgb(40 36 28 / 0.14); +} + +/* ---- The chamfer --------------------------------------------------------- + Nothing is rounded. Controls and panels have their corners cut, drawn with + `corner-shape: bevel` so the border, the fill, the shadow and the focus + ring all follow the cut and nothing inside is clipped (the chart tooltips + and the dialog's scroll area both depend on that). A browser without + `corner-shape` gets square corners: the cut tokens are zero until the + feature query sets them, so no one ever sees a rounded fallback. Use them + as `border-radius: var(--ds-cut-…); corner-shape: bevel;`. */ +:root { + --ds-cut-tick: 0px; + --ds-cut-badge: 0px; + --ds-cut-chip: 0px; + --ds-cut-control: 0px; + --ds-cut-panel: 0px; + --ds-cut-lead: 0px; +} +@supports (corner-shape: bevel) { + :root { + --ds-cut-tick: 2px; + --ds-cut-badge: 4px; + --ds-cut-chip: 6px; + --ds-cut-control: 8px; + --ds-cut-panel: 14px; + /* The longer cut a chip or paper panel takes at its top-left corner. */ + --ds-cut-lead: 10px; + } +} + +/* ---- Starlight's variables, mapped onto the tokens ---------------------- + Most components on both sites read Starlight's grey ramp and hairlines, so + this mapping is what carries the palette into them. The hue ramps (orange, + green, purple, red, blue) are deliberately left alone: the analyzer + palettes are built from them and must not shift. */ +:root, +::backdrop { + --sl-font: var(--ds-font); + --sl-font-mono: var(--ds-font-mono); + + --sl-color-white: var(--ds-text); + --sl-color-gray-1: #e2e0db; + --sl-color-gray-2: var(--ds-text-2); + --sl-color-gray-3: var(--ds-text-3); + --sl-color-gray-4: #64676b; + --sl-color-gray-5: var(--ds-line-strong); + --sl-color-gray-6: var(--ds-control); + --sl-color-black: var(--ds-canvas); + + --sl-color-text: var(--ds-text-2); + --sl-color-bg: var(--ds-canvas); + --sl-color-bg-nav: var(--ds-canvas); + --sl-color-bg-sidebar: var(--ds-canvas); + --sl-color-bg-inline-code: var(--ds-control); + --sl-color-hairline-light: var(--ds-line); + --sl-color-hairline: var(--ds-line-soft); + --sl-color-hairline-shade: var(--ds-canvas); + --sl-color-backdrop-overlay: rgb(3 4 5 / 0.72); + + --sl-shadow-md: var(--ds-shadow-float); + --sl-shadow-lg: var(--ds-shadow-panel); + + --sl-text-h1: clamp(2rem, 1.5rem + 1.6vw, 2.625rem); + --sl-text-h2: 1.75rem; + --sl-text-h3: 1.3125rem; + --sl-text-h4: 1.125rem; + --sl-text-h5: 1rem; + --sl-line-height: 1.65; + --sl-line-height-headings: 1.15; + + --sl-nav-height: 4rem; +} + +:root[data-theme='light'], +[data-theme='light'] ::backdrop { + --sl-color-white: var(--ds-text); + --sl-color-gray-1: #24272a; + --sl-color-gray-2: var(--ds-text-2); + --sl-color-gray-3: var(--ds-text-3); + --sl-color-gray-4: #8d9094; + --sl-color-gray-5: var(--ds-line-strong); + --sl-color-gray-6: var(--ds-control); + --sl-color-gray-7: var(--ds-surface); + --sl-color-black: var(--ds-canvas); + + --sl-color-text: var(--ds-text-2); + --sl-color-bg: var(--ds-canvas); + --sl-color-bg-nav: var(--ds-canvas); + --sl-color-bg-sidebar: var(--ds-canvas); + --sl-color-bg-inline-code: var(--ds-control); + --sl-color-hairline-light: var(--ds-line); + --sl-color-hairline: var(--ds-line-soft); + --sl-color-hairline-shade: var(--ds-line); + --sl-color-backdrop-overlay: rgb(40 36 28 / 0.35); +} + +/* ---- Base -------------------------------------------------------------- */ +body { + background: var(--ds-canvas); + color: var(--ds-text-2); + font-family: var(--ds-font); + font-kerning: normal; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +::selection { + background: var(--ds-accent-low); + color: var(--ds-text); +} + +/* One focus ring everywhere: 2px of the accent, clear of the shape. */ +:focus-visible { + outline: 2px solid var(--ds-accent); + outline-offset: 3px; +} + +/* ---- Header -------------------------------------------------------------- */ +/* Starlight names both the bar and its inner row `.header`; only the bar + draws the rule. */ +header.header { + border-bottom: 1px solid var(--ds-line-soft); + background: color-mix(in srgb, var(--ds-canvas) 92%, transparent); + backdrop-filter: blur(8px); +} + +.site-title { + gap: 0.6rem; + font-size: 1.3125rem; + font-weight: 500; + letter-spacing: -0.03em; + color: var(--ds-text); +} +.site-title img { + height: 1.6rem; + width: auto; +} + +button[data-open-modal] { + border-radius: var(--ds-cut-control); + corner-shape: bevel; +} +@media (min-width: 50rem) { + button[data-open-modal] { + border-color: var(--ds-line-strong); + background-color: var(--ds-control); + color: var(--ds-text-3); + transition: + border-color var(--ds-duration) ease, + color var(--ds-duration) ease; + } + button[data-open-modal]:hover { + border-color: var(--ds-accent-line); + color: var(--ds-text); + } +} +button[data-open-modal] > kbd { + border-radius: var(--ds-cut-badge); + corner-shape: bevel; + background-color: var(--ds-surface); + font-family: var(--ds-font-mono); +} + +site-search dialog { + border-radius: var(--ds-cut-panel); + corner-shape: bevel; + border-color: var(--ds-line-strong); + background-color: var(--ds-surface); +} + +starlight-theme-select select, +starlight-lang-select select { + font-family: var(--ds-font); +} + +/* ---- Page geometry ------------------------------------------------------- + The content and its contents list travel together as one block right of + the sidebar: the column of content first, then a narrow contents column + beside it. The block is the same width on every page, so the text and the + contents list sit in the same place on every page, and anything wide — + tables, charts, card grids — fits the column rather than moving it. + + On a large display the room the block leaves is split one part left to + two right, so the text lands a little left of centre, where the eye + starts, without a wide empty margin beside the sidebar. On a narrower + display the block simply fills the room, one gutter each side, and the + contents list and its gap narrow before the column does. Pages without a + contents list give its room to the content. + + On a large display the whole scale grows a little — 16px at 1440px wide, + 18px at 2560px — so the sidebars stay readable. Breakpoints are unaffected: + media queries resolve rem against the browser default, not this size. */ +html { + font-size: clamp(100%, 0.8393rem + 0.1786vw, 112.5%); +} +:root { + --sl-sidebar-width: 17rem; + --ds-column: 56rem; + --sl-content-width: var(--ds-column); + --ds-toc-width: clamp(11rem, 16vw - 1rem, 14rem); + --ds-toc-gap: clamp(2rem, 6vw - 3.5rem, 3rem); + --ds-page-pad: clamp(2.5rem, 4vw, 4rem); + --ds-page-width: calc(var(--ds-column) + var(--ds-toc-gap) + var(--ds-toc-width)); + --ds-page-lead: 0.3333; +} +@media screen and (min-width: 72rem) { + :root[data-has-sidebar] { + --sl-content-pad-x: var(--ds-page-pad); + } + :root[data-has-sidebar] .main-frame > .lg\:sl-flex { + --ds-page-start: max( + var(--ds-page-pad), + (100% - var(--ds-page-width)) * var(--ds-page-lead) + ); + margin-inline-start: var(--ds-page-start); + width: min(var(--ds-page-width), 100% - var(--ds-page-start) - var(--ds-page-pad)); + } + /* The content pane takes the block; the panels inside it add no gutter of + their own, and their rules span only the content. */ + :root[data-has-sidebar] .main-pane { + order: 1; + flex: 1 1 0; + min-width: 0; + width: auto; + } + :root[data-has-sidebar] .main-pane .content-panel { + padding-inline: 0; + } + :root[data-has-sidebar] .main-pane .sl-container { + max-width: none; + margin-inline: 0; + } + /* Starlight pins the contents pane to the window at full width; here it + is a column of the block that follows the reader down the page. */ + :root[data-has-sidebar] .right-sidebar-container { + order: 2; + flex: 0 0 var(--ds-toc-width); + width: auto; + margin-inline-start: var(--ds-toc-gap); + } + :root[data-has-sidebar] .right-sidebar { + position: sticky; + top: var(--sl-nav-height); + width: auto; + height: auto; + max-height: calc(100vh - var(--sl-nav-height)); + padding-top: 0; + border-inline-start: 0; + } + :root[data-has-sidebar] .right-sidebar-panel { + padding: 2rem 0; + } + :root[data-has-sidebar] .right-sidebar-panel .sl-container { + width: auto; + max-width: none; + } + /* The search box starts where the content does: the header's first column + runs to the sidebar's edge plus the block's offset. The percentage is the + header row's width, which is the window's less the bar's own padding. */ + :root[data-has-sidebar] .header { + --__main-column-fr: max( + var(--ds-page-pad), + ( + 100% + 2 * var(--sl-nav-pad-x) - var(--sl-sidebar-width) - + var(--ds-page-width) + ) * var(--ds-page-lead) + ); + } +} + +/* ---- Sidebar ------------------------------------------------------------- + Section labels quiet, links a step quieter, and the current page marked by + a short accent rule rather than a filled pill. */ +.sidebar-pane { + border-inline-end: 1px solid var(--ds-line-soft); +} +.sidebar-content .large, +.sidebar-content a { + font-size: 0.9375rem; +} +.sidebar-content .large { + font-weight: 500; + letter-spacing: -0.01em; + color: var(--ds-text); +} +.sidebar-content a { + position: relative; + border-radius: 0; + color: var(--ds-text-3); + transition: color var(--ds-duration) ease; +} +.sidebar-content a:hover, +.sidebar-content a:focus-visible { + color: var(--ds-text); +} +/* The top-level entries sit on the same step as a group label. */ +.sidebar-content .top-level > li > a { + color: var(--ds-text-2); + font-weight: 500; +} +.sidebar-content [aria-current='page'], +.sidebar-content [aria-current='page']:hover, +.sidebar-content [aria-current='page']:focus { + color: var(--ds-text); + background: transparent; + font-weight: 500; +} +.sidebar-content [aria-current='page']::before { + content: ''; + position: absolute; + inset-block: 0.35em; + inset-inline-start: -1px; + width: 2px; + background: var(--ds-accent); +} +/* Top-level current pages have no rail to sit on; give them a tick instead. */ +.sidebar-content .top-level > li > a[aria-current='page']::before { + inset-inline-start: -0.35rem; +} +.sidebar-content ul ul li { + border-inline-start-color: var(--ds-line-soft); +} +.sidebar-content summary { + border-radius: 0; +} +.sidebar-content .caret { + color: var(--ds-text-3); +} + +/* ---- Right-hand contents --------------------------------------------------- */ +.right-sidebar-panel h2 { + font-size: var(--sl-text-sm); + font-weight: 500; + color: var(--ds-text-3); + letter-spacing: 0; +} +.right-sidebar-panel a, +starlight-toc a { + font-size: var(--sl-text-sm); + line-height: 1.4; + border-radius: 0; + color: var(--ds-text-3); +} +.right-sidebar-panel a:hover { + color: var(--ds-text); +} +starlight-toc a[aria-current='true'] { + color: var(--ds-accent-text); + box-shadow: inset 2px 0 0 var(--ds-accent); +} +.right-sidebar { + border-inline-start-color: var(--ds-line-soft); +} + +/* ---- Page title and content --------------------------------------------- */ +.content-panel { + border-top-color: var(--ds-line-soft); +} +.content-panel h1, +#_top { + font-weight: 500; + letter-spacing: -0.035em; + line-height: 1.08; + color: var(--ds-text); + text-wrap: balance; +} + +.sl-markdown-content { + font-size: 1.0625rem; +} +.sl-markdown-content :is(h1, h2, h3, h4, h5, h6) { + font-weight: 500; + color: var(--ds-text); + text-wrap: balance; +} +.sl-markdown-content h2 { + letter-spacing: -0.03em; + line-height: 1.12; +} +.sl-markdown-content h3 { + letter-spacing: -0.02em; + line-height: 1.2; +} +.sl-markdown-content h4, +.sl-markdown-content h5 { + letter-spacing: -0.01em; +} +/* A section starts with a hairline across the column, not with space alone. */ +.sl-markdown-content :is(h2, .sl-heading-wrapper.level-h2):not(:first-child) { + margin-top: 2.75rem; + padding-top: 1.5rem; + border-top: 1px solid var(--ds-line); +} +.sl-markdown-content .sl-heading-wrapper.level-h2 > h2 { + margin-top: 0; + padding-top: 0; + border-top: 0; +} +.sl-markdown-content :is(h3, .sl-heading-wrapper.level-h3):not(:first-child) { + margin-top: 2.25rem; +} +.sl-markdown-content .sl-anchor-link { + color: var(--ds-text-3); +} + +.sl-markdown-content :is(p, li, dd) { + text-wrap: pretty; +} +.sl-markdown-content strong { + color: var(--ds-text); + font-weight: 500; +} + +.sl-markdown-content a:not(:where(.not-content *)) { + color: var(--ds-accent-text); + text-decoration-thickness: 1px; + text-underline-offset: 0.18em; + text-decoration-color: color-mix(in srgb, currentColor 45%, transparent); + transition: text-decoration-color var(--ds-duration) ease; +} +.sl-markdown-content a:not(:where(.not-content *)):hover { + text-decoration-color: currentColor; +} + +/* Lists: 6px octagon ticks in the accent. Ordered lists keep their numbers. */ +.sl-markdown-content ul:not([class]):not(:where(.not-content *)) { + list-style: none; + padding-inline-start: 1.25rem; +} +.sl-markdown-content ul:not([class]):not(:where(.not-content *)) > li { + position: relative; +} +.sl-markdown-content ul:not([class]):not(:where(.not-content *)) > li::before { + content: ''; + position: absolute; + inset-inline-start: -1.1rem; + top: 0.66em; + width: 6px; + height: 6px; + background: var(--ds-accent); + clip-path: polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%); +} +.sl-markdown-content ol:not([class]) > li::marker { + color: var(--ds-text-3); + font-family: var(--ds-font-mono); + font-size: 0.85em; +} + +.sl-markdown-content hr { + border-color: var(--ds-line); +} + +.sl-markdown-content blockquote { + border-inline-start: 1px solid var(--ds-line-strong); + color: var(--ds-text-2); +} + +/* Inline code is evidence: mono, a hairline chip, never coloured by kind. */ +.sl-markdown-content :not(pre) > code:not(:where(.not-content *)), +.sl-markdown-content code:not(:where(pre *, .not-content *)) { + font-family: var(--ds-font-mono); + font-size: 0.8em; + color: var(--ds-code-text); + background: var(--ds-control); + border: 1px solid var(--ds-line-soft); + border-radius: var(--ds-cut-badge); + corner-shape: bevel; + padding: 0.05em 0.3em; + overflow-wrap: anywhere; +} +.sl-markdown-content a code:not(:where(pre *, .not-content *)) { + color: inherit; +} + +kbd { + font-family: var(--ds-font-mono); +} + +/* ---- Tables -------------------------------------------------------------- + Plain tables on the canvas: a quiet head under a rule, body rows divided by + the softest hairline, no zebra, no side borders. */ +.sl-markdown-content table:not(:where(.not-content *)) { + border-collapse: collapse; + font-size: 0.9375rem; + line-height: 1.5; +} +/* Every table spans the column, so tables line up with each other and with + the rules between sections. A table with more columns than the column has + room for pans inside its box (the `.ds-table` wrapper every markdown table + gets, from rehype-table-scroll) rather than running under the contents + list, and it pans before its cells get too narrow to read: each column is + owed about 5rem — 4rem from eight columns up, where they are mostly + figures — so a table of many columns keeps a floor of its own, short of + the column on a 1440px display. + Text wraps between words, and identifiers and digests only at the break + points the plugin gives them — after `_ - / . :`, and in even pieces of + at most sixteen characters of a hash — never mid-token (inline code + elsewhere may break anywhere; in a cell that let the column shrink below + its longest token and split `v0.2.0`). Numbers, the right-aligned + cells, never break; their headers wrap over them. */ +.sl-markdown-content .ds-table { + overflow-x: auto; +} +.sl-markdown-content .ds-table > table { + display: table; + width: 100%; + overflow: visible; +} +.sl-markdown-content .ds-table > table:has(tr > :nth-child(4)) { min-width: 20rem; } +.sl-markdown-content .ds-table > table:has(tr > :nth-child(5)) { min-width: 25rem; } +.sl-markdown-content .ds-table > table:has(tr > :nth-child(6)) { min-width: 30rem; } +.sl-markdown-content .ds-table > table:has(tr > :nth-child(7)) { min-width: 35rem; } +.sl-markdown-content .ds-table > table:has(tr > :nth-child(8)) { min-width: 36rem; } +.sl-markdown-content .ds-table > table:has(tr > :nth-child(10)) { min-width: 42rem; } +.sl-markdown-content .ds-table > table:has(tr > :nth-child(12)) { min-width: 48rem; } +.sl-markdown-content .ds-table :is(td, th), +.sl-markdown-content .ds-table :is(td, th) code { + overflow-wrap: normal; +} +.sl-markdown-content .ds-table td code { + white-space: normal; +} +.sl-markdown-content .ds-table td[align='right'] { + white-space: nowrap; +} +.sl-markdown-content :is(th, td):not(:where(.not-content *)) { + border: 0; + border-bottom: 1px solid var(--ds-line-soft); + padding: 0.55rem 1.1rem 0.55rem 0; + background: transparent; + vertical-align: top; +} +.sl-markdown-content :is(th, td):last-child:not(:where(.not-content *)) { + padding-inline-end: 0; +} +/* Tables of many columns are mostly short figures: a tighter gap between + them keeps a twelve-column table inside a 1440px display. */ +.sl-markdown-content .ds-table > table:has(tr > :nth-child(8)) :is(th, td):not(:last-child) { + padding-inline-end: 0.7rem; +} +.sl-markdown-content thead th:not(:where(.not-content *)) { + border-bottom: 1px solid var(--ds-line); + color: var(--ds-text-3); + font-size: var(--sl-text-xs); + font-weight: 500; + text-wrap: balance; +} +.sl-markdown-content tbody th:not(:where(.not-content *)) { + color: var(--ds-text); + font-weight: 500; +} +.sl-markdown-content tbody tr:not(:where(.not-content *)) { + background: transparent; +} +.sl-markdown-content td:not(:where(.not-content *)) { + color: var(--ds-text-2); +} + +/* ---- Code blocks (Expressive Code) ---------------------------------------- */ +.expressive-code { + --ec-brdRad: 0px; + font-family: var(--ds-font-mono); +} +.expressive-code .frame, +.expressive-code figure { + border-radius: var(--ds-cut-control); + corner-shape: bevel; +} +.expressive-code pre { + border-color: var(--ds-line) !important; + background: var(--ds-well) !important; +} +.expressive-code .frame.has-title .header, +.expressive-code .frame.is-terminal .header { + background: var(--ds-surface) !important; + border-color: var(--ds-line) !important; +} +.expressive-code .frame .header::before { + border-color: var(--ds-line) !important; +} +.expressive-code .copy button { + border-radius: var(--ds-cut-badge); + corner-shape: bevel; +} + +/* ---- Asides -------------------------------------------------------------- + A notice, not a banner: a quiet panel with a hairline, the aside's kind in + its title, and the colour kept to a 2px rule on the leading edge. */ +.starlight-aside { + border: 1px solid var(--ds-line); + border-inline-start: 2px solid var(--sl-color-asides-border, var(--ds-accent)); + border-radius: 0; + background: var(--ds-surface); + padding: 1rem 1.25rem; +} +.starlight-aside--note { + --sl-color-asides-border: var(--ds-accent); + --sl-color-asides-text-accent: var(--ds-accent-text); +} +.starlight-aside__title { + font-weight: 500; + font-size: var(--sl-text-sm); + letter-spacing: 0; +} +.starlight-aside__content { + color: var(--ds-text-2); +} + +/* ---- Details --------------------------------------------------------------- + Native disclosures in running content read as an accordion row: the summary + in text, a plus that turns into a cross when open. Components that style + their own `details` carry a class and are left alone. */ +.sl-markdown-content details:not([class]) { + border-top: 1px solid var(--ds-line); + border-bottom: 1px solid var(--ds-line); + padding: 0; +} +.sl-markdown-content details:not([class]) + details:not([class]) { + margin-top: 0; + border-top: 0; +} +/* Starlight's own disclosure look (a side rule and a chevron) gives way to + the hairline row for every disclosure in content, classed or not. */ +.sl-markdown-content details:not(:where(.not-content *)) { + border-inline-start: 0; + padding-inline-start: 0; +} +.sl-markdown-content summary:not(:where(.not-content *)) { + margin-inline-start: 0; + padding-inline-start: 0; +} +.sl-markdown-content summary:not(:where(.not-content *))::before { + content: none; +} +.sl-markdown-content details:not([class]) > summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 0; + cursor: pointer; + list-style: none; + color: var(--ds-text); + font-weight: 500; +} +.sl-markdown-content details:not([class]) > summary::-webkit-details-marker { + display: none; +} +.sl-markdown-content details:not([class]) > summary::after { + content: '+'; + flex: none; + font-family: var(--ds-font-mono); + font-size: 1.1rem; + line-height: 1; + color: var(--ds-text-3); + transition: transform var(--ds-duration) ease; +} +.sl-markdown-content details:not([class])[open] > summary::after { + transform: rotate(45deg); +} +.sl-markdown-content details:not([class]) > summary:hover { + color: var(--ds-accent-text); +} +.sl-markdown-content details:not([class]) > :not(summary) { + margin-bottom: 1rem; +} + +/* ---- Cards, badges, tabs (Starlight's components) ------------------------- */ +.card { + border: 1px solid var(--ds-line); + border-radius: var(--ds-cut-panel); + corner-shape: bevel; + background: var(--ds-surface); + box-shadow: none; +} +.card .icon { + border-radius: 0; +} +.sl-badge { + border-radius: var(--ds-cut-badge); + corner-shape: bevel; + font-family: var(--ds-font); + font-weight: 500; +} +starlight-tabs [role='tablist'] { + border-bottom-color: var(--ds-line); +} +starlight-tabs [role='tab'][aria-selected='true'] { + color: var(--ds-text); + border-color: var(--ds-accent); +} + +/* ---- Pagination ------------------------------------------------------------ + Previous and next take slants: each keeps the corners on its outer side + square and cuts the two that point the way it goes, so the pair reads as a + pair of arrows. */ +.pagination-links a { + border: 1px solid var(--ds-line-strong); + background: var(--ds-control); + box-shadow: none; + color: var(--ds-text-3); + font-size: var(--sl-text-sm); + transition: + border-color var(--ds-duration) ease, + color var(--ds-duration) ease; +} +.pagination-links a[rel='prev'] { + border-radius: var(--ds-cut-panel) 0 0 var(--ds-cut-panel); + corner-shape: bevel; +} +.pagination-links a[rel='next'] { + border-radius: 0 var(--ds-cut-panel) var(--ds-cut-panel) 0; + corner-shape: bevel; +} +.pagination-links a:hover { + border-color: var(--ds-accent-line); + color: var(--ds-text-2); +} +.pagination-links .link-title { + font-size: var(--sl-text-xl); + font-weight: 500; + letter-spacing: -0.02em; + color: var(--ds-text); +} + +/* ---- Footer ---------------------------------------------------------------- */ +footer .meta { + color: var(--ds-text-3); + font-size: var(--sl-text-sm); +} +footer .meta a { + color: var(--ds-text-3); +} +footer .meta a:hover { + color: var(--ds-text); +} + +/* ========================================================================== + Shared component vocabulary + -------------------------------------------------------------------------- + Classes the sites' own components compose. Component-scoped styles may add + to them, but the look of a label, a panel, a chip or a figure is decided + here once. + ========================================================================== */ + +/* A small label above a block: sentence case, never uppercase or tracked. */ +.ds-label { + margin: 0; + font-size: var(--sl-text-xs); + font-weight: 500; + line-height: 1.4; + letter-spacing: 0; + color: var(--ds-text-3); +} + +/* A panel: the one surface that sits on the canvas with its own ground. */ +.ds-panel { + border: 1px solid var(--ds-line-strong); + border-radius: var(--ds-cut-panel); + corner-shape: bevel; + background: var(--ds-surface); + box-shadow: var(--ds-shadow-panel); +} + +/* A quieter frame for a figure or a card within a grid. */ +.ds-frame { + border: 1px solid var(--ds-line); + border-radius: var(--ds-cut-control); + corner-shape: bevel; + background: var(--ds-surface); +} + +/* A chip: a toggle, a filter, an in-page link. */ +.ds-chip { + display: inline-flex; + align-items: center; + gap: 0.4rem; + min-height: 2.25rem; + padding: 0 0.8rem; + border: 1px solid var(--ds-line-strong); + border-radius: var(--ds-cut-lead) var(--ds-cut-chip) var(--ds-cut-chip) + var(--ds-cut-chip); + corner-shape: bevel; + background: var(--ds-control); + color: var(--ds-text-2); + font-size: var(--sl-text-sm); + font-weight: 500; + line-height: 1; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + transition: + border-color var(--ds-duration) ease, + color var(--ds-duration) ease, + background-color var(--ds-duration) ease; +} +.ds-chip:hover { + border-color: var(--ds-accent-line); + color: var(--ds-text); +} +.ds-chip[aria-pressed='true'], +.ds-chip[aria-current='true'], +.ds-chip.is-selected { + border-color: var(--ds-accent); + background: var(--ds-accent-low); + color: var(--ds-text); +} + +/* A small quiet button, e.g. "Confusion matrix". */ +.ds-button { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.7rem; + border: 1px solid var(--ds-line-strong); + border-radius: var(--ds-cut-lead) var(--ds-cut-badge) var(--ds-cut-badge) + var(--ds-cut-badge); + corner-shape: bevel; + background: var(--ds-control); + color: var(--ds-text-2); + font-family: var(--ds-font); + font-size: var(--sl-text-xs); + font-weight: 500; + line-height: 1.2; + letter-spacing: 0; + text-transform: none; + white-space: nowrap; + cursor: pointer; + transition: + border-color var(--ds-duration) ease, + color var(--ds-duration) ease; +} +.ds-button:hover { + border-color: var(--ds-accent); + color: var(--ds-text); +} + +/* Numbers that sit in rows or columns. */ +.ds-num { + font-family: var(--ds-font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; +} + +/* A statement: the one sentence a page exists to say, set larger than the + prose around it and marked by a rule rather than boxed. */ +.ds-statement { + margin: var(--ds-space-lg) 0 0; + max-width: var(--ds-measure); + padding-inline-start: var(--ds-space-lg); + border-inline-start: 2px solid var(--ds-accent); + font-size: clamp(1.1875rem, 1.05rem + 0.45vw, 1.375rem); + font-weight: 500; + line-height: 1.4; + letter-spacing: -0.015em; + color: var(--ds-text); + text-wrap: pretty; +} +.ds-statement strong { + font-weight: 500; + white-space: nowrap; + color: var(--ds-accent-text); +} + +/* Figures: a row of numbers, each above what it counts, divided by hairlines. + Written as a
    of
    ; the number is shown first. The + column count comes from `--ds-figures-cols` (3 by default), and a row of + four folds to two by two on a narrow screen. */ +.ds-figures { + --ds-figures-cols: 3; + display: grid; + grid-template-columns: repeat(var(--ds-figures-cols), minmax(0, 1fr)); + margin: var(--ds-space-lg) 0 0; + padding: 0; + border-block: 1px solid var(--ds-line); +} +.ds-figures > div { + display: flex; + flex-direction: column; + gap: var(--ds-space-2xs); + min-width: 0; + margin: 0 !important; + padding: var(--ds-space-md) var(--ds-space-md) var(--ds-space-md) 0; +} +.ds-figures > div + div { + padding-inline-start: var(--ds-space-md); + border-inline-start: 1px solid var(--ds-line-soft); +} +.ds-figures :is(dt, dd) { + margin: 0 !important; + padding: 0; +} +.ds-figures dt { + font-size: var(--sl-text-sm); + font-weight: 400; + line-height: 1.4; + color: var(--ds-text-2); +} +.ds-figures dd { + order: -1; + font-family: var(--ds-font-mono); + font-size: 1.75rem; + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--ds-text); +} +.ds-figures .ds-figure-unit { + margin-inline-start: 0.35em; + font-family: var(--ds-font); + font-size: var(--sl-text-sm); + font-weight: 400; + letter-spacing: 0; + color: var(--ds-text-3); +} +/* A second
    under the label: provenance, in small type. */ +.ds-figures dd.ds-figure-meta { + order: 0; + font-family: var(--ds-font); + font-size: var(--sl-text-xs); + font-weight: 400; + line-height: 1.4; + letter-spacing: 0; + color: var(--ds-text-3); +} +.ds-figures[data-cols='4'] { + --ds-figures-cols: 4; +} +@media (max-width: 40rem) { + .ds-figures[data-cols='4'] { + --ds-figures-cols: 2; + } + .ds-figures[data-cols='4'] > div:nth-child(odd) { + padding-inline-start: 0; + border-inline-start: 0; + } + .ds-figures[data-cols='4'] > div:nth-child(n + 3) { + border-top: 1px solid var(--ds-line-soft); + } +} +/* On a phone a row of three reads as a list: each number beside its label. */ +@media (max-width: 36rem) { + .ds-figures:not(.is-compact, [data-cols='4']) { + grid-template-columns: minmax(0, 1fr); + } + .ds-figures:not(.is-compact, [data-cols='4']) > div { + display: grid; + grid-template-columns: 6rem minmax(0, 1fr); + column-gap: var(--ds-space-sm); + align-items: baseline; + padding: var(--ds-space-sm) 0; + border-inline-start: 0; + } + .ds-figures:not(.is-compact, [data-cols='4']) > div + div { + padding-inline-start: 0; + border-inline-start: 0; + border-top: 1px solid var(--ds-line-soft); + } + .ds-figures:not(.is-compact, [data-cols='4']) dd:not(.ds-figure-meta) { + grid-column: 1; + grid-row: 1 / span 2; + font-size: 1.5rem; + } + .ds-figures:not(.is-compact, [data-cols='4']) dt, + .ds-figures:not(.is-compact, [data-cols='4']) .ds-figure-meta { + grid-column: 2; + } + .ds-figures:not(.is-compact, [data-cols='4']) .ds-figure-unit { + display: block; + margin: 0; + font-size: var(--sl-text-xs); + } +} +/* Inside a card: smaller numbers, no closing rule, and the card's own inset. */ +.ds-figures.is-compact { + margin-top: var(--ds-space-lg); + border-top-color: var(--ds-line-soft); + border-bottom: 0; +} +.ds-figures.is-compact > div { + padding-block: var(--ds-space-sm) 0; + padding-inline-end: var(--ds-space-sm); +} +.ds-figures.is-compact > div + div { + padding-inline-start: var(--ds-space-sm); +} +.ds-figures.is-compact dt { + font-size: var(--sl-text-xs); + color: var(--ds-text-3); +} +.ds-figures.is-compact dd { + font-size: 1.125rem; +} + +/* A card in a grid: a name, one line of identity under it, then its body. + The head has no rule under it; spacing separates it. */ +.ds-card { + display: flex; + flex-direction: column; + min-width: 0; + margin: 0 !important; + padding: var(--ds-space-md) var(--ds-card-pad) var(--ds-card-pad); + border: 1px solid var(--ds-line); + border-radius: var(--ds-cut-control); + corner-shape: bevel; + background: var(--ds-surface); +} +.ds-card > * { + margin-top: 0; +} +.ds-card-head { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--ds-space-2xs); + margin: 0 0 var(--ds-space-md) !important; +} +.ds-card-name { + display: flex; + align-items: center; + gap: var(--ds-space-xs); + margin: 0 !important; + font-size: var(--sl-text-base); + font-weight: 500; + line-height: 1.3; + letter-spacing: -0.01em; + color: var(--ds-text); +} +.ds-card-meta { + margin: 0 !important; + overflow: hidden; + font-family: var(--ds-font-mono); + font-size: var(--sl-text-2xs); + line-height: 1.4; + color: var(--ds-text-3); + white-space: nowrap; + text-overflow: ellipsis; +} + +/* Bars: label, track, value, on one grid so every row in a card — and every + card in a grid — starts its track at the same place. */ +.ds-bars { + display: grid; + grid-template-columns: var(--ds-bars-label, 7rem) minmax(0, 1fr) auto; + align-items: center; + gap: var(--ds-space-xs) var(--ds-space-sm); + margin: 0; +} +.ds-bars > * { + margin: 0 !important; +} +.ds-bars-label { + overflow: hidden; + font-size: var(--sl-text-sm); + color: var(--ds-text-2); + white-space: nowrap; + text-overflow: ellipsis; +} +.ds-bars-value { + min-width: 3.25rem; + font-family: var(--ds-font-mono); + font-size: var(--sl-text-xs); + font-variant-numeric: tabular-nums; + text-align: end; + white-space: nowrap; + color: var(--ds-text); +} +.ds-bars-value small { + font-size: 1em; + color: var(--ds-text-3); +} +.ds-track { + display: flex; + height: 0.5rem; + overflow: hidden; + background: var(--ds-control); + box-shadow: inset 0 0 0 1px var(--ds-line-soft); +} +.ds-track > * { + height: 100%; +} + +/* A split: two things read side by side once the column has room for both + at a readable width — a passage and the figures it introduces, or two + independent paragraphs. Each keeps its own reading order; nothing flows from + one column into the other. Below the breakpoint they simply stack. */ +@media (min-width: 100rem) { + .ds-split { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + align-items: start; + gap: var(--ds-space-lg) var(--ds-space-xl); + } + .ds-split > * { + margin-top: 0 !important; + } + .ds-split > :is(p, ul, ol) + :is(p, ul, ol) { + max-width: var(--ds-measure); + } + /* In the second column a row of four figures folds to two by two. */ + .ds-split .ds-figures[data-cols='4'] { + --ds-figures-cols: 2; + } + .ds-split .ds-figures[data-cols='4'] > div:nth-child(odd) { + padding-inline-start: 0; + border-inline-start: 0; + } + .ds-split .ds-figures[data-cols='4'] > div:nth-child(n + 3) { + border-top: 1px solid var(--ds-line-soft); + } +} + +/* A figure caption. */ +.ds-caption { + margin: var(--ds-space-sm) 0 0; + max-width: var(--ds-measure); + color: var(--ds-text-3); + font-size: var(--sl-text-sm); + line-height: 1.55; +} + +/* ---- Motion ------------------------------------------------------------------ */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition-duration: 0s !important; + animation-duration: 0s !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + } +} + +/* ---- Print ---------------------------------------------------------------- */ +@media print { + .ds-panel, + .ds-frame { + box-shadow: none; + } +}