/* Projects dashboard (patterns/projects-dashboard.php, ADR-0013).
   Ported 2026-09-18 from the approved review mockup,
   dist/assets/css/dashboard.css, onto the theme's token aliases in style.css.
   The mockup's page head and <noscript> notice are not ported: the theme
   page has the site's own title band, and without scripts PHP prints the
   figures and the project register instead.

   WHY THERE IS NO CHART LIBRARY. Every chart here is HTML and CSS: a labelled
   row with a div whose inline-size is a percentage. Chart.js is 78 KB gzipped
   before a single bar is drawn, which is a third of the entire before-images
   budget (Ops Plan section 9, hard rule 5) spent on rendering fourteen
   horizontal bars that CSS draws for free. It would also put every value
   inside a <canvas>, where a screen reader finds nothing, Ctrl+F finds nothing
   and a printed page finds a blank rectangle. The markup version is a
   definition list that happens to be shaded.

   RTL: the bars grow from the inline start, so they run right-to-left in
   Arabic and left-to-right in English with no fork. Nothing in here uses left
   or right. */

/* One knob, so the chart ink is set in a single place and the dark theme moves
   it once rather than per component. --color-primary is 1.4:1 on the dark
   card and the bars would vanish into it. */
:root {
	--chart-bar: var(--color-primary);
	--chart-track: color-mix(in srgb, var(--color-primary) 10%, transparent);
}

[data-theme='dark'] {
	--chart-bar: var(--color-link);
	--chart-track: color-mix(in srgb, var(--color-on-brand) 10%, transparent);
}

/* ---- figure tiles ---- */

.figures {
	display: grid;
	gap: var(--space-s);
	grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
}

.figure {
	padding: var(--space-m);
	border-radius: var(--radius-md);
	background: var(--color-primary);
	color: var(--color-on-brand);
}

.figure__num {
	display: block;
	font-family: var(--font-heading);
	font-weight: var(--weight-heading);
	font-size: var(--size-stat);
	line-height: 1;
	color: var(--color-accent);
	font-variant-numeric: tabular-nums;
}

.figure__label {
	display: block;
	margin-block-start: var(--space-2xs);
	font-size: var(--size-small);
	opacity: 0.88;
}

/* The small line under a tile: "of 33 in the portfolio" when the tiles are
   filtered, and how many amounts sit outside a currency total. Both are scope
   information, so this is quiet type and not the give-button orange it wore
   while it only ever carried a warning.

   currentColor, not a token: a count tile is a primary field with on-brand ink
   and a money tile is a surface field with contrast ink, and one hard-coded
   value would be invisible on one of the two. */
.figure__note {
	display: block;
	margin-block-start: var(--space-2xs);
	font-size: var(--size-tiny);
	color: color-mix(in srgb, currentColor 78%, transparent);
}

/* Whether the tiles above are the portfolio or the current selection. Said out
   loud because the tiles used to ignore the filter while the sentence under
   them obeyed it, and a reader cannot tell those apart by looking. */
.figures__scope {
	margin-block-start: var(--space-2xs);
	font-size: var(--size-small);
	color: var(--color-muted);
}

/* ---- filters ----
   The workbook's three slicers, as one bar. Buttons rather than <select>,
   because the whole point of a slicer is seeing what is selected without
   opening anything. */

.filters {
	display: grid;
	gap: var(--space-m);
}

.filter-group {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: var(--space-2xs);
}

.filter-group__label {
	flex: 0 0 6.5rem;
	font-size: var(--size-small);
	font-weight: var(--weight-bold);
	color: var(--color-muted);
}

.chip-btn {
	padding: 0.3em 0.85em;
	border: 0;
	border-radius: var(--radius-sm);
	background: transparent;
	box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 28%, transparent);
	color: inherit;
	font-family: var(--font-body);
	font-size: var(--size-small);
	cursor: pointer;
	transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
}

.chip-btn:hover {
	background: color-mix(in srgb, currentColor 8%, transparent);
}

.chip-btn[aria-pressed='true'] {
	background: var(--color-primary);
	color: var(--color-on-brand);
	box-shadow: none;
	font-weight: var(--weight-bold);
}

.chip-btn__count {
	opacity: 0.7;
	font-variant-numeric: tabular-nums;
}

.filters__reset {
	justify-self: start;
	font-size: var(--size-small);
	font-weight: var(--weight-bold);
	color: var(--color-link);
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
	text-decoration: underline;
	text-underline-offset: 0.25em;
}

.filters__reset[hidden] {
	display: none;
}

/* ---- charts ----
   The chart-grid that used to live here (an auto-fit grid of 26rem tracks) went
   with the stacked views. The canvas at the foot of this file places every tile
   now, with a span per tile rather than a track size shared by all of them. */

.chart--wide {
	grid-column: 1 / -1;
}

.chart {
	padding: var(--space-m);
	border-radius: var(--radius-md);
	background: var(--color-base);
	box-shadow: 0 0 0 1px var(--chart-track);
}

[data-theme='dark'] .chart {
	background: var(--color-surface);
	box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-on-brand) 12%, transparent);
}

.chart__title {
	font-size: var(--size-large);
	color: var(--color-primary);
}

[data-theme='dark'] .chart__title {
	color: var(--color-contrast);
}

.chart__note {
	margin-block-start: var(--space-3xs);
	font-size: var(--size-tiny);
	color: var(--color-muted);
}

/* A <dl>: each bar is a term and its value, which is what a bar chart is. The
   visual bar is a pseudo-element on the row, so a screen reader reads
   "West Darfur, 6 projects" and never meets the geometry. */
.bars {
	margin-block-start: var(--space-m);
	display: grid;
	gap: var(--space-2xs);
}

.bar {
	display: grid;
	grid-template-columns: minmax(7rem, 13rem) 1fr auto;
	align-items: center;
	gap: var(--space-2xs);
	font-size: var(--size-small);
}

.bar__label {
	/* Wraps rather than ellipsising. A truncated Arabic sector name is not a
	   shorter label, it is a different one: "الأمن الغذائي وسبل ا…" and
	   "الأمن الغذائي وسبل المعيشة" are the same eleven visible characters as
	   two other sectors on the same chart. Two lines cost a few pixels of row
	   height and are always readable. */
	line-height: 1.35;
	overflow-wrap: break-word;
}

.bar__track {
	block-size: 0.9rem;
	border-radius: var(--radius-sm);
	background: var(--chart-track);
	overflow: hidden;
}

.bar__fill {
	block-size: 100%;
	inline-size: var(--pct);
	background: var(--chart-bar);
	border-radius: var(--radius-sm);
	transform-origin: inline-start;
	transition: inline-size var(--motion-duration-base) var(--motion-ease-standard);
}

.bar__value {
	font-weight: var(--weight-bold);
	font-variant-numeric: tabular-nums;
	min-inline-size: 2ch;
	text-align: end;
}

/* A row whose value is zero, recessive but still legible. It was opacity 0.45,
   which measured 2.94:1 in the light theme and 3.94:1 in the dark: de-emphasis
   applied to the ONE row a reader is most likely to be checking on purpose
   ("is this partner really at zero, or did my filter break?"). The muted token
   is the same de-emphasis and is gate-checked at 4.5:1 on both grounds. */
.bar.is-empty .bar__label,
.bar.is-empty .bar__value,
.bar.is-empty .pickable {
	color: var(--color-muted);
}

@media (prefers-reduced-motion: reduce) {
	.bar__fill,
	.chip-btn,
	.bar__label .pickable {
		transition: none;
	}
}

/* ---- project table ----
   A real table, not a grid of divs. Sortable columns, and on a phone it stops
   being a table and becomes a stack of cards: 33 rows of eight columns on a
   360px screen is either a sideways scroll or a 6px font, and both are worse
   than losing the grid. */

.table-wrap {
	margin-block-start: var(--space-m);
	overflow-x: auto;
}

.ptable {
	inline-size: 100%;
	border-collapse: collapse;
	font-size: var(--size-small);
}

.ptable th,
.ptable td {
	padding: var(--space-2xs) var(--space-s);
	text-align: start;
	vertical-align: top;
	border-block-end: 1px solid var(--chart-track);
}

.ptable thead th {
	position: sticky;
	inset-block-start: 0;
	background: var(--color-base);
	font-family: var(--font-heading);
	font-weight: var(--weight-heading);
	font-size: var(--size-tiny);
	color: var(--color-muted);
	border-block-end: var(--rule-weight) solid var(--color-primary);
	white-space: nowrap;
}

[data-theme='dark'] .ptable thead th {
	background: var(--color-base);
	border-block-end-color: var(--color-accent);
}

.ptable__sort {
	background: none;
	border: 0;
	padding: 0;
	font: inherit;
	color: inherit;
	cursor: pointer;
}

.ptable__sort::after {
	content: '';
	display: inline-block;
	inline-size: 0.55em;
	margin-inline-start: 0.4em;
	opacity: 0.35;
}

.ptable__sort[aria-sort='ascending']::after {
	content: '▲';
	opacity: 1;
}

.ptable__sort[aria-sort='descending']::after {
	content: '▼';
	opacity: 1;
}

.ptable__name {
	font-weight: var(--weight-bold);
	min-inline-size: 18rem;
}

.ptable__num {
	font-variant-numeric: tabular-nums;
	text-align: end;
	white-space: nowrap;
}

.ptable tbody tr:hover {
	background: color-mix(in srgb, var(--color-primary) 5%, transparent);
}

.sector-tags {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3xs);
}

.sector-tag {
	padding: 0.05em 0.5em;
	border-radius: var(--radius-sm);
	background: var(--chart-track);
	font-size: var(--size-tiny);
	white-space: nowrap;
}

@media (max-width: 47.99rem) {
	.ptable,
	.ptable tbody,
	.ptable tr,
	.ptable td {
		display: block;
	}

	.ptable thead {
		display: none;
	}

	.ptable tr {
		margin-block-end: var(--space-s);
		padding: var(--space-s);
		border-radius: var(--radius-md);
		box-shadow: 0 0 0 1px var(--chart-track);
	}

	.ptable td {
		border: 0;
		padding: var(--space-3xs) 0;
		display: grid;
		grid-template-columns: 7rem 1fr;
		gap: var(--space-2xs);
	}

	.ptable td::before {
		content: attr(data-label);
		font-size: var(--size-tiny);
		color: var(--color-muted);
	}

	.ptable td.ptable__name {
		grid-template-columns: 1fr;
		font-size: var(--size-body);
		padding-block-end: var(--space-2xs);
	}

	.ptable td.ptable__name::before {
		display: none;
	}

	.ptable__num {
		text-align: start;
	}
}

.result-count {
	margin-block-start: var(--space-m);
	font-size: var(--size-small);
	color: var(--color-muted);
}

.no-results {
	padding: var(--space-xl) var(--space-m);
	text-align: center;
	color: var(--color-muted);
}

/* ---- the corrections disclosure ---- */

/* ---- dashboard sections ----
   The page is now a sequence of headed sections rather than one long column:
   the figures, the filters, the counts, the budgets, the register, and the
   data notes last. The client's workbook is twelve panels tiled edge to edge
   with no grouping at all, which is exactly why it is hard to read: nothing
   tells you that the top row counts projects and the middle row counts money.
   The headings are the whole modernisation. */

.dash-section {
	margin-block-start: var(--space-xl);
}

.dash-section__head {
	margin-block-end: var(--space-m);
	padding-block-end: var(--space-2xs);
	border-block-end: var(--rule-weight) solid var(--color-primary);
}

[data-theme='dark'] .dash-section__head {
	border-block-end-color: var(--color-accent);
}

.dash-section__head :is(h2, h3) {
	font-size: var(--size-h2);
	color: var(--color-primary);
}

[data-theme='dark'] .dash-section__head :is(h2, h3) {
	color: var(--color-contrast);
}

.dash-section__head p {
	margin-block-start: var(--space-3xs);
	font-size: var(--size-small);
	color: var(--color-muted);
	max-inline-size: 70ch;
}

/* The minimum-slice note on a pie. An explanation of why a circle is not being
   drawn, so it is emphasis without alarm: no orange, which on this site means
   "give" and nothing else. */
.chart__note--quiet {
	font-weight: var(--weight-bold);
	margin-block-end: var(--space-s);
}

/* ---- the computed insight sentence ----
   One per section, recomputed on every filter change. It is set larger than
   body text and given the section's own rule, because it is the conclusion the
   charts under it support: if a reader takes one thing from the section, this
   is the thing. Never hand-written, so it cannot go stale or flatter. */

.insight {
	margin-block-end: var(--space-m);
	padding: var(--space-s) var(--space-m);
	border-inline-start: var(--rule-weight) solid var(--color-accent);
	background: var(--color-surface);
	border-radius: var(--radius-sm);
	font-size: var(--size-large);
	line-height: 1.7;
	max-inline-size: 78ch;
}

.insight b {
	color: var(--color-primary);
	font-weight: var(--weight-bold);
}

[data-theme='dark'] .insight b {
	color: var(--color-accent);
}

.swatch {
	inline-size: 0.85rem;
	block-size: 0.85rem;
	border-radius: 2px;
}

.filter-group--currency {
	padding-block-start: var(--space-2xs);
	border-block-start: 1px solid var(--chart-track);
}

.filter-group__hint {
	flex: 1 1 20rem;
	font-size: var(--size-tiny);
	color: var(--color-muted);
}

/* ---- the state x sector heatmap ----
   The crosstab from the Analysis sheet, which is already the right data in the
   wrong shape: a fifteen-column pivot table. Shading carries the magnitude and
   the numeral carries the exact value, so the pattern is readable at a glance
   and the figure is still there when someone needs it. Colour is never the only
   channel: every cell prints its own count. */

.heat {
	border-collapse: separate;
	border-spacing: 2px;
	font-size: var(--size-tiny);
	font-variant-numeric: tabular-nums;
}

.heat__corner {
	background: transparent;
}

/* Rotated column heads. Twelve Arabic sector names across the top of a table
   is otherwise either a 2000px-wide table or twelve two-character stubs. */
.heat__colhead {
	block-size: 8.5rem;
	vertical-align: bottom;
	padding: 0;
	font-weight: var(--weight-body);
	color: var(--color-muted);
	white-space: nowrap;
}

.heat__colhead span {
	display: block;
	writing-mode: vertical-rl;
	transform: rotate(180deg);
	margin-inline: auto;
	max-block-size: 8rem;
	overflow: hidden;
	text-overflow: ellipsis;
}

.heat__rowhead {
	text-align: start;
	font-weight: var(--weight-bold);
	white-space: nowrap;
	padding-inline-end: var(--space-2xs);
}

/* THE FILL IS CAPPED, AND THAT IS THE CONTRAST FIX. Rewritten 2026-07-30 after
   the rule 8 review measured a cell at 3.78:1 in the dark theme.

   The first version ran the fill from 0 to 100% of the ink colour and flipped
   the numeral to a reversed ink past --w 0.7. Two things were wrong with it.
   Substring selectors on the style attribute ([style*='--w: 0.7']) are a
   brittle way to band a continuous value, and more seriously the flip point was
   measured on the light palette and reused on the dark one, where the crossover
   sits somewhere else: at --w 0.61 on the dark ground the body ink is 3.78:1 and
   the reversed ink is 3.90:1, so NEITHER passes and no threshold could have
   saved it. That value is what the formula emits whenever the heatmap maximum is
   2, which is one state chip away.

   Capping the fill removes the crossover instead of chasing it. Full strength is
   now 55% of the ink colour in the light theme and 45% in the dark, so one ink
   clears 4.5:1 across the whole ramp and the flip rules are gone:

     light, --color-contrast on primary   5.9:1 at --w 1, 12.0:1 at --w 0.22
     dark,  --color-contrast on link      5.5:1 at --w 1,  9.9:1 at --w 0.22

   The cost is less dynamic range between a 1 and a 3, which the printed numeral
   in every non-zero cell already carries. Both worst cases are declared in
   tools/contrast-check.mjs COMPOSITED, which is where the previous version said
   they were and where they were not. */
.heat__cell {
	inline-size: 2.4rem;
	block-size: 2.2rem;
	text-align: center;
	border-radius: var(--radius-sm);
	background: color-mix(in srgb, var(--color-primary) calc(var(--w) * 55%), transparent);
	color: var(--color-contrast);
	--w: 0;
}

[data-theme='dark'] .heat__cell {
	background: color-mix(in srgb, var(--color-link) calc(var(--w) * 45%), transparent);
	color: var(--color-contrast);
}

.heat__cell.is-zero {
	background: var(--chart-track);
}

/* ============================================================================
   THE DASHBOARD SHELL
   ============================================================================
   Rebuilt 2026-07-30 after the UI session
   (docs/meetings/2026-07-30-projects-dashboard-ui.md).

   Everything logical, nothing directional: the panel pins to the inline start,
   which is the right edge in Arabic and the left in English, with no [dir]
   fork anywhere in this file. */

.dash__shell {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0;
	/* Wider than --layout-wide (75rem), which is a measure tuned for prose. A
	   17rem side panel takes the content column below the width two 26rem chart
	   cards need, so every panel dropped to one column and the view got long
	   again, which is the thing the shell exists to fix. 90rem restores the
	   two-up grid at 1366px. Nothing else on the site uses this width, and
	   nothing here is running text. */
	max-inline-size: 90rem;
	margin-inline: auto;
	padding-inline: var(--layout-gutter);
}

@media (min-width: 60rem) {
	.dash__shell {
		grid-template-columns: 17rem 1fr;
		gap: var(--space-l);
		align-items: start;
	}
}

/* Sticky rather than fixed. Fixed would take the panel out of flow and leave
   the drawer and the desktop panel needing two different layouts; sticky is one
   layout that stops scrolling at the top of the viewport. */
.dash__side {
	position: sticky;
	/* Below the site header, which stays on screen (--header-block-size, set
	   by theme.js). */
	inset-block-start: calc(var(--header-block-size) + var(--space-s));
	max-block-size: calc(100vh - var(--header-block-size) - var(--space-xl));
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: var(--space-m) 0;
}

.dash__main {
	min-inline-size: 0;
	padding-block: var(--space-m) var(--space-xl);
}

/* ---- filter groups ---- */

.filters {
	display: grid;
	gap: var(--space-s);
	padding-block-start: var(--space-m);
	border-block-start: var(--rule-weight) solid var(--color-primary);
}

[data-theme='dark'] .filters {
	border-block-start-color: var(--color-accent);
}

.filters__head {
	font-size: var(--size-large);
	color: var(--color-primary);
}

[data-theme='dark'] .filters__head {
	color: var(--color-contrast);
}

.filters__top {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--space-3xs) var(--space-s);
}

.filters__clear {
	font-size: var(--size-small);
}

.filters__clear[hidden] {
	display: none;
}

.fgroup__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-2xs);
	/* The body face on purpose. This is a 14px control label, not a heading, and
	   Cairo is variable so 700 costs nothing; taking it from the heading face
	   pulled a whole second Almarai weight onto the page. */
	font-family: var(--font-body);
	font-size: var(--size-small);
	font-weight: var(--weight-bold);
	color: var(--color-muted);
	cursor: pointer;
	list-style: none;
	padding-block: var(--space-3xs);
}

.fgroup__head::-webkit-details-marker {
	display: none;
}

.fgroup__badge[hidden] {
	display: none;
}

.fgroup__badge {
	min-inline-size: 1.3rem;
	padding: 0 0.3em;
	border-radius: 999px;
	background: var(--color-primary);
	color: var(--color-on-brand);
	font-size: var(--size-tiny);
	text-align: center;
}

.fgroup__list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3xs);
	padding-block: var(--space-2xs) var(--space-s);
}

.fgroup__hint {
	margin-block-start: var(--space-2xs);
	font-size: var(--size-tiny);
	color: var(--color-muted);
}

/* ---- the state filter, drawn as the map ----
   Replaces the ten state chips (chair, 2026-07-30). Built by
   assets/js/dashboard.js from the SVG that tools/build-sudan-map.mjs injects
   into projects.html; every path that the register knows carries data-state
   with its register key and data-band with its count band.

   TWO CHANNELS, and keeping them apart is the whole design:
     fill   = how many projects the current selection puts in this state
     stroke = whether CDF is present at all
   Nine of the nineteen shapes are outside the register. Five of those nine
   are states CDF has a BRANCH in, and a branch with no project rows is a
   different statement from empty desert, so it gets a dashed outline rather
   than a different fill. Fill is reserved for data and never says anything
   about presence.

   THE DARK THEME DOES NOT USE PRIMARY AS THE RAMP. --color-primary is 1.9:1 on
   the dark ground: a five-step ramp of it would be five shades of the same
   invisible navy. The ramp switches to --color-link, exactly as .heat__cell
   already does and for the same reason. Both ramps and both focus rings are
   declared in tools/contrast-check.mjs COMPOSITED; change a percentage here
   and change it there. */

.fgroup--map {
	padding-block-end: var(--space-2xs);
	border-block-end: 1px solid var(--chart-track);
}

.smapf {
	/* The panel is 17rem; the map is the only thing in it that wants to be as
	   wide as it can get. The 4px is room for the focus ring: the side panel is
	   a scroll container, and without it the outline on Kassala or West Darfur,
	   the two states that touch the map's edges, gets clipped on the side it
	   most needs to be seen. */
	margin-block: var(--space-2xs);
	padding-inline: 6px;
}

.smapf .sdmap {
	/* Overrides the 30rem cap base.css puts on the presence map, which was
	   sized for a homepage figure and would leave this one enormous inside a
	   side panel that scrolls. */
	max-block-size: 14rem;
	/* The presence map is drawn on a 1000-unit viewBox and this one on 500, so
	   every stroke width below is half the number it would be there. The
	   selected ring at 1.2 is the chair's 2.4, in this map's coordinate space. */
	--smap-ramp: var(--color-primary);
	--smap-ring: var(--color-primary-deep);
}

[data-theme='dark'] .smapf .sdmap {
	--smap-ramp: var(--color-link);
	/* primary-deep is 1.03:1 on this map's palest fill once the ground goes
	   dark, which is a focus ring that cannot be seen. Yellow, as everywhere
	   else in this theme. */
	--smap-ring: var(--color-accent);
}

/* Everything that is neither a branch nor in the register: the base wash and a
   thin, quiet border. Also the ground the two tiers above it are read against,
   which is why it is a wash and not the page. */
.smapf .sdmap__state {
	fill: color-mix(in srgb, var(--smap-ramp) 6%, var(--color-base));
	stroke: color-mix(in srgb, var(--color-muted) 30%, transparent);
	stroke-width: 0.5;
	stroke-linejoin: round;
}

/* A branch state the register has no rows for. Dashed, and it keeps the base
   wash: CDF is there, the register does not say so. Not interactive, because
   there is nothing to filter to. */
.smapf .sdmap__state.is-present:not([data-state]) {
	fill: color-mix(in srgb, var(--smap-ramp) 6%, var(--color-base));
	stroke: var(--color-muted);
	stroke-width: 0.7;
	stroke-dasharray: 3 2.5;
}

/* A register state. Solid stroke in the page ground, so the shapes separate
   from each other however dark the fill gets. */
.smapf .sdmap__state[data-state] {
	stroke: var(--color-base);
	stroke-width: 0.7;
	stroke-dasharray: none;
	cursor: pointer;
	transition: fill var(--motion-duration-fast) var(--motion-ease-standard);
}

/* The ramp. Light and dark carry different percentages, not because the dark
   theme wants a different design but because the dark ramp has to stop at 56%:
   the accent focus ring measures 2.39:1 over a 72% link fill and 3.49:1 over a
   56% one, and a focus indicator owes 3:1. */
.sdmap__state[data-band='0'], .smapf__swatch[data-band='0'] { --smap-w: 12%; }
.sdmap__state[data-band='1'], .smapf__swatch[data-band='1'] { --smap-w: 24%; }
.sdmap__state[data-band='2'], .smapf__swatch[data-band='2'] { --smap-w: 40%; }
.sdmap__state[data-band='3'], .smapf__swatch[data-band='3'] { --smap-w: 56%; }
.sdmap__state[data-band='4'], .smapf__swatch[data-band='4'] { --smap-w: 72%; }

[data-theme='dark'] .sdmap__state[data-band='0'],
[data-theme='dark'] .smapf__swatch[data-band='0'] { --smap-w: 10%; }
[data-theme='dark'] .sdmap__state[data-band='1'],
[data-theme='dark'] .smapf__swatch[data-band='1'] { --smap-w: 20%; }
[data-theme='dark'] .sdmap__state[data-band='2'],
[data-theme='dark'] .smapf__swatch[data-band='2'] { --smap-w: 32%; }
[data-theme='dark'] .sdmap__state[data-band='3'],
[data-theme='dark'] .smapf__swatch[data-band='3'] { --smap-w: 44%; }
[data-theme='dark'] .sdmap__state[data-band='4'],
[data-theme='dark'] .smapf__swatch[data-band='4'] { --smap-w: 56%; }

.smapf .sdmap__state[data-band] {
	fill: color-mix(in srgb, var(--smap-ramp) var(--smap-w), var(--color-base));
}

.smapf .sdmap__state[data-state]:hover {
	fill: color-mix(in srgb, var(--smap-ramp) 84%, var(--color-base));
}

[data-theme='dark'] .smapf .sdmap__state[data-state]:hover {
	fill: color-mix(in srgb, var(--smap-ramp) 68%, var(--color-base));
}

/* SELECTED. The ring is --color-accent at the chair's 2.4, halved for this
   viewBox. The fill goes to the top of the ramp with it, and that is a real
   trade rather than decoration: yellow over the palest fill is 1.09:1 in the
   light theme, so a ring alone would mark the selection with something the
   reader cannot see. At the ramp maximum accent measures 3.52:1 light and
   3.49:1 dark. The cost is that the selected state stops reporting its own
   band; its count is in the pill above the content, in the readout under the
   map and in the shape's accessible name, so it is moved and not lost. */
.smapf .sdmap__state[data-state].is-selected {
	fill: color-mix(in srgb, var(--smap-ramp) 72%, var(--color-base));
	stroke: var(--color-accent);
	stroke-width: 1.2;
}

[data-theme='dark'] .smapf .sdmap__state[data-state].is-selected {
	fill: color-mix(in srgb, var(--smap-ramp) 56%, var(--color-base));
}

.smapf .sdmap__state[data-state]:focus-visible {
	outline: 3px solid var(--smap-ring);
	outline-offset: 2px;
}

.smapf__read {
	min-block-size: 2.4em;
	font-size: var(--size-tiny);
	color: var(--color-muted);
}

.smapf__legend {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-3xs) var(--space-2xs);
	margin-block-start: var(--space-3xs);
	font-size: var(--size-tiny);
	color: var(--color-muted);
	list-style: none;
}

.smapf__legend li {
	display: flex;
	align-items: center;
	gap: 0.3em;
}

.smapf__legend-wide {
	flex-basis: 100%;
}

.smapf__swatch {
	inline-size: 0.85rem;
	block-size: 0.85rem;
	border-radius: 2px;
	background: color-mix(in srgb, var(--smap-legend-ramp, var(--color-primary)) var(--smap-w, 12%), var(--color-base));
	border: 1px solid color-mix(in srgb, var(--color-muted) 30%, transparent);
}

[data-theme='dark'] .smapf__swatch {
	--smap-legend-ramp: var(--color-link);
}

.smapf__swatch--branch {
	background: color-mix(in srgb, var(--smap-legend-ramp, var(--color-primary)) 6%, var(--color-base));
	border: 1px dashed var(--color-muted);
}

@media (prefers-reduced-motion: reduce) {
	.smapf .sdmap__state[data-state] {
		transition: none;
	}
}

/* ---- active filter pills ----
   Above the content, in the content area, not in the panel. The panel is where
   filters are CHOSEN; this is where they are seen and undone, and those are
   different jobs done at different moments. */

.pillbar {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-2xs);
	padding-block-end: var(--space-s);
	margin-block-end: var(--space-m);
	border-block-end: 1px solid var(--chart-track);
	font-size: var(--size-small);
}

/* --weight-heading, not the browser's bold-by-default for <b>. Beacon's heading
   face is Almarai 800; a <b> in the heading family resolves to 700, and 700 is a
   SEPARATE 50 KB of woff2 fetched for this one numeral and the filter-group
   summaries. Two declarations here and at .fgroup__head are worth more than
   every other byte this page could give back. */
.pillbar__count b {
	font-size: var(--size-large);
	font-family: var(--font-heading);
	font-weight: var(--weight-heading);
	color: var(--color-primary);
}

[data-theme='dark'] .pillbar__count b {
	color: var(--color-accent);
}

.pillbar__none {
	color: var(--color-muted);
}

.pillbar__list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3xs);
	margin-inline-start: auto;
	padding: 0;
	list-style: none;
}

.pill {
	display: inline-flex;
	align-items: center;
	gap: var(--space-3xs);
	padding: 0.2em 0.7em;
	border: 0;
	border-radius: var(--radius-sm);
	background: var(--color-primary);
	color: var(--color-on-brand);
	font-family: var(--font-body);
	font-size: var(--size-tiny);
	cursor: pointer;
}

.pill__facet {
	opacity: 0.75;
}

.pill__x {
	font-size: 1.15em;
	line-height: 1;
}

.pill:hover {
	background: var(--color-primary-deep);
}

[data-theme='dark'] .pill:hover {
	background: var(--color-link);
	color: var(--color-primary-deep);
}

.filters__clear,
.linkish {
	background: none;
	border: 0;
	padding: 0;
	font: inherit;
	color: var(--color-link);
	font-weight: var(--weight-bold);
	cursor: pointer;
	text-decoration: underline;
	text-underline-offset: 0.25em;
}

/* ---- view heads ---- */

.view-head {
	margin-block-end: var(--space-l);
}

.view-head h1 {
	font-size: var(--size-h1);
	color: var(--color-primary);
}

[data-theme='dark'] .view-head h1 {
	color: var(--color-contrast);
}

.view-head h1:focus {
	outline: none;
}

.view-head p {
	margin-block-start: var(--space-2xs);
	color: var(--color-muted);
	max-inline-size: 60ch;
}

/* The line between the client's panels and ours. It has to be unmistakable:
   they asked for their charts, and anything of ours presented as theirs is a
   liberty taken with someone else's reporting. */
.dash-extra {
	margin-block-start: var(--space-xl);
	padding-block-start: var(--space-l);
	border-block-start: var(--rule-weight) dashed
		color-mix(in srgb, var(--color-primary) 45%, transparent);
}

/* ---- pie and doughnut ---- */

.pie {
	display: grid;
	gap: var(--space-m);
	grid-template-columns: 1fr;
	align-items: center;
	margin-block-start: var(--space-m);
}

@media (min-width: 26rem) {
	.pie {
		grid-template-columns: 9rem 1fr;
	}

	/* Below three slices the disc is not drawn at all (see pieChart), so the
	   legend takes the full width rather than sitting in a 9rem indent beside
	   nothing. */
	.pie--legend-only {
		grid-template-columns: 1fr;
	}
}

.pie__disc {
	inline-size: 9rem;
	block-size: 9rem;
	border-radius: 50%;
	box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 25%, transparent);
}

.pie__disc--ring {
	-webkit-mask: radial-gradient(circle, transparent 46%, #000 47%);
	mask: radial-gradient(circle, transparent 46%, #000 47%);
	box-shadow: none;
}

.pie__legend {
	display: grid;
	gap: var(--space-3xs);
	padding: 0;
	list-style: none;
	font-size: var(--size-small);
}

.pie__legend li {
	display: grid;
	grid-template-columns: auto 1fr auto;
	align-items: baseline;
	gap: var(--space-2xs);
}

/* A hairline in the text colour rather than in --chart-track, which is a 10%
   tint and disappears under the lighter half of the categorical scale. The
   scale alternates dark and light steps on purpose, so half the swatches sit
   at 1.7:1 against a white card and need an edge to be a shape at all. */
.pie__legend .swatch {
	align-self: center;
	box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 35%, transparent);
}

.pie__val {
	font-variant-numeric: tabular-nums;
	color: var(--color-muted);
	white-space: nowrap;
}

/* ---- the drawer, below the panel breakpoint ---- */

.dash__drawer-toggle {
	display: inline-flex;
	align-items: center;
	gap: var(--space-2xs);
	margin-block-end: var(--space-m);
	padding: var(--space-2xs) var(--space-s);
	border: 0;
	border-radius: var(--radius-sm);
	background: var(--color-primary);
	color: var(--color-on-brand);
	font-family: var(--font-body);
	font-size: var(--size-body);
	font-weight: var(--weight-bold);
	cursor: pointer;
}

@media (min-width: 60rem) {
	.dash__drawer-toggle {
		display: none;
	}
}

@media (max-width: 59.99rem) {
	.dash__side {
		position: static;
		max-block-size: none;
		overflow: visible;
		display: none;
		margin-block-end: var(--space-l);
		padding: var(--space-m);
		border-radius: var(--radius-md);
		background: var(--color-surface);
	}

	.dash__shell.is-open .dash__side {
		display: block;
	}
}

/* ============================================================================
   THE CANVAS
   ============================================================================
   Added 2026-07-30 for the Tableau-style rebuild. The client's second reading
   of the dashboard was "functional but not appealing", and the diagnosis was
   measurable rather than a matter of taste: the analysis view was 9,386 px
   tall, and eight of the nine panels in it were the same horizontal bar chart
   at the same width and the same visual weight. Nothing on that page was ever
   next to anything else, so no two facts could be compared without scrolling,
   which is the one job a dashboard has.

   A twelve-column grid with UNEQUAL spans is the whole fix. Equal panels is the
   same statement as no hierarchy: it tells the reader that where CDF works and
   which currency it counts in are questions of the same size. The map takes
   seven columns and two rows and is the anchor; everything else is sized by how
   much of the story it carries.

   Spans ride on --x, set per tile in dashboard.js, so the composition is one
   readable list in the code that builds it rather than a dozen width classes
   spelled out here. Nothing in this block names left or right: grid flows from
   the inline start, so the same rules give Arabic a right-to-left board and
   English a left-to-right one. */

.canvas {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: var(--space-m);
	margin-block-start: var(--space-m);
}

/* Grid and flex items default to min-content, not zero, so one long word or one
   wide table inside a tile pushes the whole track past the viewport. */
.canvas > * {
	min-inline-size: 0;
}

@media (min-width: 56rem) {
	.canvas {
		grid-template-columns: repeat(12, 1fr);
		/* Tiles fill their row rather than sizing to content. Ragged bottoms are
		   what made the old chart-grid read as a pile of cards; a board reads as
		   a board only when its edges line up. The cost is a short tile with
		   space under its last bar, which is the correct trade here. */
		align-items: stretch;
	}

	.canvas > * {
		grid-column: span var(--x, 12);
		min-inline-size: 0;
	}

	/* The map. Two rows, so the partner and currency tiles stack beside it and
	   the three together make one block instead of three loose cards. */
	.canvas > .tile--anchor {
		grid-row: span 2;
	}
}

/* Tiles are flex columns so a plot can take the slack the stretch created and
   the tile's own note stays pinned under its title. */
.chart {
	display: flex;
	flex-direction: column;
}

.chart > .bars,
.chart > .pie,
.chart > .tmap,
.chart > .smapf {
	flex: 1 1 auto;
}

/* ---- the figures, flattened ----
   They were five solid navy cards with 3.75rem accent numerals, which is the
   hero-metric template and reads as a marketing band rather than as the top row
   of a dashboard. On the canvas they are one rule-separated strip: the numerals
   keep the weight that makes them scannable, the boxes go, and the strip stops
   competing with the map underneath it for the reader's first look. */

.figures {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(8.5rem, 100%), 1fr));
	gap: 0;
	margin-block-start: var(--space-l);
	border-block: 1px solid var(--chart-track);
}

.figure {
	display: flex;
	flex-direction: column;
	gap: 0.1em;
	padding: var(--space-s) var(--space-m) var(--space-s) 0;
	background: none;
	border: 0;
	border-radius: 0;
	/* A hairline between cells, not around them. The last one has no partner to
	   separate from, and a trailing rule would fence the strip. */
	border-inline-end: 1px solid var(--chart-track);
}

.figure:last-child {
	border-inline-end: 0;
}

.figure__num {
	font-family: var(--font-heading);
	font-size: var(--size-h2);
	font-weight: var(--weight-heading);
	line-height: 1.1;
	color: var(--color-primary);
	font-variant-numeric: tabular-nums;
}

[data-theme='dark'] .figure__num {
	color: var(--color-contrast);
}

.figure__label {
	font-size: var(--size-small);
	color: var(--color-muted);
}

/* ---- the treemap ----
   Area is the project count, shade is the budget. The fill formula is the one
   .heat__cell uses, deliberately and not by coincidence: it is capped at 55% of
   the ink in the light theme and 45% in the dark, which is what lets a single
   ink colour clear 4.5:1 across the whole ramp instead of needing a flip point
   that moves between themes. Both worst cases are already declared in
   tools/contrast-check.mjs COMPOSITED under the heatmap; this mark adds no new
   pair, which is the reason it reuses the formula rather than inventing one. */

.tmap {
	position: relative;
	/* Portrait-ish on a phone, where a 21:9 band would be 154px tall and every
	   cell in the tail would be a hairline. The band is what the tile gets once
	   it has the full twelve columns. */
	aspect-ratio: 4 / 3;
	margin-block-start: var(--space-m);
	min-block-size: 15rem;
}

@media (min-width: 56rem) {
	.tmap {
		aspect-ratio: 21 / 9;
	}
}

.tmap__cell {
	position: absolute;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	justify-content: flex-start;
	padding: 0.35rem 0.45rem;
	line-height: 1.25;
	font-size: var(--size-tiny);
	color: var(--color-contrast);
	background: color-mix(in srgb, var(--color-primary) calc(var(--w) * 55%), var(--color-base));
	/* The 2px gap between fills that keeps two adjacent rectangles from reading
	   as one. An outline rather than a margin, so it costs no layout maths. */
	outline: 2px solid var(--color-base);
	outline-offset: -1px;
}

[data-theme='dark'] .tmap__cell {
	background: color-mix(in srgb, var(--color-link) calc(var(--w) * 45%), var(--color-surface));
	outline-color: var(--color-surface);
}

.tmap__label {
	font-weight: var(--weight-bold);
	overflow-wrap: anywhere;
}

.tmap__val {
	font-variant-numeric: tabular-nums;
	font-size: var(--size-small);
	font-weight: var(--weight-bold);
}

/* Middle tier: room for the name and the count. */
.tmap__cell--mid {
	font-size: 0.72rem;
	padding: 0.2rem 0.3rem;
}

.tmap__cell--mid .tmap__val {
	font-size: 0.72rem;
}

/* The tail. Nothing is drawn, because an Arabic sector name in a 30px box is
   not small text, it is a column of single letters. The label and the count
   stay in the DOM behind the same clip the skip link uses, so Ctrl+F, a screen
   reader and the accessible name all still find them; the tooltip and the
   register carry the figure for anyone pointing at the cell. */
.tmap__cell--tiny > * {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

.tmap__cell--tiny {
	padding: 0;
}

/* The baseline the dots are read against. Recessive: it is the axis, not data. */

/* ---- the map as the anchor tile ---- */

.chart--map {
	container-type: inline-size;
}

.smapf--tile {
	display: flex;
	align-items: center;
	justify-content: center;
	padding-inline: 0;
	margin-block: var(--space-s);
}

.smapf--tile .sdmap {
	/* The rail capped it at 14rem to fit a 17rem scrolling panel. On the canvas
	   the cap is the tile, and the tile is seven of twelve columns. */
	max-block-size: none;
	inline-size: 100%;
	block-size: auto;
}

.chart--map .smapf__read {
	font-size: var(--size-small);
	min-block-size: 1.6em;
}

.chart--map .smapf__legend {
	font-size: var(--size-small);
}

/* ---- click the chart, not the chip ----
   Every mark that names a state, a sector or a partner is also the control that
   filters to it. The button is transparent by default so the chart does not
   turn into a wall of controls; it announces itself on hover and focus, and it
   states the selection permanently once pressed. */

.pickable {
	display: block;
	inline-size: 100%;
	padding: 0;
	border: 0;
	background: none;
	color: inherit;
	font: inherit;
	text-align: start;
	cursor: pointer;
	border-radius: var(--radius-sm);
}

.bar__label .pickable {
	text-decoration: underline;
	text-decoration-color: transparent;
	text-underline-offset: 0.25em;
	transition: text-decoration-color var(--motion-duration-fast) var(--motion-ease-standard);
}

.bar__label .pickable:hover {
	text-decoration-color: var(--color-link);
}

.bar__label .pickable[aria-pressed='true'] {
	font-weight: var(--weight-bold);
	text-decoration-color: var(--color-link);
}

.tmap__cell .pickable {
	block-size: 100%;
	display: flex;
	flex-direction: column;
	gap: 0.1em;
}

.pickable:focus-visible {
	outline: 3px solid var(--color-link);
	outline-offset: 2px;
}

/* Selected sector. Same statement the map makes about a selected state, in the
   same colour: the accent ring, at the weight this mark can carry. */
.tmap__cell:has(.pickable[aria-pressed='true']) {
	outline: 3px solid var(--color-accent);
	outline-offset: -3px;
}

/* ---- the findings strip ---- */

.findings {
	padding: var(--space-m);
	border-radius: var(--radius-md);
	background: var(--color-surface);
}

[data-theme='dark'] .findings {
	background: color-mix(in srgb, var(--color-on-brand) 5%, transparent);
}

.findings__title {
	font-size: var(--size-large);
	color: var(--color-primary);
}

[data-theme='dark'] .findings__title {
	color: var(--color-contrast);
}

.findings__list {
	display: grid;
	gap: var(--space-2xs);
	/* min() rather than a bare 24rem: an auto-fit track floor wider than the
	   viewport does not collapse, it overflows, and 24rem is 384px against a
	   360px phone. */
	grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr));
	margin-block-start: var(--space-s);
	padding: 0;
	list-style: none;
}

/* The insight box already carries its own inline-start rule inside a findings
   cell, so it does not need the card treatment a second time. */
.findings__list .insight {
	margin: 0;
	background: none;
}

/* ---- the detail band ---- */

.dash-detail {
	margin-block-start: var(--space-xl);
	padding-block-start: var(--space-l);
	border-block-start: var(--rule-weight) solid var(--color-primary);
}

[data-theme='dark'] .dash-detail {
	border-block-start-color: var(--color-accent);
}

/* ---- the register, as detail-on-demand ----
   Thirty-three rows is about 3,000 px, which is a third of the canvas spent on
   the one thing a reader goes looking for deliberately rather than reads on the
   way past. It scrolls inside its own box with the head pinned, so the table is
   still whole and still sortable and still findable by Ctrl+F, and the canvas
   above it stays a canvas. Capped in viewport units so a short laptop screen
   does not get a table taller than the screen it is on. */

@media (min-width: 56rem) {
	.dash-detail .table-wrap {
		max-block-size: min(34rem, 60vh);
		overflow: auto;
		border: 1px solid var(--chart-track);
		border-radius: var(--radius-md);
	}

	.dash-detail .ptable thead th {
		position: sticky;
		inset-block-start: 0;
		z-index: 1;
	}
}

/* ---- tiles are their own containers ----
   A bar chart five columns wide and one twelve columns wide are the same
   component in very different rooms, and the label column that fits a state
   name at 1180px leaves a 60px track at 560px. Container queries ask the tile,
   not the viewport, which is the only question that has an answer here: the
   same chart appears at both widths on the same screen. */

.chart {
	container-type: inline-size;
}

@container (max-width: 34rem) {
	.bar {
		grid-template-columns: minmax(4.5rem, 8rem) 1fr auto;
		font-size: var(--size-tiny);
	}

	.pie {
		grid-template-columns: 7rem 1fr;
	}

	.pie__disc {
		inline-size: 7rem;
		block-size: 7rem;
	}
}

/* Project titles, not category names: they need a column they can be read in
   rather than one they wrap down. */

/* Count-only tier: too narrow for an Arabic sector name, wide enough for the
   numeral that says the cell is a small sector and not a rendering fault. */
.tmap__cell--num {
	align-items: center;
	justify-content: center;
	padding: 0.15rem;
}

.tmap__cell--num .tmap__label {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

.tmap__cell--num .tmap__val {
	font-size: var(--size-small);
}
