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:
1 parent
ca0b3c311c
commit
5eb36ad3e9
12 files changed
+683
-17
No files matched your search
@@ -12,7 +12,7 @@
|
||||
|
||||
@if (_page is not null && !string.IsNullOrWhiteSpace(_page.Body))
|
||||
{
|
||||
<article>@Body</article>
|
||||
<article class="markdown-body">@Body</article>
|
||||
}
|
||||
|
||||
@if (_unavailable)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
@if (_page is not null && !string.IsNullOrWhiteSpace(_page.Body))
|
||||
{
|
||||
<article>@Body</article>
|
||||
<article class="markdown-body">@Body</article>
|
||||
}
|
||||
|
||||
@if (_unavailable)
|
||||
@@ -31,13 +31,13 @@ else
|
||||
<summary>@topic.Title</summary>
|
||||
@if (!string.IsNullOrWhiteSpace(topic.Intro))
|
||||
{
|
||||
<div class="faq-topic-intro">@((MarkupString)Markdown.ToHtml(topic.Intro))</div>
|
||||
<div class="faq-topic-intro markdown-body">@((MarkupString)Markdown.ToHtml(topic.Intro))</div>
|
||||
}
|
||||
@foreach (var faq in topic.Faqs)
|
||||
{
|
||||
<details class="faq-item">
|
||||
<summary>@faq.Question</summary>
|
||||
<div class="faq-answer">
|
||||
<div class="faq-answer markdown-body">
|
||||
@((MarkupString)Markdown.ToHtml(faq.Answer))
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -9,7 +9,7 @@ else if (_page is not null)
|
||||
{
|
||||
<PageTitle>@_page.Title</PageTitle>
|
||||
<h1>@_page.Title</h1>
|
||||
<article>@Body</article>
|
||||
<article class="markdown-body">@Body</article>
|
||||
<PageEmbeds Embed="_page.Embed" />
|
||||
}
|
||||
@* No page and not unavailable: the slug was unknown; NavigationManager.NotFound()
|
||||
|
||||
@@ -15,7 +15,7 @@ else if (_post is not null)
|
||||
<div class="post-content">
|
||||
<PostDate Value="_post.Date" />
|
||||
<h2>@_post.Title</h2>
|
||||
@Body
|
||||
<div class="markdown-body">@Body</div>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
@if (_page is not null && !string.IsNullOrWhiteSpace(_page.Body))
|
||||
{
|
||||
<article>@Body</article>
|
||||
<article class="markdown-body">@Body</article>
|
||||
}
|
||||
|
||||
@if (_unavailable)
|
||||
|
||||
@@ -1,23 +1,130 @@
|
||||
using Markdig;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Elternbeirat.Web.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Renders Markdown to HTML. Content now comes from PocketBase as raw Markdown
|
||||
/// (the <c>body</c> field), so the pages render it at display time instead of
|
||||
/// reading pre-rendered HTML from files.
|
||||
/// Renders the editors' Markdown (the <c>body</c>, <c>intro</c> and <c>answer</c>
|
||||
/// fields from PocketBase) to the HTML the pages show.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pipeline is CommonMark plus exactly two extensions, each there for a reason
|
||||
/// an editor can see:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <b>Custom containers</b> (<c>::: name</c> … <c>:::</c>) turn a block
|
||||
/// into <c><div class="name"></c>. That is how editors use the
|
||||
/// design blocks (<c>kennzahlen</c>, <c>aufruf</c>, <c>kacheln</c>,
|
||||
/// <c>hinweis</c>) without writing HTML. An unknown name just yields a
|
||||
/// div without styling, so a typo never breaks a page.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <b>Pipe tables</b>: plain CommonMark has no tables at all, so without
|
||||
/// this the table styles in <c>app.css</c> could never apply.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Raw HTML in the Markdown is <b>escaped</b>, not passed through
|
||||
/// (<see cref="MarkdownPipelineBuilder"/>'s <c>DisableHtml</c>). The result is
|
||||
/// rendered as a <c>MarkupString</c>, so a passed-through <c><script></c>
|
||||
/// would run in the visitor's browser, and the privacy text promises that no
|
||||
/// program code runs there. Editors style pages with the blocks instead.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// After parsing, a paragraph (or list item) that consists of nothing but one
|
||||
/// link gets the class <see cref="LoneLinkClass"/>. Pure CSS cannot tell
|
||||
/// "a link alone on its line" from "a link inside a sentence" (selectors ignore
|
||||
/// the text around an element), but the stylesheet needs exactly that to show a
|
||||
/// lone <c>mailto:</c> link as a button and a lone PDF link as a file card.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="IconSet"/>
|
||||
public static class Markdown
|
||||
{
|
||||
/// <summary>
|
||||
/// The class marking a paragraph or list item whose only content is one link.
|
||||
/// <c>app.css</c> keys the mail button and the PDF card off it.
|
||||
/// </summary>
|
||||
public const string LoneLinkClass = "lone-link";
|
||||
|
||||
private static readonly MarkdownPipeline Pipeline =
|
||||
new MarkdownPipelineBuilder().Build();
|
||||
new MarkdownPipelineBuilder()
|
||||
.UseCustomContainers()
|
||||
.UsePipeTables()
|
||||
.DisableHtml()
|
||||
.Build();
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Markdown string to HTML. Returns an empty string for
|
||||
/// <c>null</c> or empty input, so callers can bind the result directly.
|
||||
/// Converts a Markdown string to HTML.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The editor's Markdown; may be <see langword="null"/>.</param>
|
||||
/// <returns>
|
||||
/// The rendered HTML, or an empty string for <see langword="null"/> or empty
|
||||
/// input, so callers can bind the result directly.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// Markdown.ToHtml("::: hinweis\nBitte vormerken.\n:::");
|
||||
/// // <div class="hinweis"><p>Bitte vormerken.</p></div>
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static string ToHtml(string? markdown) =>
|
||||
string.IsNullOrEmpty(markdown)
|
||||
? ""
|
||||
: Markdig.Markdown.ToHtml(markdown, Pipeline);
|
||||
: Markdig.Markdown.ToHtml(MarkLoneLinks(Markdig.Markdown.Parse(markdown, Pipeline)), Pipeline);
|
||||
|
||||
/// <summary>
|
||||
/// Adds <see cref="LoneLinkClass"/> to every paragraph that holds nothing but
|
||||
/// one link.
|
||||
/// </summary>
|
||||
/// <param name="document">The parsed document; changed in place.</param>
|
||||
/// <returns>The same <paramref name="document"/>, for chaining.</returns>
|
||||
/// <remarks>
|
||||
/// Inside a list item the class goes on the item, not the paragraph: in a tight
|
||||
/// list Markdig writes no <c><p></c> at all, so a class on the paragraph
|
||||
/// would be silently dropped.
|
||||
/// </remarks>
|
||||
private static MarkdownDocument MarkLoneLinks(MarkdownDocument document)
|
||||
{
|
||||
foreach (var paragraph in document.Descendants<ParagraphBlock>().Where(IsLoneLink))
|
||||
{
|
||||
MarkdownObject target = paragraph.Parent is ListItemBlock { Count: 1 } item ? item : paragraph;
|
||||
target.GetAttributes().AddClass(LoneLinkClass);
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a paragraph's content is exactly one link, ignoring surrounding
|
||||
/// whitespace and line breaks.
|
||||
/// </summary>
|
||||
/// <param name="paragraph">The paragraph to inspect.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> for one link (inline <c>[text](url)</c> or an
|
||||
/// autolink <c><url></c>, but not an image) and nothing else.
|
||||
/// </returns>
|
||||
private static bool IsLoneLink(ParagraphBlock paragraph) =>
|
||||
paragraph.Inline?
|
||||
.Where(inline => !IsBlank(inline))
|
||||
.ToList() is [LinkInline { IsImage: false } or AutolinkInline];
|
||||
|
||||
/// <summary>
|
||||
/// Whether an inline carries no visible content (whitespace or a line break).
|
||||
/// </summary>
|
||||
/// <param name="inline">The inline to inspect.</param>
|
||||
/// <returns><see langword="true"/> if it can be ignored when looking for a lone link.</returns>
|
||||
private static bool IsBlank(Inline inline) =>
|
||||
inline switch
|
||||
{
|
||||
LineBreakInline => true,
|
||||
LiteralInline literal => literal.Content.IsEmptyOrWhitespace(),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user