Files
Elternbeirat/Elternbeirat.Web/wwwroot/app.css
T

1143 lines
43 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Design tokens: one place for colours, spacing, radius, shadow and type. Every
component pulls from these variables (var(--...)), so the whole site shares one
look and a change here ripples everywhere. Palette follows the school's own
colours (IGMH): its blue for the header, links and headings, its green as the
accent. No external anything -- just CSS custom properties. */
:root {
/* Brand blue and two derived shades (darker for hover, lighter for tints). This
is the IGMH blue (#328fc6) darkened so it stays legible as link/heading text
on white; the header keeps the brighter original via --color-header below. */
--color-brand: #2b6f9c;
--color-brand-dark: #1f5477;
--color-brand-tint: #eaf3f9;
/* The header bar's background: the IGMH blue, eased one step deeper. The
original #328fc6 carries white menu text at only 3.6:1, below AA for normal
text; #2a77a6 is the same hue at 4.9:1. Its own token (not --color-brand)
because it only ever carries white text and must stay a deep blue in dark
mode, where --color-brand is lightened for text/links. */
--color-header: #2a77a6;
/* Text and menu states on the header. White text, and two translucent dark
washes for the menu pills: they deepen whatever part of the gradient sits
behind a link, so one value works from the blue end to the green end and the
white text gains contrast instead of losing it (a white wash dropped the
active pill to 2.7:1). The active pill is the stronger one, so "where am I"
outranks "where is the mouse". */
--color-on-header: #fff;
--color-nav-hover: rgba(0, 0, 0, 0.12);
--color-nav-active: rgba(0, 0, 0, 0.2);
/* Footer: a dark brand blue, so the page visibly ends there. Light text in two
steps (headings and the name brighter, links softer); both stay >= 7:1 on the
band. The border token is a faint white line for the copyright rule. */
--color-footer: #1a3d57;
--color-on-footer: #f1f5f9;
--color-on-footer-soft: #c5d3df;
--color-footer-border: rgba(255, 255, 255, 0.15);
/* Accent for highlights (active nav, event dates, call-outs): the IGMH green.
The -dark shade is darkened enough to keep small text and the FAQ marker
legible on a white card. */
--color-accent: #6db863;
--color-accent-dark: #4f9247;
/* A whisper of the green for calm background areas (info boxes, highlighted
rows). Light enough that body text keeps its full contrast on top. */
--color-accent-tint: #eef7ec;
/* Warm yellow for the rare thing that must catch the eye (a "new" marker, a
deadline). Background only, never text -- yellow text is unreadable on white.
Used sparingly: if everything is highlighted, nothing is. */
--color-highlight: #fdf0c2;
/* Brand gradient for large surfaces (hero, banner): the header blue flowing into
the IGMH green. The green end is a deep shade (#3a7a3e, 5.2:1 with white),
not --color-accent, so white text stays readable across the whole sweep. */
--gradient-brand: linear-gradient(135deg, var(--color-header) 0%, #3a7a3e 100%);
/* The month band of the calendar sheet (CalendarSheet): the deep green of the
gradient's end, because it carries small white text -- on --color-accent that
would fall below AA. Same 5.2:1 as in the gradient. */
--color-calendar-band: #3a7a3e;
/* 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;
/* The light button for dark surfaces (the brand gradient of the home hero),
where a filled blue button would melt into the blue end. Dark blue text on
white, 8:1; the hover is a faint blue tint, so the button still reads as one. */
--color-button-light: #fff;
--color-button-light-hover: #eaf3f9;
--color-on-button-light: #1f5477;
/* Initials avatars on the person cards (::: team, see TeamMember.cs): only the
two brand colours, blue and green tint, so the board stays in the IGMH palette.
The colour is picked from the name, so it means nothing -- it only helps tell
the cards apart. The tints follow their tokens into dark mode; the green text
needs its own shade, because --color-accent-dark is too light for small text
on the tint (3.6:1). Blue 7.2:1, green 5.9:1. */
--color-avatar-1: var(--color-brand-tint);
--color-on-avatar-1: var(--color-brand-dark);
--color-avatar-2: var(--color-accent-tint);
--color-on-avatar-2: #2f6b33;
/* Keyboard focus ring (:focus-visible). Its own token so it can stay strong on
the light page (dark blue, 8:1 on white) and flip to a light blue on the dark
one; on the gradient the hero switches it to white. */
--color-focus: #1f5477;
/* The focus ring on the dark brand surfaces (header, hero gradient, footer),
where the dark ring above would vanish: white in both schemes, >= 4.9:1 on
every one of them. */
--color-focus-inverse: #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;
/* Text and surfaces. */
--color-text: #222;
--color-text-soft: #555;
--color-bg: #fff; /* cards / content surface */
--color-page: #f4f6f4; /* near-neutral page background with a faint cool
tint, so white cards lift off it without the old
warm beige clashing with the cool blue */
--color-surface: #fafafa;
--color-border: #e0e0e0;
/* Spacing scale -- use these instead of ad-hoc rem values so gaps stay even. */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2rem;
--space-6: 3rem;
/* Rounded corners and a soft shadow for cards, matching the friendly tone. */
--radius: 12px;
--radius-sm: 6px;
--radius-lg: 20px; /* large surfaces (hero, banner) look stiff with
the card radius at their size */
--shadow: 0 1px 3px rgba(31, 58, 95, 0.08), 0 4px 12px rgba(31, 58, 95, 0.06);
--shadow-hover: 0 2px 6px rgba(31, 58, 95, 0.12), 0 8px 20px rgba(31, 58, 95, 0.1);
--shadow-lg: 0 4px 12px rgba(31, 58, 95, 0.1), 0 16px 40px rgba(31, 58, 95, 0.12);
/* One transition speed for hovers/focus across the site. */
--transition: 0.15s ease;
/* Width of the page frame (header, content, footer). Wide enough for card grids
side by side; running text does not follow it -- paragraphs keep their own
reading width (see p below), so lines never get uncomfortably long. */
--content-width: 72rem;
/* Type. The site font is self-hosted (see @font-face below); the system stack
stays behind it, so text shows at once and only swaps when Nunito arrives. */
--font-family: "Nunito", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
/* Type scale. h1 and h2 grow fluidly with the viewport (clamp: minimum, preferred
in vw, maximum) instead of jumping at a breakpoint, so a long German compound
word in a heading still fits a phone screen. Smaller sizes stay fixed: the
difference would not be visible. Body text sits a touch above the 1rem default
because Nunito's rounded letters read small. */
--font-size-h1: clamp(2rem, 1.6rem + 1.6vw, 2.75rem);
--font-size-h2: clamp(1.5rem, 1.35rem + 0.6vw, 1.75rem);
--font-size-h3: 1.2rem;
--font-size-body: 1.0625rem;
/* Headings: extra-bold for a friendly, confident lead, pulled slightly tighter
because heavy letters at large sizes otherwise look spaced out. */
--font-weight-heading: 800;
--letter-spacing-heading: -0.015em;
/* WCAG AA check of the colour pairs, light / dark (computed, October 2026).
Text needs 4.5:1, large text and focus rings/graphics 3:1.
text on card / page ............... 15.9 / 6.2 14.6 / 7.2
soft text on card / page .......... 7.5 / 6.2 6.9 / 7.2
soft text on green / blue tint .... 6.8 / 5.6 6.6 / 5.7
link (brand) on card / page ....... 5.4 / 7.5 5.0 / 8.7
link on green / blue tint ......... 5.0 / 6.7 4.8 / 6.9
heading on card / page ............ 5.4 / 9.3 5.0 / 10.9
white on header / gradient green .. 4.9 / 5.5 5.2 / 6.4
white on menu pill hover / active . 6.0 / 6.6 6.9 / 7.6
footer heading / link on footer ... 10.4 / 10.5 7.4 / 6.9
white on button / button hover .... 5.4 / 5.5 8.1 / 8.1
light button text ................. 8.1 / 9.7 (hover 7.2 / 8.3)
white on calendar band ............ 5.2 / 6.4
avatar blue / green ............... 7.2 / 8.6 5.9 / 6.7
focus ring on page / card ......... 7.5 / 11.0 8.1 / 9.4
white ring on header / footer ..... 4.9 / 5.5 11.4 / 16.6
green markers on card (graphic) ... 3.8 / 7.5
All pass. Exempt as pure decoration: the light green --color-accent (card
stripes, heading bars, the active-menu stroke), which never carries text. */
}
/* Dark mode: follows the visitor's OS setting automatically (prefers-color-scheme).
No toggle and no JavaScript -- the browser applies this when the operating system
is set to dark. Only the colour tokens change; layout, spacing, radius and type
stay put, so the whole site adapts from this one block. Choices: a deep blue-grey
rather than pure black; cards a touch lighter than the page so they still lift off
it; the accent nudged brighter so it doesn't glare; shadows darkened since soft
light shadows are invisible on a dark background. The header keeps its deep blue. */
@media (prefers-color-scheme: dark) {
:root {
--color-brand: #7cc0ec; /* the IGMH blue lightened: headings/links need
to stay bright enough to read on the dark
page background, not just on the card */
--color-brand-dark: #a3d5f4; /* hover goes lighter, not darker, on dark */
--color-brand-tint: #223038;
--color-header: #266f9c; /* the IGMH blue eased down, to sit calmly on
dark; white on it 5.5:1 */
--color-footer: #10202e; /* a deep night blue: darker than the page, so
the band still reads as the page's end */
--color-on-footer: #c9ced6; /* the heading tone, 10:1 on the band */
--color-on-footer-soft: #a2a8b2; /* the body text tone, 6.9:1 on the band */
--color-footer-border: rgba(255, 255, 255, 0.1);
--color-accent: #86c97d; /* the IGMH green nudged brighter for dark */
--color-accent-dark: #86c97d; /* no darkening needed on a dark surface */
--color-accent-tint: #24332a; /* a dark green-grey: tinted, but the soft
body text still reads at 5.5:1 */
--color-highlight: #3a3220; /* the warm yellow as a deep amber -- a bright
yellow would glare on the dark page */
--gradient-brand: linear-gradient(135deg, var(--color-header) 0%, #2f6b33 100%);
--color-calendar-band: #2f6b33; /* the gradient's dark-mode green, 6.4:1 with white */
--color-button: #266f9c; /* the header blue: deep enough for white
text, calm on the dark page */
--color-button-hover: #1f5477;
--color-button-light: #e8eef3; /* white eased down, so it does not glare
on the darker gradient */
--color-button-light-hover: #d3dee6;
--color-on-button-light: #1a3d57; /* 9:1 on the eased white */
/* Avatar green text: the brightened accent, 6.7:1 on the dark green tint
(the blue pair follows its tokens, 8.6:1). */
--color-on-avatar-2: var(--color-accent);
--color-focus: #a3d5f4; /* light blue: a dark ring would vanish here */
--color-heading: #c9ced6; /* soft white, kept a step brighter than the
body text so headings still lead the page */
--color-text: #a2a8b2; /* eased right down to the soft tone -- full
near-white reads as glare on the dark page */
--color-text-soft: #a2a8b2;
--color-bg: #222834; /* cards / content surface */
--color-page: #171b22; /* page background, darker than the cards */
--color-surface: #1c212a;
--color-border: #333b47;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.3);
--shadow-hover: 0 2px 6px rgba(0, 0, 0, 0.5), 0 8px 20px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.5), 0 16px 40px rgba(0, 0, 0, 0.45);
}
}
/* Nunito, served from our own wwwroot -- never from Google Fonts, which would hand
every visitor's IP to Google (see docs/recht.md). One variable file covers every
weight from 400 to 800, so body, bold and headings cost a single ~36 KB request
instead of three static files. Subset to Latin incl. German and the common
European names (ä ö ü ß é ç ğ ş ł ...), € and German quotes; anything outside falls
back to the system font. No italic file: Markdown emphasis is rare, so the browser
slanting the upright face is worth the saved request. font-display: swap shows the
system font immediately and swaps once the file is there -- text never waits.
License: SIL OFL 1.1, see fonts/OFL.txt. */
@font-face {
font-family: "Nunito";
src: url("fonts/nunito-latin-wght.woff2") format("woff2");
font-weight: 400 800;
font-style: normal;
font-display: swap;
}
/* Base styles. */
html {
font-family: var(--font-family);
line-height: 1.65;
color: var(--color-text);
/* Tell the browser we support both schemes, so form controls, scrollbars and
the native <details> marker follow the OS setting instead of staying light. */
color-scheme: light dark;
/* Smooth-scroll for in-page anchors, with room so headings clear the sticky
header instead of hiding under it. */
scroll-padding-top: 5rem;
}
body {
margin: 0;
/* A soft tinted page background so white cards/content lift off it. */
background: var(--color-page);
color: var(--color-text);
font-size: var(--font-size-body);
/* Full-width bands inside the content column (the home hero) reach the window
edges with 100vw, which includes the vertical scrollbar and would add a few
pixels of sideways scroll. clip, not hidden: hidden would make body a scroll
container and stop the sticky header from sticking. */
overflow-x: clip;
}
a {
color: var(--color-brand);
text-underline-offset: 2px;
transition: color var(--transition);
}
a:hover {
color: var(--color-brand-dark);
}
/* Headings: clear hierarchy and a touch more air above them than below, so
sections read as groups. */
h1, h2, h3, h4 {
line-height: 1.25;
color: var(--color-heading);
font-weight: var(--font-weight-heading);
letter-spacing: var(--letter-spacing-heading);
margin: var(--space-5) 0 var(--space-3);
}
h1 {
font-size: var(--font-size-h1);
line-height: 1.15; /* big type needs less leading, or wrapped lines drift apart */
margin-top: 0; /* the first heading on a page needs no gap above */
}
h2 {
font-size: var(--font-size-h2);
}
h3 {
font-size: var(--font-size-h3);
}
/* h4 gets its own size so it stays a step below h3 instead of falling back to the
browser default, which can render as large as (or larger than) a small h3. Body
size: below that it would read smaller than the text it heads; the extra-bold
weight alone sets it apart. */
h4 {
font-size: var(--font-size-body);
}
/* Comfortable paragraph rhythm and a capped line length for readability. */
p {
margin: 0 0 var(--space-3);
max-width: 42rem;
}
h1:focus {
outline: none;
}
/* Keyboard focus, one look for the whole site: a thick ring in --color-focus with a
little air between ring and element, so it never melts into a card's border or a
button's fill. :focus-visible, not :focus -- the ring shows for keyboard users
and stays away after a mouse click. Covers every focusable thing a page has:
links (and with them every card, which is one big link), buttons, the <summary>
of each disclosure, form fields and anything made focusable with tabindex. Dark
brand surfaces (header, hero, footer) switch the colour to
--color-focus-inverse. */
:is(a, button, summary, input, select, textarea, [tabindex]):focus-visible {
outline: 3px solid var(--color-focus);
outline-offset: 2px;
}
/* <main> takes focus from the skip link (tabindex="-1") only so the next Tab
continues inside the content; a ring around the whole page would just be noise. */
main:focus {
outline: none;
}
/* Skip link: the first thing on every page, so keyboard and screen-reader users
can jump past the header menu straight to the content. Off screen until it gets
focus, then it drops in at the top left over the sticky header -- a link that
is only hidden visually stays in the tab order, display:none would remove it. */
.skip-link {
position: absolute;
top: var(--space-2);
left: var(--space-2);
z-index: 100;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
background: var(--color-button-light);
box-shadow: var(--shadow-lg);
color: var(--color-on-button-light);
font-weight: 700;
text-decoration: none;
transform: translateY(-200%);
}
/* It lands on the header, so it takes the light button look and the white ring
of the dark brand surfaces. Shown instantly, no transition: it is a jump aid,
not an effect. */
.skip-link:focus {
transform: none;
outline-color: var(--color-focus-inverse);
}
.skip-link:hover {
color: var(--color-on-button-light);
}
/* Text for screen readers only: off screen visually, still read aloud. For places
where the visible form is decoration (the calendar sheet) and the spoken form
must carry the full information. Not display:none -- that hides it from screen
readers too. */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* Icons (Shared/Icon.razor) sit inline next to text: nudged down to line up with the
letters instead of the baseline, and never squeezed when they share a flex row
with a long label. */
.icon {
flex-shrink: 0;
vertical-align: -0.15em;
}
/* Buttons for links that are an action (the hero's "Kommende Termine", "Kontakt
aufnehmen"). Global, because the look must be the same wherever a component puts
one. Primary = filled, secondary = outlined; both carry the same 2px border, so a
primary and a secondary side by side are exactly the same height. Hover changes
colour and shadow only -- nothing moves (hover movement was removed on purpose). */
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border: 2px solid transparent;
border-radius: var(--radius-sm);
font-weight: 700;
line-height: 1.4;
text-decoration: none;
transition: background-color var(--transition), box-shadow var(--transition),
color var(--transition), border-color var(--transition);
}
.btn-primary {
background: var(--color-button);
border-color: var(--color-button);
color: var(--color-on-button);
}
.btn-primary:hover {
background: var(--color-button-hover);
border-color: var(--color-button-hover);
color: var(--color-on-button);
box-shadow: var(--shadow-hover);
}
.btn-secondary {
background: transparent;
border-color: var(--color-button);
color: var(--color-brand);
}
.btn-secondary:hover {
background: var(--color-brand-tint);
color: var(--color-brand);
}
/* Quiet = no frame at all, just the brand-blue label; hover lays the blue tint
behind it. For a way back ("Alle Beiträge") that should be easy to find but not
compete with the page's content. The negative inline margin lines the label up
with the text below while the hover tint still gets its padding. */
.btn-quiet {
margin-inline: calc(-1 * var(--space-3));
padding-inline: var(--space-3);
color: var(--color-brand);
}
.btn-quiet:hover {
background: var(--color-brand-tint);
color: var(--color-brand);
}
/* Header navigation links. These live here (global), not in MainLayout.razor.css:
NavLink renders plain <a> elements without a scoped-CSS attribute, so a scoped
".main-nav a" rule would not match them -- the links would fall back to the
global link colour (dark blue) and become unreadable on the blue header bar.
Every link is a pill with the same padding whether active or not, so switching
pages never shifts the row. Full white, not a softened white: the blue end of
the gradient already leaves little contrast headroom. */
.main-nav a {
position: relative;
padding: var(--space-1) var(--space-3);
border-radius: 999px;
color: var(--color-on-header);
text-decoration: none;
transition: background-color var(--transition);
}
.main-nav a:hover {
color: var(--color-on-header);
background: var(--color-nav-hover);
}
/* The dark default ring would vanish on the blue header (2.3:1); white stands out
on the whole gradient. */
.main-nav a:focus-visible {
outline-color: var(--color-focus-inverse);
}
/* NavLink adds .active to the current route's link: a stronger pill plus a short
green stroke under the text mark where the visitor is. The stroke is drawn by
::after, not a border, so it follows the text width inside the rounded pill. */
.main-nav a.active {
background: var(--color-nav-active);
}
.main-nav a.active::after {
content: "";
position: absolute;
left: var(--space-3);
right: var(--space-3);
bottom: 0.2rem;
height: 2px;
border-radius: 1px;
background: var(--color-accent);
}
/* Mobile: the stacked links in the open menu become full-width rows with a
rounded highlight instead of pills, and the green stroke shrinks to a short
mark at the start, so a long row does not get a long line. Same global
placement, same NavLink reason as above. */
@media (max-width: 40rem) {
.main-nav a {
padding: 0.6rem var(--space-3);
border-radius: var(--radius-sm);
}
.main-nav a.active::after {
right: auto;
width: 1.5rem;
}
}
/* --- Lists of post and event cards ------------------------------------------------
Posts and events appear both on their own page (PostList/EventList) AND embedded
on the home page (the *Embed components). The cards themselves are components
(PostCard, EventCard) with their own scoped styles; only the list around them is
shared here: no bullets, one column with even gaps. A page may widen the grid
(the posts page goes to two columns). */
.post-list,
.event-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: var(--space-3);
}
/* FAQ topics: each is a collapsible card. Its summary is the topic heading; open it
to reveal the optional intro and the questions (themselves collapsible .faq-item
cards). The topic marker is a rotating triangle, distinct from the +/- on the
questions nested inside, so the two accordion levels read apart. */
.faq-topic {
background: var(--color-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-3);
transition: box-shadow var(--transition);
}
.faq-topic:hover {
box-shadow: var(--shadow-hover);
}
/* A <details> with class "disclosure": the browser's own marker is replaced by a
triangle that turns down when open. Shared by the FAQ topics and the past events,
so "click to open" looks the same everywhere. */
.disclosure > summary {
cursor: pointer;
font-size: 1.15rem;
font-weight: 600;
color: var(--color-brand);
list-style: none;
display: flex;
align-items: baseline;
gap: var(--space-2);
}
.disclosure > summary::-webkit-details-marker {
display: none;
}
.disclosure > summary::before {
content: "▸";
color: var(--color-accent-dark);
font-size: 0.9rem;
}
.disclosure[open] > summary::before {
content: "▾";
}
/* Space the questions off the topic heading once the topic is open. */
.faq-topic[open] > .faq-item:first-of-type {
margin-top: var(--space-3);
}
.faq-topic-intro {
margin-top: var(--space-2);
color: var(--color-text-soft);
}
/* FAQ: each question is an expandable card with an accent +/– marker. */
.faq-item {
background: var(--color-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-3);
transition: box-shadow var(--transition);
}
.faq-item:hover {
box-shadow: var(--shadow-hover);
}
.faq-item summary {
cursor: pointer;
font-weight: 600;
color: var(--color-brand);
list-style: none;
display: flex;
align-items: baseline;
gap: var(--space-2);
}
.faq-item summary::-webkit-details-marker {
display: none;
}
.faq-item summary::before {
content: "+";
color: var(--color-accent-dark);
font-weight: 700;
font-size: 1.2rem;
line-height: 1;
}
.faq-item[open] summary::before {
content: "–";
}
.faq-answer {
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>. border-box: the 42rem include the
padding, so the card ends flush with the running text instead of overhanging
it (same for the aufruf and hinweis boxes below). */
.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);
box-sizing: border-box;
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 {
box-sizing: border-box; /* padding inside the 42rem, flush with the text */
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;
box-sizing: border-box; /* padding inside the 42rem, flush with the text */
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;
}
/* ::: team -- one person card per list item (Markdown.cs/TeamMember.cs build the
avatar, name, role chip, duty tags and note). Each "## Vorsitz" etc. above stays
a normal heading; every group is a block of its own. One raster for all of them,
so the cards line up across groups: a group of one person gets the same card
width as a group of six, not a stretched one. 17rem fits a long double name.
The cards of a row share one height (the grid's default stretch), so a row reads
as a calm band; the card itself keeps its content at the top (align-content). */
.markdown-body .team > ul {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
gap: var(--space-3);
max-width: none;
margin: 0 0 var(--space-4);
padding: 0;
list-style: none;
}
.markdown-body .team li + li {
margin-top: 0;
}
/* The card: avatar in a column of its own, everything else stacked beside it.
Hover only deepens the shadow -- nothing moves. */
.markdown-body .team-card {
display: grid;
grid-template-columns: auto 1fr;
column-gap: var(--space-3);
align-content: start;
padding: var(--space-3) var(--space-4);
border-radius: var(--radius);
background: var(--color-bg);
box-shadow: var(--shadow);
transition: box-shadow var(--transition);
}
.markdown-body .team-card:hover {
box-shadow: var(--shadow-hover);
}
/* In a loose list (blank lines between items) Markdig wraps the card's text in a
<p>; display: contents lets its parts join the card's grid all the same. */
.markdown-body .team-card > p:first-child {
display: contents;
}
/* Everything but the avatar goes into the text column -- including whatever else
an editor puts into the item (a second paragraph, a sub-list). */
.markdown-body .team-card > :not(.team-avatar),
.markdown-body .team-card > p:first-child > :not(.team-avatar) {
grid-column: 2;
}
/* A card without the convention (no bold name) is just its text. */
.markdown-body .team-card-plain {
display: block;
}
.markdown-body .team-avatar {
grid-row: 1 / span 4;
display: grid;
place-items: center;
width: 3rem;
height: 3rem;
border-radius: 50%;
font-weight: var(--font-weight-heading);
letter-spacing: 0.02em;
}
.markdown-body .team-avatar-1 { background: var(--color-avatar-1); color: var(--color-on-avatar-1); }
.markdown-body .team-avatar-2 { background: var(--color-avatar-2); color: var(--color-on-avatar-2); }
.markdown-body .team-name {
color: var(--color-heading);
font-size: 1.15rem;
line-height: 1.3;
}
/* The role as a chip under the name; justify-self keeps it as wide as its text,
not the column. */
.markdown-body .team-role {
justify-self: start;
margin-top: var(--space-1);
padding: 0.1rem var(--space-2);
border-radius: 999px;
background: var(--color-brand-tint);
color: var(--color-brand-dark);
font-size: 0.85rem;
font-weight: 700;
}
/* Duties as small outlined tags: quieter than the role chip, because the role
says who someone is and the duties only what they look after. A tag never breaks
inside ("Stellvertretende Schulkonferenz" stays one piece); the row wraps between
tags instead. */
.markdown-body .team-duties {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-top: var(--space-2);
}
.markdown-body .team-duty {
white-space: nowrap;
padding: 0.05rem var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-soft);
font-size: 0.85rem;
}
.markdown-body .team-note {
margin-top: var(--space-2);
color: var(--color-text-soft);
font-size: 0.95rem;
}
/* ::: team gross -- the chair. Same raster and card as every other group, so the
page keeps one rhythm; only the role chip stands out, filled in the button blue
instead of the pale tint (white on it 5.4:1, in dark mode too). */
.markdown-body .team.gross .team-role {
background: var(--color-button);
color: var(--color-on-button);
}
/* 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);
}
/* Reduced motion: visitors who asked their system for less movement get none.
The site has no animations, only short transitions (hover colours, the
hamburger turning into an X, the mobile menu folding open); these are cut to
an instant here. Near zero rather than none, so anything that waits for a
transition to end still gets its event. */
@media (prefers-reduced-motion: reduce) {
*,
::before,
::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
}