Project KPI Dashboard (Arabic)
Those are this report's own sheets, printed by the engine from the template and the payload below — not screenshots kept beside them.
The source
This is the whole thing — the template, the pages that print from it, and the stylesheet
they are dressed in, as HTML with {{ }} bindings. Nothing is generated from a
designer format; what you see here is what the engine renders.
template.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!--
Two files at the same kind of path, from two different folders. The typeface is this report's
own — it is the only Arabic report in the project — and is found in the folder beside this
template. The house style is the project's: the report's own folder is searched first and does
not hold it, so it falls through to shared-assets, where it is held once for every report.
-->
<link rel="stylesheet" href="assets/css/fonts.css">
<link rel="stylesheet" href="assets/css/house.css">
<!-- This report's own, over the top of the house style. -->
<link rel="stylesheet" href="assets/css/report.css">
<!-- Shared as well: one copy of the chart library for every report in the project that draws. -->
<script src="assets/js/vendor/amcharts5-5.14.4.js"></script>
<script src="assets/js/charts.js"></script>
</head>
<body>
<script id="report-data" type="application/json">{{json}}</script>
<!-- Placed in the page and fixed to the top, so it is on both sheets and keeps the report's
stylesheet, its direction and its logo. -->
<template id="report-header">
<div class="masthead">
<img class="logo" src="assets/images/logo.svg" alt="">
<span class="titles">
<span class="org">{{report.org}}</span>
<span class="subject">{{report.subject}}</span>
</span>
<span class="meta">
<span class="period">{{report.period}}</span>
<span class="classification">{{report.classification}}</span>
</span>
</div>
</template>
<!-- Drawn by the browser, which is the only thing that can count sheets. It loads nothing, so no
webfont reaches it — hence figures and a code, which read the same in any face. -->
<template id="report-footer">
<div class="pagefoot" dir="ltr">
<span><span class="pageNumber"></span> / <span class="totalPages"></span></span>
<span>{{report.code}}</span>
</div>
</template>
</body>
</html>
page1.html
<div class="sheet">
<header class="pagehead">
<div>
<h1>{{report.title}}</h1>
<p class="lede">{{report.lede}}</p>
</div>
<span class="stamp">صدر في {{date report.issuedOn "d MMMM yyyy" "ar-AE"}}</span>
</header>
<section class="kpis">
{{#each kpis}}
<article class="kpi kpi--{{tone}}">
<span class="kpi-label">{{label}}</span>
<span class="kpi-figure">
<span class="value">{{number value format "ar-AE"}}</span>
<span class="unit">{{unit}}</span>
</span>
<span class="kpi-delta">
<span class="chip chip--{{tone}}">
{{#if (eq direction "up")}}▲{{else}}▼{{/if}} {{delta}}
</span>
<span class="caption">{{caption}}</span>
</span>
<div class="meter meter--{{tone}}"><span style="width: {{meter}}%"></span></div>
<span class="kpi-target">{{meterNote}}</span>
</article>
{{/each}}
</section>
<div class="panels">
<section class="card">
<div class="card-head">
<h2>{{charts.status.title}}</h2>
<span class="note">{{charts.status.note}}</span>
</div>
<div class="chart chart--status" id="chart-status"></div>
</section>
<section class="card">
<div class="card-head">
<h2>{{charts.quarters.title}}</h2>
<span class="note">{{charts.quarters.note}}</span>
</div>
<div class="chart chart--quarters" id="chart-quarters"></div>
</section>
</div>
<section class="card">
<div class="card-head">
<h2>{{charts.progress.title}}</h2>
<span class="note">{{charts.progress.note}}</span>
</div>
<div class="chart chart--progress" id="chart-progress"></div>
</section>
<section class="card">
<div class="card-head">
<h2>{{highlights.title}}</h2>
<span class="note">{{highlights.note}}</span>
</div>
<div class="highlights">
{{#each highlights.rows}}
<article class="highlight highlight--{{tone}}">
<span class="what">{{title}}</span>
<span class="why">{{note}}</span>
</article>
{{/each}}
</div>
</section>
<p class="footnote">{{report.footnote}}</p>
</div>
page2.html
<div class="sheet">
<header class="pagehead">
<div>
<h1>تفاصيل المحفظة</h1>
<p class="lede">أكبر المشاريع بالميزانية، والتصعيدات والمعالم التي تتابعها اللجنة هذا الربع.</p>
</div>
<span class="stamp">{{report.period}}</span>
</header>
<section class="card">
<div class="card-head">
<h2>{{charts.sectors.title}}</h2>
<span class="note">{{charts.sectors.note}}</span>
</div>
<div class="chart chart--sectors" id="chart-sectors"></div>
</section>
<section class="card">
<div class="card-head">
<h2>{{portfolio.title}}</h2>
<span class="note">{{portfolio.note}}</span>
</div>
<table class="grid portfolio">
<thead>
<tr>
<th class="col-project">المشروع</th>
<th class="col-sector">القطاع</th>
<th class="col-money">الميزانية</th>
<th class="col-money">المنصرف</th>
<th class="col-progress">الإنجاز</th>
<th class="col-status">الحالة</th>
</tr>
</thead>
<tbody>
{{#each portfolio.rows}}
<tr>
<td class="project">
<span class="title">{{name}}</span>
<!-- Isolated: a Latin code inside an Arabic line leaves the algorithm to guess where its
dash belongs, and bdi is what stops it guessing. -->
<bdi class="code">{{code}}</bdi>
</td>
<td class="sector">{{sector}}</td>
<td class="num">{{number budget "N0" "ar-AE"}}</td>
<td class="num">{{number spent "N0" "ar-AE"}}</td>
<td>
<div class="progress">
<div class="meter meter--{{tone}}"><span style="width: {{completion}}%"></span></div>
<span class="pct">{{completion}}%</span>
</div>
</td>
<td><span class="pill pill--{{tone}}">{{status}}</span></td>
</tr>
{{/each}}
</tbody>
</table>
</section>
<div class="panels">
<section class="card">
<div class="card-head">
<h2>{{risks.title}}</h2>
<span class="note">{{risks.note}}</span>
</div>
<ul class="risks">
{{#each risks.rows}}
<li>
<span class="what">{{title}}</span>
<span class="who">{{owner}} · المعالجة قبل {{due}}</span>
<span class="chip chip--{{tone}}">{{severity}}</span>
</li>
{{/each}}
</ul>
</section>
<section class="card card--fill">
<div class="card-head">
<h2>{{milestones.title}}</h2>
<span class="note">{{milestones.note}}</span>
</div>
<ol class="milestones">
{{#each milestones.rows}}
<li class="ms--{{tone}}">
<span class="when">{{when}}</span>
<span class="what">{{title}}</span>
<span class="pill pill--{{tone}}">{{state}}</span>
</li>
{{/each}}
</ol>
</section>
</div>
<p class="footnote">{{report.footnote}}</p>
</div>
assets/css/fonts.css
/*
IBM Plex Sans Arabic, split by script and by weight so a sheet of Arabic pulls the Arabic file and
nothing else. The unicode-range on each face is what decides which.
This report's own, not the project's. It is the only report here whose contents are Arabic, and a
typeface in shared-assets is one every report in the project carries — six files and a family
name in the house style, for seven reports that print in Latin. Put it back in shared-assets the
day a second Arabic report exists.
It has to be carried. A render blocks every host but the report's own, and Chromium has no fallback
to reach for in that state — an Arabic page with no font in the folder prints as boxes.
*/
@font-face
{
font-family: 'IBM Plex Sans Arabic';
font-style: normal;
font-weight: 400;
src: url('../fonts/ibm-plex-sans-arabic-400-arabic.woff2') format('woff2');
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face
{
font-family: 'IBM Plex Sans Arabic';
font-style: normal;
font-weight: 400;
src: url('../fonts/ibm-plex-sans-arabic-400-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face
{
font-family: 'IBM Plex Sans Arabic';
font-style: normal;
font-weight: 600;
src: url('../fonts/ibm-plex-sans-arabic-600-arabic.woff2') format('woff2');
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face
{
font-family: 'IBM Plex Sans Arabic';
font-style: normal;
font-weight: 600;
src: url('../fonts/ibm-plex-sans-arabic-600-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face
{
font-family: 'IBM Plex Sans Arabic';
font-style: normal;
font-weight: 700;
src: url('../fonts/ibm-plex-sans-arabic-700-arabic.woff2') format('woff2');
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
}
@font-face
{
font-family: 'IBM Plex Sans Arabic';
font-style: normal;
font-weight: 700;
src: url('../fonts/ibm-plex-sans-arabic-700-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
assets/css/report.css
/*
This report's own layout, over the project's house style.
What is here is the composition — how many tiles across, how tall a chart is, what the table's
columns are worth. The palette, the type, the sheet and the parts those are assembled from come
from assets/css/house.css, which is the project's and lives in shared-assets.
Everything is laid out with logical properties. The report is right-to-left, and the engine sets
that on <html> from the report's own setting — so start is the right edge here, and nothing below
has to know that.
*/
/*
The two softer inks, darker here than the house style sets them. The greys sit well enough on a
sheet, but Word draws the text at its own scale and thins the strokes further — the labels and
the notes come out looking blurred. Darker ink holds them together. Only this report is moved;
house.css is the project's and is left as it is.
*/
:root
{
--ink-soft: #000000;
--ink-faint: #000000;
}
/*
The one report here whose contents are Arabic, so the one that sets the Arabic face. The faces
are in this report's own assets/css/fonts.css; nothing about Arabic is in shared-assets, because
a report that prints in Latin should neither name the family nor carry the files.
*/
body
{
font-family: "IBM Plex Sans Arabic", "Segoe UI", Tahoma, sans-serif;
}
/* ---------- the figures across the top ---------- */
.kpis
{
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 11px;
margin-top: 14px;
}
/*
A tile is a column: label, figure, movement, then the measure against target. Fixed order rather
than a free-form card, because four of them are read across and the eye should not have to find
each part again in the next one.
*/
.kpi
{
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
padding: 11px 12px 10px;
background: var(--panel);
border: 1px solid var(--rule);
border-radius: var(--radius);
overflow: hidden;
}
/* The tone as an edge rather than a wash: it marks the tile without tinting the figure inside it. */
.kpi::before
{
content: "";
position: absolute;
inset-block-start: 0;
inset-inline: 0;
height: 3px;
background: var(--accent);
}
.kpi--good::before { background: var(--good); }
.kpi--warn::before { background: var(--warn); }
.kpi--bad::before { background: var(--bad); }
.kpi--idle::before { background: var(--idle); }
.kpi-label
{
color: var(--ink-soft);
font-size: 10px;
font-weight: 600;
}
.kpi-figure
{
display: flex;
align-items: baseline;
gap: 5px;
margin-top: -1px;
}
.kpi-figure .value
{
font-size: 23px;
font-weight: 700;
line-height: 1.15;
letter-spacing: -.02em;
}
.kpi-figure .unit
{
color: var(--ink-faint);
font-size: 9.5px;
}
.kpi-delta
{
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.kpi-delta .caption
{
color: var(--ink-faint);
font-size: 9px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.kpi .meter { margin-top: 3px; }
.kpi-target
{
color: var(--ink-faint);
font-size: 9px;
}
/* ---------- the charts ---------- */
/* Two panels of equal width, and the same grid does the pair of panels on the second sheet. */
.panels
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 12px;
}
.panels + .card,
.card + .card,
.card + .panels { margin-top: 12px; }
.chart--status { height: 208px; }
.chart--quarters { height: 208px; }
.chart--progress { height: 232px; }
.chart--sectors { height: 150px; }
/* ---------- what the sheet is saying ---------- */
/*
Three readings of the figures above, in the words somebody would use at the meeting. Last on the
sheet rather than first: they are the conclusion, and a reader who disagrees has the charts above
to argue with.
*/
.highlights
{
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.highlight
{
display: flex;
flex-direction: column;
gap: 2px;
padding-inline-start: 10px;
border-inline-start: 3px solid var(--idle);
}
.highlight--good { border-inline-start-color: var(--good); }
.highlight--warn { border-inline-start-color: var(--warn); }
.highlight--bad { border-inline-start-color: var(--bad); }
.highlight .what
{
font-size: 11px;
font-weight: 700;
}
.highlight .why
{
color: var(--ink-soft);
font-size: 9.5px;
line-height: 1.6;
}
/* ---------- the table of projects ---------- */
/*
Fixed widths, because table-layout is fixed and the browser is therefore not sizing these from
their content. The project column takes what the rest do not: it is the one holding a name that
can run long, and the only one that may wrap.
*/
.portfolio .col-project { width: auto; }
.portfolio .col-sector { width: 108px; }
.portfolio .col-money { width: 74px; text-align: end; }
.portfolio .col-progress { width: 104px; }
.portfolio .col-status { width: 88px; }
.portfolio .project
{
display: flex;
align-items: baseline;
gap: 7px;
}
.portfolio .project .title { font-weight: 600; }
.portfolio .project .code
{
color: var(--ink-faint);
font-size: 9px;
letter-spacing: .02em;
white-space: nowrap;
}
.portfolio .sector
{
color: var(--ink-soft);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* The bar carries the reading and the figure confirms it, so the figure is small and out of the way. */
.progress
{
display: flex;
align-items: center;
gap: 7px;
}
.progress .meter { flex: 1 1 auto; }
.progress .pct
{
flex: 0 0 auto;
width: 28px;
text-align: end;
color: var(--ink-soft);
font-size: 9.5px;
font-weight: 600;
}
/* ---------- escalations ---------- */
.risks
{
margin: 0;
padding: 0;
list-style: none;
}
.risks li
{
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 2px 8px;
padding: 6px 0;
border-bottom: 1px solid var(--rule-soft);
}
.risks li:last-child { border-bottom: 0; }
.risks .what
{
font-size: 10.5px;
font-weight: 600;
}
.risks .who
{
grid-column: 1;
color: var(--ink-faint);
font-size: 9px;
}
/* Spans both rows of its own item, so the chip sits against the pair rather than beside the top line. */
.risks .chip
{
grid-column: 2;
grid-row: 1 / span 2;
}
/* ---------- the year's milestones ---------- */
/*
A rail with a dot per item. The rail is a border on the list and the dots sit on top of it, so
neither is a picture to load and both stay sharp at whatever the composer rasterises to.
*/
/*
The pair of panels are the same height, because a grid row makes them so, and this is the shorter
of the two. Rather than leave the difference as a gap under the last milestone, the list takes the
height and spreads its items down it.
*/
.card--fill
{
display: flex;
flex-direction: column;
}
.milestones
{
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
margin: 2px 0 0;
padding: 0;
padding-inline-start: 15px;
border-inline-start: 1px solid var(--rule);
list-style: none;
}
.milestones li
{
position: relative;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 1px 8px;
padding: 5px 0;
}
.milestones li::before
{
content: "";
position: absolute;
inset-inline-start: -20px;
top: 11px;
width: 7px;
height: 7px;
border: 1.5px solid var(--idle);
border-radius: 50%;
background: var(--page);
}
.milestones .ms--done::before { border-color: var(--accent); background: var(--accent); }
.milestones .ms--good::before { border-color: var(--good); background: var(--good); }
.milestones .ms--bad::before { border-color: var(--bad); background: var(--bad); }
.milestones .when
{
grid-column: 1;
grid-row: 2;
color: var(--ink-faint);
font-size: 9px;
}
.milestones .what
{
grid-column: 1;
grid-row: 1;
font-size: 10.5px;
font-weight: 600;
}
.milestones .pill
{
grid-column: 2;
grid-row: 1 / span 2;
}
shared-assets/css/house.css
/*
The project's house style: what every report in it looks like before it looks like itself.
It is in shared-assets rather than in a report because it is the project's, not one report's —
the palette, the type, the sheet, the running header, and the handful of parts (card, chip, pill,
meter, table) that every report here is assembled from. A report's own stylesheet composes these
into its layout; it does not redecide what a card is.
A template references it as assets/css/house.css, exactly as it references its own files. The
shared folder is searched behind the report's own, so the path means the same thing in every
report — and a report that needs its own version keeps a file at that path and wins.
*/
:root
{
/* Named for the job, not the hue, so a rebrand is this block and nothing else. */
--ink: #0f172a;
--ink-soft: #475569;
--ink-faint: #6b7a90;
--ink-invert: #ffffff;
--page: #ffffff;
--panel: #ffffff;
--panel-2: #f8fafc;
--rule: #e2e8f0;
--rule-soft: #eef2f7;
--brand: #0b1f3a;
--brand-2: #12325c;
--accent: #1d4ed8;
--accent-soft: #eef3ff;
--cyan: #0ea5e9;
--good: #047857;
--good-soft: #e7f6f0;
--warn: #b45309;
--warn-soft: #fdf3e3;
--bad: #b91c1c;
--bad-soft: #fdeceb;
--idle: #64748b;
--idle-soft: #f1f5f9;
--radius: 7px;
--radius-sm: 4px;
}
body
{
margin: 0;
color: var(--ink);
background: var(--page);
/*
No Arabic face here, and no Arabic font in shared-assets at all. Naming one for every report in
the project meant seven reports asked for a typeface none of them loads, so what they actually
printed in was the second name in the list. The one Arabic report carries the family and the
files itself.
*/
font-family: "Segoe UI", Tahoma, sans-serif;
font-size: 11px;
line-height: 1.55;
/* Every figure in this project is read down a column, so digits are the same width. */
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
}
/*
The sheet.
The engine prints with no page margin, so the edges are the report's own to leave. Cloned decoration
keeps the padding on every sheet a long page runs onto rather than only the first, and the top
padding is what the fixed running header sits in — it reserves no room of its own.
*/
.sheet
{
box-sizing: border-box;
width: 794px;
min-height: 1123px;
padding: 96px 40px 44px;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
/* ---------- the running header, on every sheet ---------- */
/*
The engine fixes #report-page-header to the top of the sheet; everything below is this project's
band inside it. A band rather than a line of text: it is the first thing on every page and the
only thing carrying the mark, and at 76px it is still under a fifteenth of the sheet.
*/
#report-page-header
{
height: 76px;
background: linear-gradient(105deg, var(--brand) 0%, var(--brand-2) 62%, #17416f 100%);
color: var(--ink-invert);
}
.masthead
{
display: flex;
align-items: center;
gap: 14px;
height: 73px;
padding: 0 40px;
box-sizing: border-box;
}
/* The accent edge, under the band. Three pixels is enough to read as deliberate. */
#report-page-header::after
{
content: "";
display: block;
height: 3px;
background: linear-gradient(to left, var(--accent), var(--cyan) 55%, rgba(14, 165, 233, 0));
}
.masthead .logo
{
width: 30px;
height: 30px;
flex: 0 0 auto;
}
.masthead .titles
{
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.masthead .org
{
font-size: 12.5px;
font-weight: 700;
letter-spacing: -.005em;
}
.masthead .subject
{
font-size: 10.5px;
color: #a9c2e0;
}
.masthead .meta
{
margin-inline-start: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
text-align: end;
white-space: nowrap;
}
.masthead .period
{
font-size: 11px;
font-weight: 600;
}
/* A quiet outline rather than a filled badge: it is a caveat, not a headline. */
.masthead .classification
{
padding: 1px 7px;
border: 1px solid rgba(255, 255, 255, .34);
border-radius: 999px;
font-size: 9px;
color: #cddcee;
}
/* ---------- the page's own heading, under the band ---------- */
.pagehead
{
display: flex;
align-items: flex-end;
gap: 12px;
padding-bottom: 9px;
border-bottom: 1px solid var(--rule);
}
.pagehead h1
{
margin: 0;
font-size: 19px;
font-weight: 700;
letter-spacing: -.015em;
}
.pagehead .lede
{
margin: 3px 0 0;
color: var(--ink-soft);
font-size: 10.5px;
}
.pagehead .stamp
{
margin-inline-start: auto;
color: var(--ink-faint);
font-size: 10px;
white-space: nowrap;
}
/* ---------- parts ---------- */
.card
{
box-sizing: border-box;
padding: 12px 14px 10px;
background: var(--panel);
border: 1px solid var(--rule);
border-radius: var(--radius);
}
.card-head
{
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 8px;
}
.card h2
{
margin: 0;
font-size: 12.5px;
font-weight: 700;
}
.card .note
{
margin-inline-start: auto;
color: var(--ink-faint);
font-size: 9.5px;
white-space: nowrap;
}
/* A chart draws itself left to right whatever the page does, and its own labels sit inside it. */
.chart
{
width: 100%;
direction: ltr;
}
.chip
{
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 7px;
border-radius: 999px;
font-size: 9.5px;
font-weight: 700;
line-height: 1.7;
white-space: nowrap;
}
.chip--good { background: var(--good-soft); color: var(--good); }
.chip--bad { background: var(--bad-soft); color: var(--bad); }
.chip--warn { background: var(--warn-soft); color: var(--warn); }
.chip--idle { background: var(--idle-soft); color: var(--idle); }
/*
A status, as a word with a dot rather than as a filled block: eight of them down a table column
would otherwise be eight blocks of colour competing with the figures beside them.
*/
.pill
{
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 10px;
font-weight: 600;
white-space: nowrap;
}
.pill::before
{
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex: 0 0 auto;
}
.pill--good { color: var(--good); }
.pill--warn { color: var(--warn); }
.pill--bad { color: var(--bad); }
.pill--idle { color: var(--idle); }
.pill--done { color: var(--accent); }
/*
A proportion, drawn rather than written. Vector, so it stays sharp where a canvas would not — the
small measures are the ones a reader leans in on.
*/
.meter
{
height: 5px;
border-radius: 3px;
background: var(--rule-soft);
overflow: hidden;
}
.meter > span
{
display: block;
height: 100%;
border-radius: 3px;
background: var(--accent);
}
.meter--good > span { background: var(--good); }
.meter--warn > span { background: var(--warn); }
.meter--bad > span { background: var(--bad); }
/* ---------- tables ---------- */
/*
Fixed layout and a header group, which is the pair that keeps a long table on the sheet: without
the first, columns size to their content and the far one runs off the paper; without the second,
a table that paginates leaves the next sheet as unlabelled columns of numbers.
*/
table.grid
{
width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 10.5px;
}
table.grid thead { display: table-header-group; }
table.grid tr { break-inside: avoid; }
/*
Border-box, because a fixed layout takes the width it is given as the whole column: with padding
added on top instead, six columns quietly cost another 96px and the one column left on auto pays
for all of it.
*/
table.grid th,
table.grid td { box-sizing: border-box; }
table.grid th
{
padding: 6px 8px;
background: var(--panel-2);
border-bottom: 1px solid var(--rule);
color: var(--ink-soft);
font-size: 9.5px;
font-weight: 700;
text-align: start;
white-space: nowrap;
}
table.grid td
{
padding: 6px 8px;
border-bottom: 1px solid var(--rule-soft);
vertical-align: middle;
overflow-wrap: break-word;
}
table.grid tbody tr:last-child td { border-bottom: 0; }
.num
{
text-align: end;
font-weight: 600;
}
/* ---------- the foot of the page ---------- */
.footnote
{
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid var(--rule);
color: var(--ink-faint);
font-size: 9.5px;
}
/*
The running footer, which the browser draws rather than the page.
It is a document of its own at print time: this stylesheet is read and sent along with it, so type
and layout arrive, but nothing it would have to fetch does — no webfont, no image, no background.
So it is set in whatever face the renderer has, and kept to figures and a code, which read the same
in any of them.
*/
.pagefoot
{
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 40px;
box-sizing: border-box;
color: #8a97a8;
font-family: "Segoe UI", Arial, sans-serif;
font-size: 8.5px;
letter-spacing: .04em;
}
What it carries
Stylesheets, scripts, images and fonts travel inside the report, so nothing is resolved
from the outside while it renders. Anything under shared-assets/ belongs to
the project rather than to this report — held once for every report in it, and reached
from the template at the same assets/… path as the report's own.