feat: initial Münster Haushalt icicle viewer
Editorial single-page viewer for the City of Münster's 2026/2027 budget draft, built as an Astro v6 SPA with a 4-level zoomable icicle (Produktbereich → Produktgruppe → Category → Breakdown). Highlights: - Multi-flow data layer over the official open-data CSVs (Aufwendungen + Erträge, 2008–2028) with overlap reconciliation across plan years. - Year slider as a 21-year mini-histogram of both flows; drag-to-scrub and click-to-jump, with bars morphing via CSS transitions on SVG geometry attributes. - Vertically centred icicle with year-outline rectangles framing each year's relative budget size, à la Bostock's animated treemap. - Headline "ausgibt / einnimmt" toggle; sidebar Aufwendungen/Erträge rows double as flow toggles. Active flow in Aufwendungen-purple / Erträge-orange (OKLCH). - Click-to-zoom via path-keyed lookup with ZOOM_COL_BOUNDS that reallocate the depth axis per zoom state. Zoomed item moves to the sidebar; canvas shows its descendants only (no adjacent-block leaks). - Sidebar shows path-specific Aufwendungen/Erträge/Saldo plus the source-PDF Beschreibung; Erläuterungen behind a collapsed details. - Build-time PDF extraction (scripts/extract-pg-sections.mjs) parses 68 Produktgruppen' Beschreibung + Erläuterungen sections from Band 1, including 10 cells of structured Mio.-€ breakdowns (Steuern, Transferaufwendungen, etc.) that drive the level-4 view. - URL state sync for path, year, and flow via history.replaceState so any zoom is shareable. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/* ──────────────────────────────────────────────────────────────────────────
|
||||
ms-haushalt · global tokens, reset, and base typography.
|
||||
See .impeccable.md for the design context this expresses.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
@import "@fontsource-variable/geist";
|
||||
@import "@fontsource-variable/geist-mono";
|
||||
|
||||
/* Figtree Variable — display + body face. SIL OFL, served from
|
||||
public/fonts. Roman + italic axes, weight 300–900. */
|
||||
@font-face {
|
||||
font-family: "Figtree Variable";
|
||||
src: url("/fonts/Figtree-VariableFont_wght.woff2") format("woff2-variations");
|
||||
font-weight: 300 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Figtree Variable";
|
||||
src: url("/fonts/Figtree-Italic-VariableFont_wght.woff2") format("woff2-variations");
|
||||
font-weight: 300 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* ── Color (OKLCH, paper-feeling, two-hue flow palette) ───────────── */
|
||||
/* Background: warm uncoated stock, never pure white. Tinted toward the
|
||||
Aufwendungen hue family by 0.005-0.01 chroma per the impeccable
|
||||
"tint neutrals toward brand hue" principle. */
|
||||
--paper: oklch(98.4% 0.006 60);
|
||||
--paper-deep: oklch(96.0% 0.008 60);
|
||||
|
||||
/* Ink: warmed near-black. Same principle inverted. */
|
||||
--ink: oklch(20% 0.02 50);
|
||||
--ink-soft: oklch(38% 0.02 55);
|
||||
--ink-mute: oklch(58% 0.015 60);
|
||||
|
||||
/* Hairlines & faint surfaces. */
|
||||
--rule: oklch(86% 0.012 60);
|
||||
--rule-soft: oklch(92% 0.008 60);
|
||||
|
||||
/* Two-hue flow palette per design brief §9.5: deep purple for
|
||||
Aufwendungen (money leaving), orange for Erträge (money coming in).
|
||||
Far enough apart on the OKLCH wheel to read as a clear pivot; both
|
||||
hold their own against the warm-paper background. NOT red/green. */
|
||||
--flow-aufwand-h: 295; /* deep plum-purple */
|
||||
--flow-aufwand-c: 0.17;
|
||||
--flow-ertrag-h: 55; /* warm orange / amber */
|
||||
--flow-ertrag-c: 0.16;
|
||||
|
||||
/* Solid ink-on-paper renders of each flow accent (used for active toggle
|
||||
state, breadcrumb separators, slider handle, etc.). */
|
||||
--flow-aufwand: oklch(40% var(--flow-aufwand-c) var(--flow-aufwand-h));
|
||||
--flow-ertrag: oklch(58% var(--flow-ertrag-c) var(--flow-ertrag-h));
|
||||
|
||||
/* ── Spacing (4pt scale, semantic names) ──────────────────────────── */
|
||||
--space-2xs: 4px;
|
||||
--space-xs: 8px;
|
||||
--space-sm: 12px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
--space-2xl: 48px;
|
||||
--space-3xl: 64px;
|
||||
--space-4xl: 96px;
|
||||
|
||||
/* ── Type scale ───────────────────────────────────────────────────── */
|
||||
/* Display sizes are fluid (editorial, not app-chrome), per the
|
||||
typography rule. Body and UI sizes are fixed rem. The display
|
||||
range is intentionally tighter than the previous version — the
|
||||
hero needs to stay above the fold on a typical 13" laptop. */
|
||||
--size-display-1: clamp(2.625rem, 5.5vw + 1rem, 5.25rem); /* hero stack */
|
||||
--size-display-2: clamp(1.75rem, 2.5vw + 0.5rem, 2.5rem); /* sidebar heading */
|
||||
--size-display-3: 1.375rem; /* secondary */
|
||||
|
||||
--size-body: 1rem; /* 16px */
|
||||
--size-body-lg: 1.0625rem;/* 17px lede */
|
||||
--size-caption: 0.8125rem;/* 13px */
|
||||
--size-eyebrow: 0.6875rem;/* 11px small caps */
|
||||
--size-micro: 0.75rem; /* 12px sidebar dt labels */
|
||||
|
||||
/* ── Faces ────────────────────────────────────────────────────────── */
|
||||
/* Display + body: Figtree (variable, SIL OFL, served from
|
||||
public/fonts). Numbers: Geist Mono. The `--face-serif` /
|
||||
`--face-sans` token names are legacy aliases — both now point to
|
||||
Figtree since it carries display and body alike. */
|
||||
--face-display: "Figtree Variable", -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", system-ui, sans-serif;
|
||||
--face-serif: var(--face-display);
|
||||
--face-sans: var(--face-display);
|
||||
--face-mono: "Geist Mono Variable", "Geist Variable", ui-monospace,
|
||||
"SF Mono", "Menlo", monospace;
|
||||
|
||||
/* ── Layout rhythm ────────────────────────────────────────────────── */
|
||||
--measure: 65ch;
|
||||
--page-max: 1280px;
|
||||
--page-pad-x: clamp(1rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
/* ── Reset ──────────────────────────────────────────────────────────── */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
font-feature-settings:
|
||||
"kern" 1,
|
||||
"liga" 1,
|
||||
"calt" 1,
|
||||
"ss01" 1;
|
||||
/* German-specific OpenType: lining figures for tabular data, oldstyle
|
||||
elsewhere. Enabled per-element where useful. */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--face-serif);
|
||||
font-size: var(--size-body);
|
||||
line-height: 1.55;
|
||||
font-feature-settings:
|
||||
"kern" 1,
|
||||
"liga" 1,
|
||||
"calt" 1;
|
||||
/* Slight optical correction for warmly-tinted paper background. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 var(--space-md);
|
||||
max-width: var(--measure);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
margin: 0;
|
||||
font-family: var(--face-serif);
|
||||
font-weight: 800;
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.01em;
|
||||
font-variant-ligatures: discretionary-ligatures;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.18em;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Utility patterns we use repeatedly ─────────────────────────────── */
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--face-sans);
|
||||
font-size: var(--size-eyebrow);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-family: var(--face-sans);
|
||||
font-size: var(--size-caption);
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tabular {
|
||||
font-family: var(--face-mono);
|
||||
font-variant-numeric: tabular-nums lining-nums;
|
||||
font-feature-settings: "tnum" 1, "lnum" 1;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.001ms !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user