Style editor Markdown with .markdown-body and add ::: design blocks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tleiningerandClaude Opus 5.5 committed 2026-10-01 12:52:51 +02:00
1 parent ca0b3c311c
commit 5eb36ad3e9
12 files changed
+683 -17

No files matched your search

+360
View File
@@ -37,6 +37,14 @@
not --color-accent, so white text stays readable across the whole sweep. */
--gradient-brand: linear-gradient(135deg, var(--color-header) 0%, #3a7a3e 100%);
/* Buttons in editor content (a lone mail link, links in an "aufruf" box): a
filled blue with white text. Own tokens, not --color-brand, because in dark
mode --color-brand turns light for link text, and white on that light blue
would fail WCAG AA. Both shades carry white at >= 5.5:1. */
--color-button: #2b6f9c;
--color-button-hover: #1f5477;
--color-on-button: #fff;
/* Headings. Its own token (not --color-brand) so headings and links can differ:
in dark mode headings become a soft white while links keep the brand blue. */
--color-heading: #2b6f9c;
@@ -123,6 +131,10 @@
--gradient-brand: linear-gradient(135deg, var(--color-header) 0%, #2f6b33 100%);
--color-button: #266f9c; /* the header blue: deep enough for white
text, calm on the dark page */
--color-button-hover: #1f5477;
--color-heading: #c9ced6; /* soft white, kept a step brighter than the
body text so headings still lead the page */
@@ -461,3 +473,351 @@ h1:focus {
padding-top: var(--space-2);
color: var(--color-text);
}
/* --- Editor content (.markdown-body) -------------------------------------------
Everything the editors write in PocketBase (page bodies, posts, FAQ answers, the
intros on /posts and /events) is Markdown rendered by Shared/Markdown.cs and
wrapped in .markdown-body. Editors cannot add classes or HTML, so the look of
their text comes entirely from these rules. Every selector is scoped to
.markdown-body so nothing here leaks into the site's own components, and the
rules must hold for any Markdown an editor might type, including none at all. */
/* The line icons the content styles need, as data URIs: no request, no foreign
host. Same Lucide paths as Shared/IconSet.cs (license notice there); CSS cannot
reach that C# set, so these are copies. Drawn in black and used as a mask over
background-color: currentColor, so each icon takes the colour of its text --
dark mode included -- just like the inline-SVG <Icon> component. */
.markdown-body {
--icon-external-link: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M10 14 21 3'/%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3C/svg%3E");
--icon-file-text: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z'/%3E%3Cpath d='M14 2v5a1 1 0 0 0 1 1h5'/%3E%3Cpath d='M10 9H8'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E");
--icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
--icon-mail: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7'/%3E%3Crect x='2' y='4' width='20' height='16' rx='2'/%3E%3C/svg%3E");
}
/* A block's last element needs no gap below: the surrounding card or page already
spaces it, and a trailing margin makes boxes look bottom-heavy. */
.markdown-body > :last-child,
.markdown-body div > :last-child {
margin-bottom: 0;
}
/* Section headings get a short green bar above them. The bodies are mostly a run
of ## sections, and the bar makes each start easy to spot when scrolling on a
phone. Decoration only: the heading text carries the meaning. */
.markdown-body h2::before {
content: "";
display: block;
width: 2.5rem;
height: 4px;
margin-bottom: var(--space-2);
border-radius: 2px;
background: var(--color-accent);
}
/* Lists: the reading width of paragraphs, a little air between items, and the
bullets and numbers in the green accent so lists tie into the palette. */
.markdown-body ul,
.markdown-body ol {
margin: 0 0 var(--space-3);
padding-left: 1.5rem;
max-width: 42rem;
}
.markdown-body li + li {
margin-top: var(--space-1);
}
.markdown-body li::marker {
color: var(--color-accent-dark);
font-weight: 700;
}
/* Quotes (e.g. a passage from the rules of procedure): an accent bar and a faint
green wash set them apart from the editor's own words. */
.markdown-body blockquote {
margin: 0 0 var(--space-3);
padding: var(--space-3) var(--space-4);
max-width: 42rem;
border-left: 4px solid var(--color-accent);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
background: var(--color-accent-tint);
}
/* Links: a soft underline that firms up on hover. Underlined at all (not colour
alone) so links stay recognisable for colour-blind visitors. */
.markdown-body a {
text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
text-decoration-thickness: 1px;
text-underline-offset: 3px;
transition: color var(--transition), text-decoration-color var(--transition);
}
.markdown-body a:hover {
text-decoration-color: currentColor;
}
/* Links that leave the site get a small external-link icon, so visitors know
before clicking. Editors write links to our own pages as relative paths
(/contact), so "absolute http(s) URL" means "another site". PDFs are left out:
they carry their own file icon below. */
.markdown-body a:is([href^="http://"], [href^="https://"]):not([href$=".pdf" i])::after {
content: "";
display: inline-block;
width: 0.8em;
height: 0.8em;
margin-left: 0.2em;
vertical-align: -0.05em;
background-color: currentColor;
mask: var(--icon-external-link) center / contain no-repeat;
}
/* PDF links inside running text: a file icon in front and a small "PDF" tag
behind, so visitors know a download (not a web page) opens. */
.markdown-body a[href$=".pdf" i]::before {
content: "";
display: inline-block;
width: 1em;
height: 1em;
margin-right: 0.25em;
vertical-align: -0.15em;
background-color: currentColor;
mask: var(--icon-file-text) center / contain no-repeat;
}
.markdown-body a[href$=".pdf" i]::after {
content: "PDF";
margin-left: 0.35em;
padding: 0 0.35em;
border-radius: 4px;
background: var(--color-brand-tint);
color: var(--color-text-soft);
font-size: 0.75em;
font-weight: 700;
letter-spacing: 0.04em;
}
/* A PDF link standing alone in its paragraph or list item becomes a file card;
this is what turns a plain list of documents on /downloads into a download
area. Markdown.cs marks such paragraphs/items .lone-link, because CSS cannot see
the text around a link. Two child paths: in a tight list the link sits directly
in the <li>, in a loose list inside a <p>. */
.markdown-body .lone-link > a[href$=".pdf" i],
.markdown-body .lone-link > p > a[href$=".pdf" i] {
display: flex;
align-items: center;
gap: var(--space-3);
max-width: 42rem;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
box-shadow: var(--shadow);
font-weight: 600;
text-decoration: none;
transition: box-shadow var(--transition), border-color var(--transition);
}
.markdown-body .lone-link > a[href$=".pdf" i]:hover,
.markdown-body .lone-link > p > a[href$=".pdf" i]:hover {
border-color: var(--color-brand);
box-shadow: var(--shadow-hover);
}
.markdown-body .lone-link > a[href$=".pdf" i]::before,
.markdown-body .lone-link > p > a[href$=".pdf" i]::before {
flex-shrink: 0;
width: 1.5rem;
height: 1.5rem;
margin: 0;
color: var(--color-accent-dark);
background-color: var(--color-accent-dark);
}
/* The "PDF" tag moves to the card's right edge. */
.markdown-body .lone-link > a[href$=".pdf" i]::after,
.markdown-body .lone-link > p > a[href$=".pdf" i]::after {
margin-left: auto;
}
/* A list of file cards needs no bullets and no indent: the cards are the
structure. */
.markdown-body li.lone-link:has(> a[href$=".pdf" i], > p > a[href$=".pdf" i]) {
list-style: none;
}
.markdown-body ul:has(> li.lone-link > a[href$=".pdf" i], > li.lone-link > p > a[href$=".pdf" i]) {
padding-left: 0;
}
/* Buttons: a mail link alone in its paragraph (the contact page's main action),
and every link inside an "aufruf" box. Filled with the button blue (see
--color-button for why not --color-brand); hover darkens it and adds a shadow,
nothing moves. */
.markdown-body .lone-link > a[href^="mailto:"],
.markdown-body .aufruf a {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
background: var(--color-button);
color: var(--color-on-button);
font-weight: 700;
text-decoration: none;
transition: background-color var(--transition), box-shadow var(--transition);
}
.markdown-body .lone-link > a[href^="mailto:"]:hover,
.markdown-body .aufruf a:hover {
background: var(--color-button-hover);
color: var(--color-on-button);
box-shadow: var(--shadow-hover);
}
/* The mail button leads with an envelope, so it reads as "write to us" at a
glance. */
.markdown-body .lone-link > a[href^="mailto:"]::before {
content: "";
width: 1.1em;
height: 1.1em;
background-color: currentColor;
mask: var(--icon-mail) center / contain no-repeat;
}
/* --- Design blocks (::: name ... :::) ------------------------------------------
Markdown.cs turns "::: name" into <div class="name">. The names are German
because editors type them (CLAUDE.md: values German). An unknown name has no
rule here and so simply reads as normal text -- a typo never breaks a page. */
/* ::: aufruf -- a highlighted call to action: a tinted box with a brand-blue edge.
Its links become buttons (rule above). */
.markdown-body .aufruf {
margin: 0 0 var(--space-4);
padding: var(--space-4);
max-width: 42rem;
border-left: 4px solid var(--color-brand);
border-radius: var(--radius);
background: var(--color-brand-tint);
}
/* ::: hinweis -- a quiet info box with an info icon in front. Green wash, not the
yellow highlight: it informs, it does not warn. The icon is decoration; the text
says what matters. */
.markdown-body .hinweis {
position: relative;
margin: 0 0 var(--space-4);
padding: var(--space-3) var(--space-4) var(--space-3) 3.25rem;
max-width: 42rem;
border-radius: var(--radius);
background: var(--color-accent-tint);
}
.markdown-body .hinweis::before {
content: "";
position: absolute;
top: calc(var(--space-3) + 0.2rem);
left: var(--space-4);
width: 1.25rem;
height: 1.25rem;
background-color: var(--color-accent-dark);
mask: var(--icon-info) center / contain no-repeat;
}
/* ::: kacheln and ::: kennzahlen -- a list laid out as a grid of cards. auto-fill
with a minimum width gives as many columns as fit (one on a phone) without any
breakpoint. */
.markdown-body :is(.kacheln, .kennzahlen) > ul {
display: grid;
gap: var(--space-3);
max-width: none;
margin: 0 0 var(--space-4);
padding: 0;
list-style: none;
}
.markdown-body .kacheln > ul {
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
}
.markdown-body .kennzahlen > ul {
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
}
/* The grid gap does the spacing; the list's own item spacing would only push the
later cards of the first row down. */
.markdown-body :is(.kacheln, .kennzahlen) li + li {
margin-top: 0;
}
/* Kacheln: each item a white card; a bold start ("- **Mensa** ...") becomes the
card's title on its own line. */
.markdown-body .kacheln li {
padding: var(--space-3) var(--space-4);
border-radius: var(--radius);
background: var(--color-bg);
box-shadow: var(--shadow);
}
.markdown-body .kacheln li > strong:first-child {
display: block;
margin-bottom: var(--space-1);
color: var(--color-heading);
}
/* Kennzahlen: "- **seit 1975** Gründung" -- the bold part becomes a big figure,
the rest a small caption beneath. Brand blue for the figure, not the green: the
green does not reach 3:1 on its own tint, the blue does. */
.markdown-body .kennzahlen li {
padding: var(--space-4) var(--space-3);
border-radius: var(--radius);
background: var(--color-accent-tint);
color: var(--color-text-soft);
text-align: center;
}
.markdown-body .kennzahlen li > strong:first-child {
display: block;
color: var(--color-brand);
font-size: 2rem;
font-weight: var(--font-weight-heading);
letter-spacing: var(--letter-spacing-heading);
line-height: 1.15;
}
/* Tables (pipe tables): not in the content yet, styled ahead of time. As a block
with overflow they scroll sideways on a phone instead of stretching the page. */
.markdown-body table {
display: block;
max-width: 100%;
overflow-x: auto;
margin: 0 0 var(--space-4);
border-collapse: collapse;
}
.markdown-body th,
.markdown-body td {
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border);
text-align: left;
vertical-align: top;
}
.markdown-body th {
background: var(--color-surface);
color: var(--color-heading);
font-weight: 700;
}
/* Images never overflow the column and get the site's rounded corners. */
.markdown-body img {
max-width: 100%;
height: auto;
border-radius: var(--radius-sm);
}
/* A horizontal rule as a calm thin line with room around it. */
.markdown-body hr {
margin: var(--space-5) 0;
border: none;
border-top: 1px solid var(--color-border);
}