diff --git a/docs/typography.md b/docs/typography.md new file mode 100644 index 0000000..db2c0aa --- /dev/null +++ b/docs/typography.md @@ -0,0 +1,44 @@ +# Typo-Regelwerk – Windwiderstand + +Verbindliche Typografie für die komplette Seite. Umsetzung in `src/app.css`. + +## Schriften + +- **Alles in Inter.** Lora nur für Zitate (kursiv). Sonst nie. +- Erlaubte Gewichte: **300 / 400 / 500 / 600 / 700**. Nie 800/900. +- Immer Satzschreibung (sentence case). Kein ALL-CAPS außer kleinen Labels. +- Keine Ausrufezeichen. Keine Emoji. + +## Skala + +| Rolle | Größe | Gewicht | Line-height | Letter-spacing | +|---|---|---|---|---| +| Hero-H1 | clamp(38–66px) | 700 | 1.05 | -0.025em | +| Leitstatement | clamp(26–38px) | 600 | 1.2 | -0.02em | +| H2 Sektion | 34px | 600 | — | -0.02em | +| H3 Karte/Sub | 22px | 600 | — | -0.01em | +| Fließtext | 18px | 400 | 1.6 | — (bewusst groß & luftig) | +| Klein/Meta | 13–14px | 400–500 | — | — | +| Kicker-Label | 12px | 600 | — | 0.14em, UPPERCASE | + +## Schriftfarbe + +- Fließtext `#333735` (stein-700), Überschriften `#1f2221` (stein-800) — kein reines Schwarz +- Links `#436e85` (himmel-500), hover `#375a6d` +- Kicker / Nummer-Akzent `#2d7a45` (wald) + +## Betonung + +Über `` mit Gewicht **600** — nie kursiv, nie farbig. + +## Rhythmus + +- Nummerierte Sektionen (01–05) + Kicker-Label als Sektionsmarke +- `text-wrap: balance` auf Headlines +- Fließtext auf ~60–62ch begrenzen +- Leitstatement zusätzlich auf ~24ch begrenzen +- Großzügiger Weißraum, klare Hierarchie, keine Effekte + +## Weiterlesen-Link + +15px, Gewicht 600, Farbe himmel-500, dünne Unterlinie `#b8c9d4`, mit `mdi:arrow-right`. diff --git a/package-lock.json b/package-lock.json index b26cc87..82b8dd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@fontsource-variable/aleo": "^5.2.7", + "@fontsource-variable/lora": "^5.2.8", "@fontsource-variable/rubik": "^5.2.8", "@fontsource/inter": "^5.2.8", "@iconify/svelte": "^5.2.1", @@ -593,6 +594,15 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@fontsource-variable/lora": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/lora/-/lora-5.2.8.tgz", + "integrity": "sha512-cxjTJ9BbOWIzusewR4UMBLVePvTSWV6dtNaNsCkF/oKoyA68fJGWfaYCILOOP1BObE4dmjfZ3xo6m9hdHhtYhg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@fontsource-variable/rubik": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/@fontsource-variable/rubik/-/rubik-5.2.8.tgz", diff --git a/package.json b/package.json index 11278f3..f0fc553 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "@fontsource-variable/aleo": "^5.2.7", + "@fontsource-variable/lora": "^5.2.8", "@fontsource-variable/rubik": "^5.2.8", "@fontsource/inter": "^5.2.8", "@iconify/svelte": "^5.2.1", diff --git a/src/app.css b/src/app.css index 044d280..c124ce6 100644 --- a/src/app.css +++ b/src/app.css @@ -6,7 +6,7 @@ @import "@fontsource/inter/latin-500.css"; @import "@fontsource/inter/latin-600.css"; @import "@fontsource/inter/latin-700.css"; -@import "@fontsource-variable/aleo"; +@import "@fontsource-variable/lora"; @import "tailwindcss"; @@ -133,15 +133,15 @@ /* Link (Himmel) – für @apply text-link */ --color-link: var(--color-himmel-500); - /* Fonts – Tailwind-Utilities: font-sans (Inter, default), font-serif (Aleo, Zitate) */ + /* Fonts – Tailwind-Utilities: font-sans (Inter, default), font-serif (Lora, Zitate) */ --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; - --font-serif: "Aleo Variable", Aleo, Georgia, "Times New Roman", serif; + --font-serif: "Lora Variable", Lora, Georgia, "Times New Roman", serif; } :root { - /* Typography – Inter (primary), Aleo (Zitate) */ + /* Typography – Inter (primary), Lora (Zitate) */ --font-body: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; - --font-secondary: "Aleo Variable", Aleo, Georgia, "Times New Roman", serif; + --font-secondary: "Lora Variable", Lora, Georgia, "Times New Roman", serif; /* Semantic (nur Light Theme) */ --bg-primary: #ffffff; @@ -177,7 +177,7 @@ font-family: var(--font-body); font-size: 1.125rem; /* 18px – Body Large laut Design System */ - line-height: 1.556; + line-height: 1.6; color: var(--color-font); } @@ -199,28 +199,49 @@ h2, h3, h4 { color: var(--color-headings); + text-wrap: balance; +} + +/* Leitstatement – redaktioneller Aufhänger: größer als Fließtext, leichter als H2 */ +.leitstatement { + font-size: clamp(1.625rem, 3vw, 2.375rem); /* 26–38px */ + font-weight: 600; + line-height: 1.2; + letter-spacing: -0.02em; + color: var(--text-primary); + max-width: 24ch; + text-wrap: balance; +} + +/* Kicker – Sektionsmarke, z. B. „01 — ÜBER UNS" */ +.kicker { + font-size: 0.75rem; /* 12px */ + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent); /* wald */ } /* Mobile first, dann Tablet (768px), Desktop (1024px) */ h1 { font-size: 1.75rem; line-height: 2.125rem; - font-weight: 700; + font-weight: 600; letter-spacing: -0.015em; } h2 { - font-size: 1.375rem; - line-height: 1.75rem; - font-weight: 700; - letter-spacing: -0.01em; + font-size: 1.5rem; + line-height: 1.875rem; + font-weight: 600; + letter-spacing: -0.02em; } h3 { font-size: 1.25rem; line-height: 1.625rem; font-weight: 600; - letter-spacing: -0.005em; + letter-spacing: -0.01em; } h4 { @@ -235,8 +256,8 @@ h4 { line-height: 2.625rem; } h2 { - font-size: 1.5rem; - line-height: 2rem; + font-size: 1.75rem; + line-height: 2.25rem; } h3 { font-size: 1.375rem; @@ -254,12 +275,12 @@ h4 { line-height: 3rem; } h2 { - font-size: 1.75rem; - line-height: 2.25rem; + font-size: 2.125rem; + line-height: 2.625rem; } h3 { - font-size: 1.5rem; - line-height: 2rem; + font-size: 1.375rem; + line-height: 1.875rem; } } @@ -337,6 +358,29 @@ main a, overflow-wrap: anywhere; } +/* Weiterlesen-/„Mehr"-Links: ein Absatz, der NUR aus einem Link besteht, wird + * als kompakter CTA-Link gerendert (kleiner, dünne Unterlinie, Pfeil). + * Konvention: Link allein im Absatz = Weiterlesen-Link. `main …`-Prefix hebt + * die Spezifität über die generische Content-Link-Regel. */ +main .markdown p:has(> a:only-child) { + margin-top: 0.75rem; +} +main .markdown p > a:only-child { + font-size: 15px; /* px, da :root=18px → rem-Basis verschoben */ + font-weight: 600; + text-decoration-thickness: 1px; + text-decoration-color: #b8c9d4; + text-underline-offset: 3px; +} +/* nbsp + Pfeil bleiben im Textfluss → durchgehende Unterlinie ohne Bruch. */ +main .markdown p > a:only-child::after { + content: "\00a0→"; + font-weight: 400; +} +main .markdown p > a:only-child:hover { + text-decoration-color: currentColor; +} + main strong { font-weight: 600; } @@ -654,11 +698,12 @@ main pre code, } } -/* Zitate: Aleo (Design System – nur für Zitate) */ +/* Zitate: Lora kursiv (Design System – nur für Zitate) */ blockquote[data-block-type="quote"], blockquote[data-block-type="quote"] p, blockquote[data-block-type="quote"] cite { font-family: var(--font-secondary); + font-style: italic; } /* Footer */ diff --git a/src/lib/block-layout.ts b/src/lib/block-layout.ts index 7b15ec3..b917876 100644 --- a/src/lib/block-layout.ts +++ b/src/lib/block-layout.ts @@ -56,6 +56,8 @@ export type BlockLayout = { | "7" | "8" | "9" | "10" | "11" | "12"; /** Abstand nach unten (rem): 0, 0.5, 1, 1.5, 2. */ spaceBottom?: 0 | 0.5 | 1 | 1.5 | 2; + /** Abstand nach oben (rem): 0, 0.5, 1, 1.5, 2, 3, 4. */ + spaceTop?: 0 | 0.5 | 1 | 1.5 | 2 | 3 | 4; /** true = Block fullwidth aus dem Grid ausbrechen (viewport-breit). */ breakout?: boolean; }; @@ -127,6 +129,16 @@ const SPACE_BOTTOM: Record = { 2: "mb-8", // 2rem }; +const SPACE_TOP: Record = { + 0: "mt-0", + 0.5: "mt-2", // 0.5rem + 1: "mt-4", // 1rem + 1.5: "mt-6", // 1.5rem + 2: "mt-8", // 2rem + 3: "mt-12", // 3rem + 4: "mt-16", // 4rem +}; + /** * Gibt Tailwind-Klassen für das Block-Layout zurück (12-Spalten-Grid + Abstand). * Parent muss grid grid-cols-12 haben. @@ -148,8 +160,12 @@ export function getBlockLayoutClasses( : Number(layout.spaceBottom); const spaceClass = SPACE_BOTTOM[sb as keyof typeof SPACE_BOTTOM] ?? "mb-4"; + const st = + typeof layout.spaceTop === "number" ? layout.spaceTop : Number(layout.spaceTop); + const spaceTopClass = SPACE_TOP[st as keyof typeof SPACE_TOP] ?? ""; + if (breakout) { - return "w-full"; + return [spaceTopClass, "w-full"].filter(Boolean).join(" "); } const mobile = String(layout.mobile ?? "12"); @@ -164,6 +180,7 @@ export function getBlockLayoutClasses( desktop ? (LG_COL_SPAN[desktop] ?? "") : "", tabletStart ? (MD_COL_START[tabletStart] ?? "") : "", desktopStart ? (LG_COL_START[desktopStart] ?? "") : "", + spaceTopClass, spaceClass, ]; return parts.filter(Boolean).join(" "); diff --git a/src/lib/block-types.ts b/src/lib/block-types.ts index 33a1d72..ec4e4cf 100644 --- a/src/lib/block-types.ts +++ b/src/lib/block-types.ts @@ -238,8 +238,8 @@ export interface PostOverviewBlockData { excludeTag?: string[]; /** Max. Anzahl Einträge. */ numberItems?: number; - /** "list" | "cards" | "carousel". */ - design?: "list" | "cards" | "carousel"; + /** "list" | "cards" | "carousel" | "featured" (1. Post groß links, Rest kompakt rechts). */ + design?: "list" | "cards" | "carousel" | "featured"; layout?: BlockLayout; /** Nach resolvePostOverviewBlocks: geladene/gefilterte Posts (PostEntry[]). */ postsResolved?: unknown[]; diff --git a/src/lib/components/PostCardCompact.svelte b/src/lib/components/PostCardCompact.svelte new file mode 100644 index 0000000..ab82f49 --- /dev/null +++ b/src/lib/components/PostCardCompact.svelte @@ -0,0 +1,76 @@ + + + +
+ {#if rawImg} + + {:else if fallbackImg} + {post.headline + {:else} +
+ +
+ {/if} +
+
+
+ {dateStr}{#if tagNames.length} · {tagNames.join(", ")}{/if} +
+
+ {post.headline ?? post.linkName ?? post.slug ?? post._slug} +
+ {#if post.excerpt} +

{post.excerpt}

+ {/if} +
+
diff --git a/src/lib/components/blocks/CalendarCompactBlock.svelte b/src/lib/components/blocks/CalendarCompactBlock.svelte index 88e3b09..e2d42c4 100644 --- a/src/lib/components/blocks/CalendarCompactBlock.svelte +++ b/src/lib/components/blocks/CalendarCompactBlock.svelte @@ -41,7 +41,7 @@ return d.toLocaleDateString("de-DE", { weekday: "short", day: "2-digit", - month: "short", + month: "long", }); } function fmtTime(d: Date): string { @@ -51,6 +51,18 @@ return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); } + /** Titel + optional nachgestellte "(…)"-Klammer separat (Klammer gedimmt wie im Design). */ + function splitTitle(raw: string): { main: string; paren: string } { + const m = raw.match(/^(.*?)\s*(\([^()]*\))\s*$/); + if (m) return { main: m[1], paren: m[2] }; + return { main: raw, paren: "" }; + } + + function locationText(it: CalendarItemData): string { + if (!it.location) return ""; + return typeof it.location === "string" ? it.location : ((it.location as { text?: string })?.text ?? ""); + } + const linkHref = $derived(() => { const lt = block.linkTo; if (!lt) return null; @@ -62,50 +74,60 @@
-
-
+
+

{block.title ?? t(T.calendar_next_events)}

+ {#if linkHref()} + + {t(T.calendar_all_upcoming)} + + + {/if} +
{#if upcoming.length === 0} -

+

{t(T.calendar_no_future_events)}

{:else} -
    +
      {#each upcoming as { it, start } (it._slug ?? it.title)} {@const timeStr = fmtTime(start)} + {@const loc = locationText(it)} + {@const title = splitTitle(it.title ?? t(T.calendar_untitled))} {@const base = linkHref()} {@const itemHref = base && it._slug ? `${base}#event-${it._slug}` : null}
    • -
      - +
      + {start.toLocaleDateString("de-DE", { month: "short" }).replace(".", "")} - + {start.getDate()}
      -

      {it.title ?? t(T.calendar_untitled)}

      -

      - {fmtDate(start)}{timeStr ? ` · ${timeStr}` : ""} - {#if it.location} - · {typeof it.location === "string" ? it.location : (it.location as { text?: string })?.text ?? ""} - {/if} +

      + {title.main}{#if title.paren}{" "}{title.paren}{/if} +

      +

      +

      @@ -113,14 +135,4 @@ {/each}
    {/if} - - {#if linkHref()} - - {t(T.calendar_all_upcoming)} - - {/if}
diff --git a/src/lib/components/blocks/HeadlineBlock.svelte b/src/lib/components/blocks/HeadlineBlock.svelte index 913f7d3..f20a2da 100644 --- a/src/lib/components/blocks/HeadlineBlock.svelte +++ b/src/lib/components/blocks/HeadlineBlock.svelte @@ -13,6 +13,10 @@ : "text-left", ); const Tag = $derived((block.tag || "h2") as "h1" | "h2" | "h3" | "h4" | "h5" | "h6"); + + // Zeilenumbrüche (\n im CMS-Text) → harte
. Erlaubt bewusste Umbrüche in + // Headlines, ohne HTML im Text-Feld zuzulassen. + const lines = $derived((block.text ?? "").split("\n"));
@@ -20,6 +24,6 @@ this={Tag} class={alignClass} > - {block.text ?? ""} + {#each lines as line, i}{#if i > 0}
{/if}{line}{/each}
diff --git a/src/lib/components/blocks/OrganisationsBlock.svelte b/src/lib/components/blocks/OrganisationsBlock.svelte index 164a42b..bf358fd 100644 --- a/src/lib/components/blocks/OrganisationsBlock.svelte +++ b/src/lib/components/blocks/OrganisationsBlock.svelte @@ -141,16 +141,18 @@ href={addOrganisationCtaLink?.url} target={addOrganisationCtaLink?.newTab ? "_blank" : undefined} rel={addOrganisationCtaLink?.newTab ? "noopener noreferrer" : undefined} - class="h-full w-full max-w-full md:max-w-md" + class="h-full w-full max-w-full md:flex-row md:items-center md:justify-between md:gap-4" > - {#if block.addOrganisationTitle} -
{block.addOrganisationTitle}
- {/if} - {#if ctaHtml} -
{@html ctaHtml}
- {/if} +
+ {#if block.addOrganisationTitle} +
{block.addOrganisationTitle}
+ {/if} + {#if ctaHtml} +
{@html ctaHtml}
+ {/if} +
{#if addOrganisationCtaLink} -
+
diff --git a/src/lib/components/blocks/PostOverviewBlock.svelte b/src/lib/components/blocks/PostOverviewBlock.svelte index b79c747..14d919b 100644 --- a/src/lib/components/blocks/PostOverviewBlock.svelte +++ b/src/lib/components/blocks/PostOverviewBlock.svelte @@ -1,6 +1,7 @@ diff --git a/src/lib/iconify-lucide-subset.generated.json b/src/lib/iconify-lucide-subset.generated.json index bc9ce12..0c59b31 100644 --- a/src/lib/iconify-lucide-subset.generated.json +++ b/src/lib/iconify-lucide-subset.generated.json @@ -6,12 +6,6 @@ "chevron-right": { "body": "" }, - "search": { - "body": "" - }, - "x": { - "body": "" - }, "calendar": { "body": "" }, @@ -21,6 +15,15 @@ "calendar-clock": { "body": "" }, + "users": { + "body": "" + }, + "star": { + "body": "" + }, + "arrow-right": { + "body": "" + }, "rss": { "body": "" }, @@ -57,6 +60,9 @@ "mail": { "body": "" }, + "x": { + "body": "" + }, "map-pin": { "body": "" }, @@ -66,6 +72,9 @@ "external-link": { "body": "" }, + "search": { + "body": "" + }, "menu": { "body": "" }, @@ -87,6 +96,9 @@ "image": { "body": "" }, + "sparkles": { + "body": "" + }, "qr-code": { "body": "" }, @@ -117,9 +129,6 @@ "arrow-down-a-z": { "body": "" }, - "users": { - "body": "" - }, "copy": { "body": "" }, @@ -129,15 +138,15 @@ "zoom-in": { "body": "" }, - "arrow-right": { - "body": "" - }, "file-down": { "body": "" }, "calendar-search": { "body": "" }, + "calendar-check": { + "body": "" + }, "plus": { "body": "" },