feat(ui): deadline banner with gradient blob bg, swap Lora→Aleo
Deploy / verify (push) Failing after 17s
Deploy / deploy (push) Has been skipped

- DeadlineBannerBlock: redesign nach Tailwind-UI-Vorlage (gray-50, blurry blobs, dismiss button)
- new .gradient-blob-bg utility (radial-gradient blobs via ::before/::after)
- apply blob bg to Header
- @theme: register --font-sans (Inter) + --font-serif (Aleo)
- replace @fontsource-variable/lora with /aleo for blockquote font
- Header: simplify active nav-link styling
- QuoteBlock: formatting + drop borderClass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-05 16:17:08 +02:00
parent 3f62473c32
commit 44197429d6
6 changed files with 2842 additions and 485 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
"vitest": "^2.1.9" "vitest": "^2.1.9"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/lora": "^5.2.8", "@fontsource-variable/aleo": "^5.2.7",
"@fontsource-variable/rubik": "^5.2.8", "@fontsource-variable/rubik": "^5.2.8",
"@fontsource/inter": "^5.2.8", "@fontsource/inter": "^5.2.8",
"@iconify/svelte": "^5.2.1", "@iconify/svelte": "^5.2.1",
+358 -320
View File
@@ -6,7 +6,7 @@
@import "@fontsource/inter/latin-500.css"; @import "@fontsource/inter/latin-500.css";
@import "@fontsource/inter/latin-600.css"; @import "@fontsource/inter/latin-600.css";
@import "@fontsource/inter/latin-700.css"; @import "@fontsource/inter/latin-700.css";
@import "@fontsource-variable/lora"; @import "@fontsource-variable/aleo";
@import "tailwindcss"; @import "tailwindcss";
@@ -15,327 +15,366 @@
========================================================================== */ ========================================================================== */
@theme { @theme {
/* Palette: Wald (Primary) */ /* Palette: Wald (Primary) */
--color-wald-50: #f0f5f1; --color-wald-50: #f0f5f1;
--color-wald-100: #d9e8dc; --color-wald-100: #d9e8dc;
--color-wald-200: #b3d1b9; --color-wald-200: #b3d1b9;
--color-wald-300: #7fb58a; --color-wald-300: #7fb58a;
--color-wald-400: #4a9960; --color-wald-400: #4a9960;
--color-wald-500: #2d7a45; --color-wald-500: #2d7a45;
--color-wald-600: #236335; --color-wald-600: #236335;
--color-wald-700: #1a4d28; --color-wald-700: #1a4d28;
--color-wald-800: #12361c; --color-wald-800: #12361c;
--color-wald-900: #0a2011; --color-wald-900: #0a2011;
/* Palette: Erde (Secondary) */ /* Palette: Erde (Secondary) */
--color-erde-50: #faf6f1; --color-erde-50: #faf6f1;
--color-erde-100: #f0e6d6; --color-erde-100: #f0e6d6;
--color-erde-200: #e0ccad; --color-erde-200: #e0ccad;
--color-erde-300: #c9a87a; --color-erde-300: #c9a87a;
--color-erde-400: #b08a52; --color-erde-400: #b08a52;
--color-erde-500: #8b6d3f; --color-erde-500: #8b6d3f;
--color-erde-600: #6e5530; --color-erde-600: #6e5530;
--color-erde-700: #524023; --color-erde-700: #524023;
--color-erde-800: #372b18; --color-erde-800: #372b18;
--color-erde-900: #1c160c; --color-erde-900: #1c160c;
/* Palette: Himmel (Accent / Links) */ /* Palette: Himmel (Accent / Links) */
--color-himmel-50: #f2f5f7; --color-himmel-50: #f2f5f7;
--color-himmel-100: #dce4ea; --color-himmel-100: #dce4ea;
--color-himmel-200: #b8c9d4; --color-himmel-200: #b8c9d4;
--color-himmel-300: #8aaabb; --color-himmel-300: #8aaabb;
--color-himmel-400: #5e8ba2; --color-himmel-400: #5e8ba2;
--color-himmel-500: #436e85; --color-himmel-500: #436e85;
--color-himmel-600: #34576a; --color-himmel-600: #34576a;
--color-himmel-700: #264150; --color-himmel-700: #264150;
--color-himmel-800: #1a2c36; --color-himmel-800: #1a2c36;
--color-himmel-900: #0e181e; --color-himmel-900: #0e181e;
/* Palette: Fire (Tertiary) */ /* Palette: Fire (Tertiary) */
--color-fire-50: #fdf2f1; --color-fire-50: #fdf2f1;
--color-fire-100: #fbe5e3; --color-fire-100: #fbe5e3;
--color-fire-200: #f7c9c5; --color-fire-200: #f7c9c5;
--color-fire-300: #f2a8a2; --color-fire-300: #f2a8a2;
--color-fire-400: #eb8078; --color-fire-400: #eb8078;
--color-fire-500: #e35651; --color-fire-500: #e35651;
--color-fire-600: #d93d36; --color-fire-600: #d93d36;
--color-fire-700: #c82e27; --color-fire-700: #c82e27;
--color-fire-800: #b5231e; --color-fire-800: #b5231e;
--color-fire-900: #981b18; --color-fire-900: #981b18;
--color-fire-950: #5d0f0b; --color-fire-950: #5d0f0b;
/* Palette: Stein (Neutrals) */ /* Palette: Stein (Neutrals) */
--color-stein-0: #ffffff; --color-stein-0: #ffffff;
--color-stein-50: #f7f8f7; --color-stein-50: #f7f8f7;
--color-stein-100: #eceeed; --color-stein-100: #eceeed;
--color-stein-200: #d5d8d6; --color-stein-200: #d5d8d6;
--color-stein-300: #b0b5b2; --color-stein-300: #b0b5b2;
--color-stein-400: #868c89; --color-stein-400: #868c89;
--color-stein-500: #636966; --color-stein-500: #636966;
--color-stein-600: #4a4f4c; --color-stein-600: #4a4f4c;
--color-stein-700: #333735; --color-stein-700: #333735;
--color-stein-800: #1f2221; --color-stein-800: #1f2221;
--color-stein-900: #0f1110; --color-stein-900: #0f1110;
/* Semantic: Success, Warning, Error, Info */ /* Semantic: Success, Warning, Error, Info */
--color-success: var(--color-wald-500); --color-success: var(--color-wald-500);
--color-success-subtle: var(--color-wald-50); --color-success-subtle: var(--color-wald-50);
--color-warning: #a6780a; --color-warning: #a6780a;
--color-warning-subtle: #fdf8ec; --color-warning-subtle: #fdf8ec;
--color-error: #b53629; --color-error: #b53629;
--color-error-subtle: #fdf2f1; --color-error-subtle: #fdf2f1;
--color-info: var(--color-himmel-500); --color-info: var(--color-himmel-500);
--color-info-subtle: var(--color-himmel-50); --color-info-subtle: var(--color-himmel-50);
/* Button (Primary = Wald) für @apply bg-btn-bg, text-btn-txt, bg-btn-hover-bg */ /* Button (Primary = Wald) für @apply bg-btn-bg, text-btn-txt, bg-btn-hover-bg */
--color-btn-bg: var(--color-wald-500); --color-btn-bg: var(--color-wald-500);
--color-btn-hover-bg: var(--color-wald-600); --color-btn-hover-bg: var(--color-wald-600);
--color-btn-txt: #fff; --color-btn-txt: #fff;
/* Link (Himmel) für @apply text-link */ /* Link (Himmel) für @apply text-link */
--color-link: var(--color-himmel-500); --color-link: var(--color-himmel-500);
/* Fonts Tailwind-Utilities: font-sans (Inter, default), font-serif (Aleo, Zitate) */
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-serif: "Aleo Variable", Aleo, Georgia, "Times New Roman", serif;
} }
:root { :root {
/* Typography Inter (primary), Lora (Zitate) */ /* Typography Inter (primary), Aleo (Zitate) */
--font-body: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; --font-body: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-secondary: "Lora Variable", Lora, Georgia, "Times New Roman", serif; --font-secondary: "Aleo Variable", Aleo, Georgia, "Times New Roman", serif;
/* Semantic (nur Light Theme) */ /* Semantic (nur Light Theme) */
--bg-primary: #ffffff; --bg-primary: #ffffff;
--bg-secondary: #f7f8f7; --bg-secondary: #f7f8f7;
--bg-tertiary: #eceeed; --bg-tertiary: #eceeed;
--bg-elevated: #ffffff; --bg-elevated: #ffffff;
--text-primary: #1f2221; --text-primary: #1f2221;
--text-secondary: #636966; --text-secondary: #636966;
--text-tertiary: #868c89; --text-tertiary: #868c89;
--border-default: #d5d8d6; --border-default: #d5d8d6;
--border-strong: #b0b5b2; --border-strong: #b0b5b2;
--accent: #2d7a45; --accent: #2d7a45;
--accent-hover: #236335; --accent-hover: #236335;
--link: #436e85; --link: #436e85;
/* Legacy / App-Aliase (weiterverwendet) */ /* Legacy / App-Aliase (weiterverwendet) */
--color-font: var(--text-primary); --color-font: var(--text-primary);
--color-headings: var(--text-primary); --color-headings: var(--text-primary);
--color-font-highlight: var(--color-error); --color-font-highlight: var(--color-error);
--color-navigation: var(--color-stein-0); --color-navigation: var(--color-stein-0);
--background-color: var(--bg-primary); --background-color: var(--bg-primary);
--color-text: var(--text-primary); --color-text: var(--text-primary);
--color-link: var(--link); --color-link: var(--link);
--color-btn-bg: var(--accent); --color-btn-bg: var(--accent);
--color-btn-hover-bg: var(--accent-hover); --color-btn-hover-bg: var(--accent-hover);
--color-btn-txt: #fff; --color-btn-txt: #fff;
/* Breakout-Bereich: Verlauf Wald-50 → Wald-100 */ /* Breakout-Bereich: Verlauf Wald-50 → Wald-100 */
--color-container-breakout: linear-gradient( --color-container-breakout: linear-gradient(
to bottom, to bottom,
white, white,
var(--color-wald-50) var(--color-wald-50)
); );
font-family: var(--font-body); font-family: var(--font-body);
font-size: 1.125rem; /* 18px Body Large laut Design System */ font-size: 1.125rem; /* 18px Body Large laut Design System */
line-height: 1.556; line-height: 1.556;
color: var(--color-font); color: var(--color-font);
} }
html { html {
background: var(--background-color); background: var(--background-color);
} }
main { main {
background: var(--bg-primary); background: var(--bg-primary);
} }
/* ========================================================================== /* ==========================================================================
Typography Type Scale (02-typography.md) Typography Type Scale (02-typography.md)
Erlaubte Gewichte: 300, 400, 500, 600, 700. Inter primary, Lora nur Zitate. Erlaubte Gewichte: 300, 400, 500, 600, 700. Inter primary, Aleo nur Zitate.
========================================================================== */ ========================================================================== */
h1, h1,
h2, h2,
h3, h3,
h4 { h4 {
color: var(--color-headings); color: var(--color-headings);
} }
/* Mobile first, dann Tablet (768px), Desktop (1024px) */ /* Mobile first, dann Tablet (768px), Desktop (1024px) */
h1 { h1 {
font-size: 1.75rem; font-size: 1.75rem;
line-height: 2.125rem; line-height: 2.125rem;
font-weight: 700; font-weight: 700;
letter-spacing: -0.015em; letter-spacing: -0.015em;
} }
h2 { h2 {
font-size: 1.375rem; font-size: 1.375rem;
line-height: 1.75rem; line-height: 1.75rem;
font-weight: 700; font-weight: 700;
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
h3 { h3 {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.625rem; line-height: 1.625rem;
font-weight: 600; font-weight: 600;
letter-spacing: -0.005em; letter-spacing: -0.005em;
} }
h4 { h4 {
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.5rem; line-height: 1.5rem;
font-weight: 600; font-weight: 600;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
h1 { h1 {
font-size: 2.125rem; font-size: 2.125rem;
line-height: 2.625rem; line-height: 2.625rem;
} }
h2 { h2 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
} }
h3 { h3 {
font-size: 1.375rem; font-size: 1.375rem;
line-height: 1.875rem; line-height: 1.875rem;
} }
h4 { h4 {
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.625rem; line-height: 1.625rem;
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
line-height: 3rem; line-height: 3rem;
} }
h2 { h2 {
font-size: 1.75rem; font-size: 1.75rem;
line-height: 2.25rem; line-height: 2.25rem;
} }
h3 { h3 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
} }
} }
/* Page-Titel (wie www.windwiderstand.de) */ /* Page-Titel (wie www.windwiderstand.de) */
.pageTitle h1 { .pageTitle h1 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
font-weight: 700; font-weight: 700;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.pageTitle h2 { .pageTitle h2 {
font-size: 1rem; font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
font-weight: 500; font-weight: 500;
text-transform: none; text-transform: none;
letter-spacing: 0; letter-spacing: 0;
} }
.pageTitle h1, .pageTitle h1,
.pageTitle h2 { .pageTitle h2 {
text-wrap: balance; text-wrap: balance;
word-break: break-word; word-break: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.pageTitle strong { .pageTitle strong {
font-weight: 700; font-weight: 700;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.pageTitle h1 { .pageTitle h1 {
font-size: 2.25rem; font-size: 2.25rem;
line-height: 2.5rem; line-height: 2.5rem;
} }
.pageTitle h2 { .pageTitle h2 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
padding-left: 0.25rem; padding-left: 0.25rem;
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.pageTitle h1 { .pageTitle h1 {
font-size: 3.75rem; font-size: 3.75rem;
line-height: 1.14; line-height: 1.14;
font-weight: 700; font-weight: 700;
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
.pageTitle h2 { .pageTitle h2 {
font-size: 1.875rem; font-size: 1.875rem;
line-height: 2.25rem; line-height: 2.25rem;
padding-left: 0.25rem; padding-left: 0.25rem;
} }
} }
/* Content-Links: Himmel (Design System Links) */ /* Content-Links: Himmel (Design System Links) */
main a:not(.no-underline) { main a:not(.no-underline) {
color: var(--color-link); color: var(--color-link);
text-decoration: underline; text-decoration: underline;
} }
main a:not(.no-underline):hover { main a:not(.no-underline):hover {
color: var(--color-himmel-600); color: var(--color-himmel-600);
} }
/* Lange URLs umbrechen, damit sie nicht über den Bildschirmrand ragen */ /* Lange URLs umbrechen, damit sie nicht über den Bildschirmrand ragen */
main a, main a,
.markdown a { .markdown a {
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-all; word-break: break-all;
} }
main strong { main strong {
font-weight: 600; font-weight: 600;
} }
main ul:not(.not-prose) { main ul:not(.not-prose) {
margin-left: 1em; margin-left: 1em;
margin-bottom: 1em; margin-bottom: 1em;
} }
main ul:not(.not-prose) li { main ul:not(.not-prose) li {
list-style: disc; list-style: disc;
} }
main ol:not(.not-prose) { main ol:not(.not-prose) {
margin-left: 1.1em; margin-left: 1.1em;
margin-bottom: 1em; margin-bottom: 1em;
} }
main ol:not(.not-prose) li { main ol:not(.not-prose) li {
list-style-type: decimal; list-style-type: decimal;
} }
/* Buttons Wald (Primary) */ /* Buttons Wald (Primary) */
.btn-primary { .btn-primary {
@apply inline-block no-underline transition-colors duration-200 font-medium px-4 py-2 rounded-xs shadow-sm bg-btn-bg text-btn-txt; @apply inline-block no-underline transition-colors duration-200 font-medium px-4 py-2 rounded-xs shadow-sm bg-btn-bg text-btn-txt;
} }
.btn-primary:hover { .btn-primary:hover {
@apply bg-btn-hover-bg; @apply bg-btn-hover-bg;
} }
/* Buttons Stein outline (Secondary) */ /* Buttons Stein outline (Secondary) */
.btn-secondary { .btn-secondary {
@apply inline-block no-underline transition-colors duration-200 font-medium px-4 py-2 rounded-xs border border-stein-300 bg-white text-stein-700 shadow-sm; @apply inline-block no-underline transition-colors duration-200 font-medium px-4 py-2 rounded-xs border border-stein-300 bg-white text-stein-700 shadow-sm;
} }
.btn-secondary:hover { .btn-secondary:hover {
@apply bg-stein-100 border-stein-400; @apply bg-stein-100 border-stein-400;
}
/* Dekorativer Hintergrund mit zwei verschwommenen Verlaufs-Blobs (Wald → Erde).
Nutzung: Container mit relativer Positionierung; Blobs sitzen in ::before/::after.
Inhalt sollte z-Index >= 0 haben (Standard); Blobs liegen unter Stacking-Kontext. */
.gradient-blob-bg {
@apply relative isolate overflow-hidden;
}
.gradient-blob-bg::before,
.gradient-blob-bg::after {
content: "";
position: absolute;
top: 50%;
z-index: -10;
width: 36rem;
aspect-ratio: 577 / 310;
transform: translate3d(0, -50%, 0);
background: radial-gradient(
ellipse at center,
var(--color-himmel-400) 0%,
var(--color-himmel-200) 45%,
transparent 70%
);
opacity: 0.35;
filter: blur(40px);
pointer-events: none;
}
.gradient-blob-bg::before {
left: max(-7rem, calc(50% - 52rem));
}
.gradient-blob-bg::after {
left: max(45rem, calc(50% + 8rem));
} }
/* Globaler Focus-Ring wald-Akzent für Tastatur-Nutzer (a11y + Polish) */ /* Globaler Focus-Ring wald-Akzent für Tastatur-Nutzer (a11y + Polish) */
*:focus-visible { *:focus-visible {
outline: 2px solid var(--color-wald-500); outline: 2px solid var(--color-wald-500);
outline-offset: 2px; outline-offset: 2px;
border-radius: 2px; border-radius: 2px;
} }
/* Lead-Paragraph: erstes <p> in einem Top-Level MarkdownBlock innerhalb /* Lead-Paragraph: erstes <p> in einem Top-Level MarkdownBlock innerhalb
@@ -343,53 +382,53 @@ main ol:not(.not-prose) li {
damit weder Cards (.markdown in Callouts) noch Footer-MarkdownBlocks damit weder Cards (.markdown in Callouts) noch Footer-MarkdownBlocks
mit Copyright-Text getroffen werden. */ mit Copyright-Text getroffen werden. */
article [data-block-type="markdown"] .markdown > p:first-child { article [data-block-type="markdown"] .markdown > p:first-child {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.55; line-height: 1.55;
font-weight: 300; font-weight: 300;
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: 1.25em; margin-bottom: 1.25em;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
article [data-block-type="markdown"] .markdown > p:first-child { article [data-block-type="markdown"] .markdown > p:first-child {
font-size: 1.375rem; font-size: 1.375rem;
} }
} }
/* Content-Row-Trenner: subtiler wald-Verlauf zwischen content-rows (Forest-Identität) */ /* Content-Row-Trenner: subtiler wald-Verlauf zwischen content-rows (Forest-Identität) */
.content-row + .content-row { .content-row + .content-row {
position: relative; position: relative;
margin-top: 2rem; margin-top: 2rem;
} }
.content-row + .content-row::before { .content-row + .content-row::before {
content: ""; content: "";
display: block; display: block;
height: 1px; height: 1px;
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
transparent, transparent,
var(--color-wald-200), var(--color-wald-200),
transparent transparent
); );
margin: 0 auto 2rem; margin: 0 auto 2rem;
max-width: 50%; max-width: 50%;
} }
/* Content-Blöcke */ /* Content-Blöcke */
.content p { .content p {
@apply mb-4; @apply mb-4;
} }
.content h1, .content h1,
.content h2, .content h2,
.content h3, .content h3,
.content h4 { .content h4 {
@apply mb-2; @apply mb-2;
} }
/* Markdown: Abstände, Listen, Bilder, Tabellen (Stein-Palette) */ /* Markdown: Abstände, Listen, Bilder, Tabellen (Stein-Palette) */
.markdown { .markdown {
@apply min-w-0 overflow-x-auto; @apply min-w-0 overflow-x-auto;
} }
.markdown h1, .markdown h1,
@@ -397,164 +436,163 @@ article [data-block-type="markdown"] .markdown > p:first-child {
.markdown h3, .markdown h3,
.markdown h4, .markdown h4,
.markdown hr { .markdown hr {
@apply mt-4 mb-3; @apply mt-4 mb-3;
} }
.markdown hr { .markdown hr {
@apply border-stein-200 border-dotted mb-4; @apply border-stein-200 border-dotted mb-4;
} }
.markdown li, .markdown li,
.markdown li p { .markdown li p {
@apply mt-0 mb-0; @apply mt-0 mb-0;
} }
.markdown p { .markdown p {
@apply mb-4; @apply mb-4;
} }
.markdown ul, .markdown ul,
.markdown ol { .markdown ol {
@apply mt-0 mb-4; @apply mt-0 mb-4;
} }
.markdown li ul, .markdown li ul,
.markdown li ol { .markdown li ol {
@apply mb-0; @apply mb-0;
} }
/* Markdown-Bilder: /cms-images-Transform, max-height begrenzt Höhe; Link öffnet Bild in neuem Tab */ /* Markdown-Bilder: /cms-images-Transform, max-height begrenzt Höhe; Link öffnet Bild in neuem Tab */
.markdown img { .markdown img {
@apply max-w-full w-full my-2 border border-stein-200 rounded-xs bg-stein-200 shadow-sm inline-block; @apply max-w-full w-full my-2 border border-stein-200 rounded-xs bg-stein-200 shadow-sm inline-block;
max-height: 30vh; max-height: 30vh;
} }
.markdown .markdown-image-link { .markdown .markdown-image-link {
@apply inline-block; @apply inline-block;
} }
.markdown table { .markdown table {
@apply w-full min-w-max border-collapse text-left text-sm mb-4; @apply w-full min-w-max border-collapse text-left text-sm mb-4;
} }
.markdown thead { .markdown thead {
@apply bg-stein-100; @apply bg-stein-100;
} }
.markdown th { .markdown th {
@apply px-3 py-2 font-medium border border-stein-200; @apply px-3 py-2 font-medium border border-stein-200;
} }
.markdown td { .markdown td {
@apply px-3 py-2 border border-stein-200; @apply px-3 py-2 border border-stein-200;
} }
.markdown tbody tr:nth-child(even) { .markdown tbody tr:nth-child(even) {
@apply bg-stein-50; @apply bg-stein-50;
} }
.markdown tbody tr:hover { .markdown tbody tr:hover {
@apply bg-stein-100; @apply bg-stein-100;
} }
.markdown table a { .markdown table a {
@apply text-link underline underline-offset-2 break-all; @apply text-link underline underline-offset-2 break-all;
} }
.markdown table a:hover { .markdown table a:hover {
@apply text-himmel-600; @apply text-himmel-600;
} }
/* Code: Inline und Block (pre/code) lesbare Größe, Stein-Palette */ /* Code: Inline und Block (pre/code) lesbare Größe, Stein-Palette */
main code, main code,
.content code, .content code,
.markdown code { .markdown code {
@apply font-mono bg-stein-100 text-stein-800 border border-stein-200; @apply font-mono bg-stein-100 text-stein-800 border border-stein-200;
@apply text-sm px-1.5 py-0.5; @apply text-sm px-1.5 py-0.5;
} }
main pre, main pre,
.content pre, .content pre,
.markdown pre { .markdown pre {
@apply mt-3 mb-4 overflow-x-auto rounded-xs border border-stein-200 bg-stein-100 px-4 py-3 text-stein-800 leading-normal; @apply mt-3 mb-4 overflow-x-auto rounded-xs border border-stein-200 bg-stein-100 px-4 py-3 text-stein-800 leading-normal;
} }
main pre code, main pre code,
.content pre code, .content pre code,
.markdown pre code { .markdown pre code {
@apply border-0 bg-transparent p-0 text-inherit; @apply border-0 bg-transparent p-0 text-inherit;
} }
/* Container */ /* Container */
.container-custom { .container-custom {
@apply w-full mx-auto px-4 md:px-6; @apply w-full mx-auto px-4 md:px-6;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.container-custom { .container-custom {
max-width: 768px; max-width: 768px;
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.container-custom { .container-custom {
max-width: 1024px; max-width: 1024px;
} }
} }
@media (min-width: 1280px) { @media (min-width: 1280px) {
.container-custom { .container-custom {
max-width: 1280px; max-width: 1280px;
} }
} }
/* Zitate: Lora (Design System nur für Zitate) */ /* Zitate: Aleo (Design System nur für Zitate) */
blockquote[data-block-type="quote"], blockquote[data-block-type="quote"],
blockquote[data-block-type="quote"] p, blockquote[data-block-type="quote"] p,
blockquote[data-block-type="quote"] cite { blockquote[data-block-type="quote"] cite {
font-family: var(--font-secondary); font-family: var(--font-secondary);
} }
/* Footer */ /* Footer */
footer a, footer a,
.content-footer a { .content-footer a {
color: var(--color-link); color: var(--color-link);
} }
footer a:hover, footer a:hover,
.content-footer a:hover { .content-footer a:hover {
color: var(--color-himmel-600); color: var(--color-himmel-600);
} }
.container-breakout { .container-breakout {
margin-left: calc(50% - 50vw + 0.01rem); margin-left: calc(50% - 50vw + 0.01rem);
margin-right: calc(50% - 50vw + 0.01rem); margin-right: calc(50% - 50vw + 0.01rem);
background: var(--color-container-breakout); background: var(--color-container-breakout);
} }
/* Kalender: Container Query links Kalender, rechts Eventblock ab ~500px Containerbreite */ /* Kalender: Container Query links Kalender, rechts Eventblock ab ~500px Containerbreite */
.calendar-block { .calendar-block {
container-type: inline-size; container-type: inline-size;
container-name: calendar; container-name: calendar;
} }
@container calendar (min-width: 600px) { @container calendar (min-width: 600px) {
.calendar-block .calendar-grid-wrapper { .calendar-block .calendar-grid-wrapper {
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
} }
.calendar-block .calendar-column { .calendar-block .calendar-column {
min-width: 300px; min-width: 300px;
max-width: 450px; max-width: 450px;
} }
.calendar-block .calendar-events-wrapper { .calendar-block .calendar-events-wrapper {
position: relative; position: relative;
} }
.calendar-block .calendar-events-panel { .calendar-block .calendar-events-panel {
position: absolute; position: absolute;
inset: 0; inset: 0;
border-top: none; border-top: none;
border-left: 1px solid var(--color-stein-200); border-left: 1px solid var(--color-stein-200);
} }
} }
/* ── Print rules ──────────────────────────────────────────────────────────── /* ── Print rules ────────────────────────────────────────────────────────────
@@ -563,35 +601,35 @@ footer a:hover,
set page margins, force black-on-white, expand links, and keep images set page margins, force black-on-white, expand links, and keep images
inside the page break. */ inside the page break. */
@media print { @media print {
@page { @page {
margin: 1.5cm; margin: 1.5cm;
} }
html, html,
body { body {
background: white \!important; background: white \!important;
color: black \!important; color: black \!important;
font-size: 11pt; font-size: 11pt;
} }
/* Show the URL after each anchor inside the article so printed pages stay /* Show the URL after each anchor inside the article so printed pages stay
useful when the screen-only links are gone. */ useful when the screen-only links are gone. */
article a[href]:not([href^="#"])::after { article a[href]:not([href^="#"])::after {
content: " (" attr(href) ")"; content: " (" attr(href) ")";
font-size: 0.85em; font-size: 0.85em;
color: #555; color: #555;
word-break: break-all; word-break: break-all;
} }
/* Don't break inside images, keep figures together with their captions. */ /* Don't break inside images, keep figures together with their captions. */
img, img,
figure { figure {
break-inside: avoid; break-inside: avoid;
max-width: 100% \!important; max-width: 100% \!important;
height: auto \!important; height: auto \!important;
} }
/* Keep headings with the next paragraph. */ /* Keep headings with the next paragraph. */
h1, h1,
h2, h2,
h3, h3,
h4 { h4 {
break-after: avoid; break-after: avoid;
} }
} }
+7 -6
View File
@@ -100,7 +100,7 @@
<header <header
id="horizontal-navigation" id="horizontal-navigation"
class="sticky top-0 z-20 border-b border-white/30 shadow-2xl backdrop-blur-md bg-wald-50" class="gradient-blob-bg sticky top-0 z-20 border-b border-white/30 shadow-2xl backdrop-blur-md bg-wald-50"
> >
<div class="flex items-center justify-between container-custom py-2"> <div class="flex items-center justify-between container-custom py-2">
<a <a
@@ -129,9 +129,7 @@
{@const active = isActiveLink(link.href, $page.url.pathname)} {@const active = isActiveLink(link.href, $page.url.pathname)}
<a <a
href={link.href} href={link.href}
class="text-[.7rem] py-1 transition-colors hover:text-wald-700 {active class="text-[.7rem] py-1 {active ? 'font-bold ' : ''}"
? 'font-bold text-wald-700 border-b-2 border-wald-600'
: 'border-b-2 border-transparent'}"
aria-current={active ? "page" : undefined} aria-current={active ? "page" : undefined}
> >
{link.label} {link.label}
@@ -242,13 +240,16 @@
aria-label={t(T.header_nav_aria)} aria-label={t(T.header_nav_aria)}
> >
{#each links as link} {#each links as link}
{@const active = isActiveLink(link.href, $page.url.pathname)} {@const active = isActiveLink(
link.href,
$page.url.pathname,
)}
<a <a
href={link.href} href={link.href}
class="block py-4 px-3 rounded-xs transition-colors text-base {active class="block py-4 px-3 rounded-xs transition-colors text-base {active
? 'font-bold text-wald-300 bg-wald-700/20 border-l-4 border-wald-400' ? 'font-bold text-wald-300 bg-wald-700/20 border-l-4 border-wald-400'
: 'text-stein-100 hover:text-stein-0 hover:bg-stein-0/10 border-l-4 border-transparent'}" : 'text-stein-100 hover:text-stein-0 hover:bg-stein-0/10 border-l-4 border-transparent'}"
aria-current={active ? 'page' : undefined} aria-current={active ? "page" : undefined}
onclick={closeMenu} onclick={closeMenu}
> >
{link.label} {link.label}
@@ -1,149 +1,147 @@
<script lang="ts"> <script lang="ts">
import { getBlockLayoutClasses } from "$lib/block-layout"; import { getBlockLayoutClasses } from "$lib/block-layout";
import type { DeadlineBannerBlockData } from "$lib/block-types"; import type { DeadlineBannerBlockData } from "$lib/block-types";
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte";
import "$lib/iconify-offline"; import "$lib/iconify-offline";
let { block }: { block: DeadlineBannerBlockData } = $props(); let { block }: { block: DeadlineBannerBlockData } = $props();
const layoutClasses = $derived(getBlockLayoutClasses(block.layout)); const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
const variant = $derived(block.variant ?? "accent"); const showCountdown = $derived(block.showCountdown !== false);
const showCountdown = $derived(block.showCountdown !== false);
type ResolvedItem = { let dismissed = $state(false);
title?: string;
terminZeit?: string;
description?: string;
link?: DeadlineBannerBlockData["link"];
};
const autoItem = $derived.by((): ResolvedItem | null => { type ResolvedItem = {
if (block.mode !== "auto") return null; title?: string;
const now = Date.now(); terminZeit?: string;
const items = (block.items ?? []) description?: string;
.filter( link?: DeadlineBannerBlockData["link"];
(x): x is ResolvedItem => };
typeof x === "object" && x !== null && "terminZeit" in x,
)
.map((x): { x: ResolvedItem; ts: number } => ({ x, ts: Date.parse(x.terminZeit ?? "") }))
.filter(({ ts }) => !isNaN(ts) && ts >= now)
.sort((a, b) => a.ts - b.ts);
return items[0]?.x ?? null;
});
const text = $derived( const autoItem = $derived.by((): ResolvedItem | null => {
block.mode === "auto" ? autoItem?.title ?? "" : block.text ?? "", if (block.mode !== "auto") return null;
); const now = Date.now();
const dateIso = $derived( const items = (block.items ?? [])
block.mode === "auto" ? autoItem?.terminZeit ?? "" : block.date ?? "", .filter(
); (x): x is ResolvedItem =>
const linkValue = $derived( typeof x === "object" && x !== null && "terminZeit" in x,
block.mode === "auto" ? autoItem?.link : block.link, )
); .map((x): { x: ResolvedItem; ts: number } => ({
x,
const dateObj = $derived.by(() => { ts: Date.parse(x.terminZeit ?? ""),
if (!dateIso) return null; }))
const d = new Date(dateIso); .filter(({ ts }) => !isNaN(ts) && ts >= now)
return isNaN(d.getTime()) ? null : d; .sort((a, b) => a.ts - b.ts);
}); return items[0]?.x ?? null;
const dateStr = $derived.by(() => {
if (!dateObj) return "";
return dateObj.toLocaleDateString("de-DE", {
day: "numeric",
month: "long",
}); });
});
const daysLeft = $derived.by(() => { const text = $derived(
if (!dateObj) return null; block.mode === "auto" ? (autoItem?.title ?? "") : (block.text ?? ""),
const ms = dateObj.getTime() - Date.now(); );
if (ms < 0) return null; const dateIso = $derived(
return Math.ceil(ms / (1000 * 60 * 60 * 24)); block.mode === "auto"
}); ? (autoItem?.terminZeit ?? "")
: (block.date ?? ""),
);
const linkValue = $derived(
block.mode === "auto" ? autoItem?.link : block.link,
);
const countdownStr = $derived.by(() => { const dateObj = $derived.by(() => {
if (!showCountdown || daysLeft == null) return ""; if (!dateIso) return null;
if (daysLeft === 0) return "Heute"; const d = new Date(dateIso);
if (daysLeft === 1) return "Morgen"; return isNaN(d.getTime()) ? null : d;
return `Noch ${daysLeft} Tage`; });
});
function getHref(link: DeadlineBannerBlockData["link"]): string { const dateStr = $derived.by(() => {
if (typeof link === "string" && link) return link; if (!dateObj) return "";
if (link && typeof link === "object" && link.url) return link.url; return dateObj.toLocaleDateString("de-DE", {
return ""; day: "numeric",
} month: "long",
const href = $derived(getHref(linkValue)); });
const newTab = $derived( });
linkValue && typeof linkValue === "object" ? !!linkValue.newTab : false,
);
const variantClasses = $derived( const daysLeft = $derived.by(() => {
variant === "urgent" if (!dateObj) return null;
? "bg-erde-100 text-erde-900" const ms = dateObj.getTime() - Date.now();
: variant === "info" if (ms < 0) return null;
? "bg-himmel-100 text-himmel-900" return Math.ceil(ms / (1000 * 60 * 60 * 24));
: "bg-wald-500 text-wald-50", });
);
const iconName = $derived( const countdownStr = $derived.by(() => {
variant === "urgent" ? "mdi:alert-circle" : "mdi:calendar-clock", if (!showCountdown || daysLeft == null) return "";
); if (daysLeft === 0) return "Heute";
const hasContent = $derived(!!(text || dateStr || countdownStr)); if (daysLeft === 1) return "Morgen";
return `Noch ${daysLeft} Tage`;
});
function getHref(link: DeadlineBannerBlockData["link"]): string {
if (typeof link === "string" && link) return link;
if (link && typeof link === "object" && link.url) return link.url;
return "";
}
const href = $derived(getHref(linkValue));
const newTab = $derived(
linkValue && typeof linkValue === "object" ? !!linkValue.newTab : false,
);
const ctaLabel = $derived(
linkValue && typeof linkValue === "object" && linkValue.linkName
? linkValue.linkName
: "Mehr erfahren",
);
const sentence = $derived.by(() => {
const parts: string[] = [];
if (text) parts.push(text);
if (dateStr) parts.push(`am ${dateStr}`);
return parts.join(" ");
});
const hasContent = $derived(!!(sentence || countdownStr));
</script> </script>
{#if hasContent} {#if hasContent && !dismissed}
<div <div
class="deadline-banner {layoutClasses}" class="deadline-banner {layoutClasses}"
data-block-type="deadline_banner" data-block-type="deadline_banner"
data-block-slug={block._slug} data-block-slug={block._slug}
> >
{#snippet inner()} <div
<div class="container-custom flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-1 px-4 py-2 text-xs sm:text-sm"> class="gradient-blob-bg flex items-center gap-x-6 bg-gray-50 px-6 py-2.5 sm:px-3.5"
{#if dateStr} >
<span class="inline-flex items-center rounded-xs bg-white/50 text-wald-900 px-2 py-0.5 text-sm sm:text-base font-bold whitespace-nowrap shrink-0"> <span class="hidden flex-1 sm:block" aria-hidden="true"></span>
<span class="inline-flex items-center gap-1.5"> <p class="my-0! text-sm/6 text-gray-900">
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" /> {sentence}{#if countdownStr}{sentence ? " · " : ""}<span
{dateStr} class="font-semibold">{countdownStr}</span
</span> >{/if}{#if href}
{#if countdownStr} {" "}<a
<span {href}
class="ml-0 inline-flex min-w-0 max-w-0 scale-[0.96] items-center overflow-hidden whitespace-nowrap rounded-xs border border-transparent py-0.5 text-[0.6rem] font-semibold uppercase tracking-wide opacity-0 transition-[max-width,margin,opacity,transform,padding,border-color] duration-200 ease-out group-hover:ml-2 group-hover:max-w-48 group-hover:border-wald-900 group-hover:px-2 group-hover:opacity-100 group-hover:scale-100 group-focus-within:ml-2 group-focus-within:max-w-48 group-focus-within:border-wald-900 group-focus-within:px-2 group-focus-within:opacity-100 group-focus-within:scale-100" target={newTab ? "_blank" : undefined}
> rel={newTab ? "noopener noreferrer" : undefined}
{countdownStr} class="font-semibold whitespace-nowrap text-gray-900 hover:text-gray-700"
</span> >
{/if} {ctaLabel}&nbsp;<Icon
</span> icon="mdi:arrow-right"
{:else} class="inline-block size-4 align-[-0.15em]"
<span class="inline-flex items-center gap-0 shrink-0"> aria-hidden="true"
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" /> />
{#if countdownStr} </a>
<span {/if}
class="ml-0 inline-flex min-w-0 max-w-0 shrink-0 scale-[0.96] items-center overflow-hidden whitespace-nowrap rounded-full px-0 py-0.5 text-xs font-semibold uppercase tracking-wide opacity-0 transition-[max-width,margin,opacity,transform,padding] duration-200 ease-out group-hover:ml-2 group-hover:max-w-48 group-hover:px-2 group-hover:opacity-100 group-hover:scale-100 group-focus-within:ml-2 group-focus-within:max-w-48 group-focus-within:px-2 group-focus-within:opacity-100 group-focus-within:scale-100" </p>
> <div class="flex flex-1 justify-end">
{countdownStr} <button
</span> type="button"
{/if} onclick={() => (dismissed = true)}
</span> class="-m-3 p-3 focus-visible:-outline-offset-4"
{/if} >
{#if text} <span class="sr-only">Schließen</span>
<span class="min-w-0 wrap-break-word line-clamp-2 sm:line-clamp-1 font-semibold text-shadow-sm">{text}</span> <Icon
{/if} icon="mdi:close"
</div> class="size-5 text-gray-900"
{/snippet} aria-hidden="true"
{#if href} />
<a </button>
href={href} </div>
target={newTab ? "_blank" : undefined} </div>
rel={newTab ? "noopener noreferrer" : undefined} </div>
class="group block no-underline text-inherit hover:no-underline transition-all hover:brightness-95 {variantClasses}"
>
{@render inner()}
</a>
{:else}
<div class="group {variantClasses}">
{@render inner()}
</div>
{/if}
</div>
{/if} {/if}
+36 -23
View File
@@ -1,34 +1,47 @@
<script lang="ts"> <script lang="ts">
import { getBlockLayoutClasses } from "$lib/block-layout"; import { getBlockLayoutClasses } from "$lib/block-layout";
import type { QuoteBlockData } from "$lib/block-types"; import type { QuoteBlockData } from "$lib/block-types";
let { let {
block, block,
bare = false, bare = false,
extraClass = "", extraClass = "",
}: { block: QuoteBlockData; bare?: boolean; extraClass?: string } = $props(); }: {
block: QuoteBlockData;
bare?: boolean;
extraClass?: string;
} = $props();
const layoutClasses = $derived(getBlockLayoutClasses(block.layout)); const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
const isCenter = $derived(block.variant === "center"); const isCenter = $derived(block.variant === "center");
const alignClass = $derived( const alignClass = $derived(
isCenter ? "text-center" : block.variant === "right" ? "text-right" : "text-left", isCenter
); ? "text-center"
const borderClass = $derived(isCenter ? "" : "border-l-4 border-wald-700 pl-4"); : block.variant === "right"
? "text-right"
: "text-left",
);
</script> </script>
{#snippet quoteMarkup()} {#snippet quoteMarkup()}
<blockquote class="{borderClass} {alignClass} {extraClass}"> <blockquote class="{alignClass} {extraClass}">
<p class="text-lg text-stein-700 mb-2! ">"{block.quote ?? ""}"</p> <p class="text-lg mb-2! font-serif">
{#if block.author} {block.quote ?? ""}
<cite class="mt-1 block not-italic text-xs text-stein-400">{block.author}</cite> </p>
{/if} {#if block.author}
</blockquote> <cite class="mt-1 block not-italic text-xs">{block.author}</cite>
{/if}
</blockquote>
{/snippet} {/snippet}
{#if bare} {#if bare}
{@render quoteMarkup()}
{:else}
<div class={layoutClasses} data-block-type="quote" data-block-slug={block._slug}>
{@render quoteMarkup()} {@render quoteMarkup()}
</div> {:else}
<div
class={layoutClasses}
data-block-type="quote"
data-block-slug={block._slug}
>
{@render quoteMarkup()}
</div>
{/if} {/if}
+2307
View File
File diff suppressed because it is too large Load Diff