2026-04-13 21:22:24 +01:00
|
|
|
/* SatsChart — Cascade bar chart for KS2 SATs results */
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
2026-04-14 13:52:00 +01:00
|
|
|
gap: 1.75rem;
|
|
|
|
|
padding-top: 0.25rem;
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Individual subject column ── */
|
|
|
|
|
.subjectChart {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subjectName {
|
2026-04-14 13:52:00 +01:00
|
|
|
font-size: 0.65rem;
|
|
|
|
|
font-weight: 700;
|
2026-04-13 21:22:24 +01:00
|
|
|
text-transform: uppercase;
|
2026-04-14 13:52:00 +01:00
|
|
|
letter-spacing: 0.1em;
|
2026-08-06 12:13:04 +01:00
|
|
|
color: var(--text-muted);
|
2026-04-14 13:52:00 +01:00
|
|
|
margin-bottom: 0.75rem;
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chartArea {
|
|
|
|
|
position: relative;
|
|
|
|
|
padding-bottom: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Gridlines ── */
|
|
|
|
|
.gridlines {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 20px;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gridline {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 1px;
|
2026-08-06 12:13:04 +01:00
|
|
|
background: var(--bg-secondary);
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:55:43 +01:00
|
|
|
/* ── Per-bar national average marker ──
|
|
|
|
|
Each bar compares against its own benchmark (expected vs higher standard /
|
|
|
|
|
greater depth), so the marker sits on the individual bar's track rather than
|
|
|
|
|
as one line spanning both bars. */
|
2026-08-15 09:48:41 +01:00
|
|
|
/*
|
|
|
|
|
* Knockout, not a colour.
|
|
|
|
|
*
|
|
|
|
|
* This marker was var(--brand) — the same value as .barExpected and
|
|
|
|
|
* .barExceeding, so wherever it crossed a bar it measured 1.00:1 and was not
|
|
|
|
|
* rendered distinguishably at all. It scored 5.47:1 only against the empty
|
|
|
|
|
* track, which means it was visible precisely when a school was BELOW the
|
|
|
|
|
* national average and vanished for every school at or above it.
|
|
|
|
|
*
|
|
|
|
|
* No single colour fixes this, because the marker's position is data-driven:
|
|
|
|
|
* it can land on the bar, on the empty track, or straddle the boundary. So it
|
|
|
|
|
* is drawn as a knockout — a light core carrying a dark edge. On the teal bar
|
|
|
|
|
* the core reads; on the pale track the edge reads. Both tokens flip with the
|
|
|
|
|
* theme, so the pairing holds in dark mode too.
|
|
|
|
|
*
|
|
|
|
|
* The edge is box-shadow rather than border so it costs no layout width and
|
|
|
|
|
* cannot shift the 50% translate.
|
|
|
|
|
*/
|
2026-07-21 14:55:43 +01:00
|
|
|
.natTick {
|
2026-04-13 21:22:24 +01:00
|
|
|
position: absolute;
|
2026-07-21 14:55:43 +01:00
|
|
|
top: -3px;
|
|
|
|
|
bottom: -3px;
|
2026-08-15 09:48:41 +01:00
|
|
|
width: 3px;
|
2026-07-21 14:55:43 +01:00
|
|
|
transform: translateX(-50%);
|
2026-08-15 09:48:41 +01:00
|
|
|
background: var(--bg-card);
|
|
|
|
|
box-shadow: 0 0 0 1px var(--text-primary);
|
2026-07-21 14:55:43 +01:00
|
|
|
border-radius: 2px;
|
|
|
|
|
z-index: 4;
|
2026-04-13 21:22:24 +01:00
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:55:43 +01:00
|
|
|
.natTick::before {
|
|
|
|
|
content: '';
|
2026-04-13 21:22:24 +01:00
|
|
|
position: absolute;
|
2026-08-15 09:48:41 +01:00
|
|
|
top: -4px;
|
2026-07-21 14:55:43 +01:00
|
|
|
left: 50%;
|
2026-04-13 21:22:24 +01:00
|
|
|
transform: translateX(-50%);
|
2026-08-15 09:48:41 +01:00
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
2026-07-21 14:55:43 +01:00
|
|
|
border-radius: 50%;
|
2026-08-15 09:48:41 +01:00
|
|
|
background: var(--bg-card);
|
|
|
|
|
box-shadow: 0 0 0 1px var(--text-primary);
|
2026-07-21 14:55:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barHeaderRight {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.natLabel {
|
2026-04-13 21:22:24 +01:00
|
|
|
font-size: 0.55rem;
|
|
|
|
|
font-weight: 700;
|
2026-08-06 12:13:04 +01:00
|
|
|
color: var(--brand-strong);
|
2026-07-21 14:55:43 +01:00
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.03em;
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Bar rows ── */
|
|
|
|
|
.barGroup {
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2026-04-14 13:52:00 +01:00
|
|
|
gap: 0.35rem;
|
2026-04-13 21:22:24 +01:00
|
|
|
padding-top: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barRow {
|
|
|
|
|
display: flex;
|
2026-04-14 08:40:38 +01:00
|
|
|
flex-direction: column;
|
2026-04-14 13:52:00 +01:00
|
|
|
gap: 0.18rem;
|
2026-04-14 08:40:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barHeader {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: baseline;
|
2026-04-13 21:22:24 +01:00
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 08:40:38 +01:00
|
|
|
.barTrack {
|
|
|
|
|
width: 100%;
|
2026-04-14 13:52:00 +01:00
|
|
|
height: 14px;
|
2026-04-14 08:40:38 +01:00
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bar {
|
|
|
|
|
height: 100%;
|
2026-04-14 13:52:00 +01:00
|
|
|
border-radius: 4px;
|
2026-04-13 21:22:24 +01:00
|
|
|
position: relative;
|
|
|
|
|
transition: width 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 09:48:41 +01:00
|
|
|
/*
|
|
|
|
|
* Expected and Exceeding were both var(--brand) — one colour for two series,
|
|
|
|
|
* distinguished only by which row you were looking at, while the legend
|
|
|
|
|
* claimed two.
|
|
|
|
|
*
|
|
|
|
|
* They are a sequential pair, not two categories: "exceeding" is a subset of
|
|
|
|
|
* the same cohort at a harder bar. So they take two steps of the same hue
|
|
|
|
|
* rather than two different hues, with the harder measure the more intense
|
|
|
|
|
* step. --brand-stronger is darker than --brand in the light theme and lighter
|
|
|
|
|
* in the dark one, which is the right direction in both: further from the
|
|
|
|
|
* ground.
|
|
|
|
|
*/
|
2026-04-13 21:22:24 +01:00
|
|
|
.barExpected {
|
2026-08-06 12:13:04 +01:00
|
|
|
background: var(--brand);
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barExceeding {
|
2026-08-15 09:48:41 +01:00
|
|
|
background: var(--brand-stronger);
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barLabel {
|
2026-08-06 12:13:04 +01:00
|
|
|
font-family: var(--font-display);
|
2026-04-14 13:52:00 +01:00
|
|
|
font-size: 0.78rem;
|
2026-04-14 08:40:38 +01:00
|
|
|
font-weight: 700;
|
2026-08-06 12:13:04 +01:00
|
|
|
color: var(--text-primary);
|
2026-04-13 21:22:24 +01:00
|
|
|
white-space: nowrap;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.barLabelSuffix {
|
2026-04-14 08:40:38 +01:00
|
|
|
font-weight: 500;
|
2026-08-06 12:13:04 +01:00
|
|
|
color: var(--text-muted);
|
2026-04-14 13:52:00 +01:00
|
|
|
font-size: 0.6rem;
|
2026-04-14 08:40:38 +01:00
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.06em;
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Ruler ── */
|
|
|
|
|
.ruler {
|
|
|
|
|
position: relative;
|
2026-04-14 13:52:00 +01:00
|
|
|
height: 14px;
|
|
|
|
|
margin-top: 0.3rem;
|
2026-08-06 12:13:04 +01:00
|
|
|
border-top: 1px solid var(--border);
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rulerTick {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
width: 1px;
|
2026-04-14 13:52:00 +01:00
|
|
|
height: 3px;
|
2026-08-06 12:13:04 +01:00
|
|
|
background: var(--border);
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rulerLabel {
|
|
|
|
|
position: absolute;
|
2026-04-14 13:52:00 +01:00
|
|
|
top: 5px;
|
2026-04-13 21:22:24 +01:00
|
|
|
font-size: 0.5rem;
|
|
|
|
|
font-weight: 500;
|
2026-08-06 12:13:04 +01:00
|
|
|
color: var(--text-muted);
|
2026-04-13 21:22:24 +01:00
|
|
|
transform: translateX(-50%);
|
|
|
|
|
letter-spacing: 0.01em;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Anchor first/last labels to edges */
|
|
|
|
|
.rulerLabelFirst {
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rulerLabelLast {
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Legend ── */
|
|
|
|
|
.legend {
|
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 1.25rem;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legendItem {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
font-weight: 500;
|
2026-08-06 12:13:04 +01:00
|
|
|
color: var(--text-muted);
|
2026-04-13 21:22:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legendSwatch {
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Responsive ── */
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
.container {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legend {
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
}
|
|
|
|
|
}
|