Files
pdf-reporting/report_generator/template/report.css
T
2026-09-24 21:31:37 +02:00

473 lines
9.8 KiB
CSS

/* The print stylesheet of the SAST/SCA report.
*
* Follows the design sample in `sample_report_template/`. Everything is expressed here rather than
* in the template: the template places content, this decides what it looks like, and the two are
* changed for different reasons.
*
* Space Grotesk throughout, with a monospace face for anything a reader might have to type or
* compare character by character -- identifiers, paths, package URLs, CVSS vectors, VEX
* justifications. That distinction is the one typographic rule the whole report rests on.
*/
@font-face {
font-family: "Space Grotesk";
font-weight: 400;
src: url("fonts/SpaceGrotesk-Regular.ttf");
}
@font-face {
font-family: "Space Grotesk";
font-weight: 500;
src: url("fonts/SpaceGrotesk-Medium.ttf");
}
@font-face {
font-family: "Space Grotesk";
font-weight: 700;
src: url("fonts/SpaceGrotesk-Bold.ttf");
}
/* Shipped rather than asked of the host. `monospace` resolves to whatever a machine happens to
have -- DejaVu on one, Andale on another -- and a report whose identifiers change shape between
two machines is a report nobody can compare. Space Mono is the companion face to Space Grotesk. */
@font-face {
font-family: "Space Mono";
font-weight: 400;
src: url("fonts/SpaceMono-Regular.ttf");
}
@font-face {
font-family: "Space Mono";
font-weight: 700;
src: url("fonts/SpaceMono-Bold.ttf");
}
:root {
--ink: #1a1a1a;
--muted: #55585c;
--rule: #1a1a1a;
--border: #c9cbcd;
--header-fill: #e7e8ea;
--label-fill: #f4f5f6;
/* The filtering reason is the one row that argues rather than states, and it is tinted so that
it reads as an aside even when the page is skimmed. */
--aside-fill: #fdf5ea;
--critical: #d0021b;
--high: #e8710a;
--medium: #f0ad20;
--low: #1f9d55;
}
@page {
size: A4;
margin: 22mm 18mm 18mm 18mm;
@top-center {
content: element(page-header);
width: 100%;
vertical-align: bottom;
padding-bottom: 4mm;
}
@bottom-left {
content: string(scan-footer);
font-family: "Space Grotesk", sans-serif;
font-size: 6.5pt;
color: var(--muted);
}
@bottom-center {
content: counter(page);
font-family: "Space Grotesk", sans-serif;
font-size: 6.5pt;
color: var(--muted);
}
@bottom-right {
content: string(date-footer);
font-family: "Space Grotesk", sans-serif;
font-size: 6.5pt;
color: var(--muted);
}
}
html {
font-family: "Space Grotesk", sans-serif;
font-size: 8.5pt;
line-height: 1.45;
color: var(--ink);
}
body {
margin: 0;
}
code,
.mono {
font-family: "Space Mono", monospace;
font-size: 0.93em;
}
/* The cover carries the mark large already; a second, small one above it would only repeat it. */
@page :first {
@top-center {
content: none;
}
}
/* The mark -- the logo and the company name, which the image carries together -- above every page,
ruled off from the content. */
.page-header {
position: running(page-header);
padding-bottom: 1.6mm;
border-bottom: 0.4pt solid var(--border);
}
.page-header img {
height: 5mm;
display: block;
}
/* Carried into the running footer on every page. Set once, on the cover. */
.footer-scan {
string-set: scan-footer content();
}
.footer-date {
string-set: date-footer content();
}
.footer-scan,
.footer-date {
position: absolute;
visibility: hidden;
}
/* --- Cover ------------------------------------------------------------------------------- */
.cover-mark {
text-align: right;
margin-bottom: 42mm;
}
.cover-mark img {
height: 15mm;
}
.cover-title {
font-size: 19pt;
font-weight: 400;
margin: 0 0 9mm 0;
}
/* --- Headings ---------------------------------------------------------------------------- */
/* A section heading and the rule under it are one thing, so the rule belongs to the heading and
cannot be separated from it by a page break. */
h1 {
font-size: 12.5pt;
font-weight: 700;
margin: 12mm 0 4mm 0;
padding-bottom: 1.6mm;
border-bottom: 0.8pt solid var(--rule);
break-after: avoid;
}
h1:first-of-type {
margin-top: 0;
}
h2 {
font-size: 9pt;
font-weight: 700;
margin: 7mm 0 2.5mm 0;
break-after: avoid;
}
h3 {
font-size: 8.5pt;
font-weight: 700;
margin: 5mm 0 2mm 0;
break-after: avoid;
}
p {
margin: 0 0 2.5mm 0;
}
p.note {
color: var(--ink);
max-width: 155mm;
}
/* --- Definition rows --------------------------------------------------------------------- */
/* Label left, value right, on one grid so that every block of them lines up down the page even
when they are separated by headings. */
.facts {
width: 100%;
border-collapse: collapse;
margin-bottom: 3mm;
}
.facts td {
padding: 0.7mm 0;
vertical-align: top;
}
.facts td:first-child {
width: 46mm;
padding-right: 4mm;
}
.facts.wide td:first-child {
width: 62mm;
}
.facts tr.gap td {
padding-top: 4mm;
}
/* --- Ledger tables (severity counts, languages) ------------------------------------------- */
/* Ruled above and below with hairlines between, and a heavier line before the total: the shape a
reader already knows from an invoice, used for the same reason -- the last row is a sum. */
.ledger {
width: 62%;
border-collapse: collapse;
margin-bottom: 4mm;
}
.ledger th,
.ledger td {
padding: 1.4mm 1mm;
border-bottom: 0.4pt solid var(--border);
}
.ledger thead th {
border-bottom: 0.8pt solid var(--rule);
text-align: left;
font-weight: 700;
}
.ledger td:last-child {
text-align: right;
}
.ledger tr.total td {
border-top: 0.8pt solid var(--rule);
border-bottom: none;
font-weight: 700;
}
/* --- Grid tables -------------------------------------------------------------------------- */
.grid {
width: 100%;
border-collapse: collapse;
margin-bottom: 4mm;
/* A table that splits across a page keeps its header on the second half. */
break-inside: auto;
}
.grid th,
.grid td {
border: 0.4pt solid var(--border);
padding: 1.6mm 2mm;
vertical-align: top;
text-align: left;
}
.grid thead th {
background: var(--header-fill);
font-weight: 700;
}
.grid tr.total td {
font-weight: 700;
}
.grid tr,
.facts tr {
break-inside: avoid;
}
/* --- Cards -------------------------------------------------------------------------------- */
/* One finding, as a titled block of label/value rows. Kept on one page where it fits: a card split
down the middle loses the thing that makes it a card. */
.card {
width: 100%;
border-collapse: collapse;
margin-bottom: 3.5mm;
break-inside: avoid;
}
.card caption {
background: var(--header-fill);
border: 0.4pt solid var(--border);
padding: 1.6mm 2mm;
text-align: left;
font-weight: 700;
caption-side: top;
}
.card th,
.card td {
border: 0.4pt solid var(--border);
padding: 1.6mm 2mm;
vertical-align: top;
text-align: left;
}
.card th {
width: 34mm;
background: var(--label-fill);
font-weight: 400;
}
.card tr.aside th,
.card tr.aside td {
background: var(--aside-fill);
}
/* The justification itself, set as the token it is rather than as prose about a token. */
.token {
font-family: "Space Mono", monospace;
font-size: 0.93em;
background: #ffffff;
border: 0.4pt solid var(--border);
border-radius: 1mm;
padding: 0.3mm 1.2mm;
}
/* --- Severity ----------------------------------------------------------------------------- */
.sev {
font-weight: 700;
}
.sev-critical {
color: var(--critical);
}
.sev-high {
color: var(--high);
}
.sev-medium {
color: var(--medium);
}
.sev-low {
color: var(--low);
}
/* --- Distribution bars --------------------------------------------------------------------- */
/* Label, count, then one bar made of stacked severity segments. The bar is scaled against the
largest row rather than against the total, so the shape of the distribution is what is read and
the busiest row fills the width. */
.bars {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5mm;
}
.bars td {
border: 0.4pt solid var(--border);
padding: 1.2mm 2mm;
vertical-align: middle;
}
.bars td.count {
width: 12mm;
text-align: right;
}
.bars td.track {
width: 50%;
padding: 1.2mm 2mm;
}
.bar {
height: 3mm;
width: 100%;
/* The segments are inline-block, so the newlines between them in the template would be
rendered as spaces and push a full-width bar onto a second line. */
font-size: 0;
white-space: nowrap;
}
.bar span {
display: inline-block;
height: 3mm;
vertical-align: top;
}
.seg-critical {
background: var(--critical);
}
.seg-high {
background: var(--high);
}
.seg-medium {
background: var(--medium);
}
.seg-low {
background: var(--low);
}
/* A distribution and its legend travel together. Where the pair does not fit, it moves to the
next page whole rather than leaving a stranded row and an orphaned key behind. */
.block {
break-inside: avoid;
}
.legend {
font-size: 7.5pt;
color: var(--ink);
margin-bottom: 5mm;
}
.legend .key {
display: inline-block;
width: 2.4mm;
height: 2.4mm;
margin-right: 1.2mm;
}
.legend .item {
margin-right: 4mm;
}
.legend .caveat {
color: var(--muted);
}
/* --- Table of contents -------------------------------------------------------------------- */
/* Page numbers are read off where each link lands, so they stay right however long a section runs. */
.toc {
list-style: none;
padding: 0;
margin: 0;
}
.toc li {
padding: 1.4mm 0;
border-bottom: 0.4pt solid var(--border);
}
.toc a {
color: var(--ink);
text-decoration: none;
}
.toc a::after {
content: leader(".") target-counter(attr(href), page);
}