Refresh header and footer with signet, gradient, nav pills and three-column footer; fix box-sizing of design boxes #53

Merged
Tom merged 1 commits from design/header-footer into main 2026-10-01 13:36:59 +02:00
10 changed files with 402 additions and 54 deletions

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
using Elternbeirat.Web.Features.Layout;
namespace Elternbeirat.Web.Tests;
/// <summary>
/// Unit tests for the footer's copyright year. Pure logic, no PocketBase needed.
/// </summary>
public class FooterYearTests
{
[Fact]
public void New_year_in_Berlin_wins_over_the_UTC_year()
{
// 23:30 UTC on New Year's Eve is already 00:30 in Berlin (winter time, +1),
// so the footer must show the new year although the server clock says the old.
var now = new DateTimeOffset(2026, 12, 31, 23, 30, 0, TimeSpan.Zero);
FooterYear.For(now).ShouldBe(2027);
}
[Fact]
public void Ordinary_day_keeps_its_year()
{
var now = new DateTimeOffset(2026, 10, 1, 12, 0, 0, TimeSpan.Zero);
FooterYear.For(now).ShouldBe(2026);
}
}
@@ -43,6 +43,15 @@ public class NavBuilderTests
footer.Select(link => link.Title).ShouldBe(["Impressum", "Datenschutz"]); footer.Select(link => link.Title).ShouldBe(["Impressum", "Datenschutz"]);
} }
[Fact]
public void No_pages_give_empty_menus()
{
// PocketBase down or nothing published yet: both menus are empty lists, not
// null, so the layout can simply skip a column instead of crashing.
NavBuilder.Header([]).ShouldBeEmpty();
NavBuilder.Footer([]).ShouldBeEmpty();
}
[Fact] [Fact]
public void Home_page_maps_to_root_every_other_slug_to_its_path() public void Home_page_maps_to_root_every_other_slug_to_its_path()
{ {
+37
View File
@@ -115,6 +115,30 @@ public sealed partial class RouteSmokeTests : IDisposable
html.ShouldNotContain("href=\"/draft\""); // public=false, filtered out html.ShouldNotContain("href=\"/draft\""); // public=false, filtered out
} }
[Fact]
public async Task Footer_links_every_public_header_and_footer_page_in_order()
{
// The footer repeats the header pages as "Schnellzugriff" and then lists the
// footer pages, each column in its order field. Only the <footer> is checked,
// so the header menu cannot make the test pass on its own. The expected list
// mirrors the fixture's pages; the draft (public=false) must be missing.
var client = _factory.CreateClient();
var html = await client.GetStringAsync(new Uri("/", UriKind.Relative));
var footer = FooterElement().Match(html);
footer.Success.ShouldBeTrue();
var hrefs = Href().Matches(footer.Value).Select(match => match.Groups[1].Value);
hrefs.ShouldBe(
[
"/", "/board", "/patrons", "/faqs", "/downloads", "/posts", "/events",
"/contact", "/imprint", "/privacy", "/faq-lunch",
]);
footer.Value.ShouldContain("Schnellzugriff");
footer.Value.ShouldContain("Weiteres");
}
[Fact] [Fact]
public async Task Home_renders_the_embeds_its_page_opts_into() public async Task Home_renders_the_embeds_its_page_opts_into()
{ {
@@ -245,4 +269,17 @@ public sealed partial class RouteSmokeTests : IDisposable
/// </summary> /// </summary>
[GeneratedRegex("""(?:\b(?:src|srcset)\s*=\s*|<link\b[^>]*\bhref\s*=\s*|url\(\s*|@import\s+)["']?\s*(?:https?:)?//""", RegexOptions.IgnoreCase)] [GeneratedRegex("""(?:\b(?:src|srcset)\s*=\s*|<link\b[^>]*\bhref\s*=\s*|url\(\s*|@import\s+)["']?\s*(?:https?:)?//""", RegexOptions.IgnoreCase)]
private static partial Regex ForeignResource(); private static partial Regex ForeignResource();
/// <summary>
/// Matches the page's <c>&lt;footer&gt;</c> element, from its opening tag to the
/// closing one. The layout renders exactly one footer and nests no other.
/// </summary>
[GeneratedRegex("""<footer\b.*?</footer>""", RegexOptions.Singleline)]
private static partial Regex FooterElement();
/// <summary>
/// Matches an <c>href</c> attribute and captures its value.
/// </summary>
[GeneratedRegex(@"\bhref=""([^""]*)""")]
private static partial Regex Href();
} }
@@ -8,7 +8,15 @@
flex wrapper -- see MainLayout.razor.css. *@ flex wrapper -- see MainLayout.razor.css. *@
<header class="site-header"> <header class="site-header">
<div class="header-inner"> <div class="header-inner">
<a class="brand" href="/">Elternbeirat IGMH</a> @* Two-line lettering next to the signet: the body's name large, the
school small below it. One link, so the whole mark leads home. *@
<a class="brand" href="/">
<Signet />
<span class="brand-text">
<span class="brand-name">Elternbeirat</span>
<span class="brand-school">IGMH Mannheim</span>
</span>
</a>
@* CSS-only mobile menu: the hidden checkbox holds the open/closed @* CSS-only mobile menu: the hidden checkbox holds the open/closed
state, the label is the hamburger button, and :checked reveals the state, the label is the hamburger button, and :checked reveals the
@@ -37,13 +45,48 @@
</main> </main>
<footer class="site-footer"> <footer class="site-footer">
@* A soft wave as the transition from the page into the dark footer. Pure
decoration, so hidden from screen readers; it is filled with the footer
colour and stretched to the full width (preserveAspectRatio="none"). *@
<svg class="footer-wave" viewBox="0 0 1440 48" preserveAspectRatio="none" aria-hidden="true" focusable="false">
<path d="M0 24C240 48 480 48 720 28S1200 0 1440 20V48H0z" />
</svg>
<div class="footer-inner"> <div class="footer-inner">
<div class="footer-brand">
<Signet />
<span>Elternbeirat der IGMH</span> <span>Elternbeirat der IGMH</span>
<nav class="footer-nav"> </div>
@* Both link columns come from PocketBase like the header. An empty list
(no such pages, or PocketBase down) renders nothing -- no lone
heading above an empty column. *@
@if (_header.Count > 0)
{
<nav class="footer-nav" aria-label="Schnellzugriff">
<h2 class="footer-heading">Schnellzugriff</h2>
<ul>
@foreach (var link in _header)
{
<li><a href="@link.Href">@link.Title</a></li>
}
</ul>
</nav>
}
@if (_footer.Count > 0)
{
<nav class="footer-nav" aria-label="Weiteres">
<h2 class="footer-heading">Weiteres</h2>
<ul>
@foreach (var link in _footer) @foreach (var link in _footer)
{ {
<a href="@link.Href">@link.Title</a> <li><a href="@link.Href">@link.Title</a></li>
} }
</ul>
</nav> </nav>
}
</div> </div>
<p class="footer-copyright">© @_year Elternbeirat der IGMH</p>
</footer> </footer>
@@ -23,6 +23,9 @@ public partial class MainLayout
private IReadOnlyList<NavLinkItem> _header = []; private IReadOnlyList<NavLinkItem> _header = [];
private IReadOnlyList<NavLinkItem> _footer = []; private IReadOnlyList<NavLinkItem> _footer = [];
// Berlin's year, not the server's (UTC) -- see FooterYear.
private readonly int _year = FooterYear.For(TimeProvider.System.GetUtcNow());
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
@@ -25,11 +25,12 @@
z-index keeps it above the content; the opaque background stops content showing z-index keeps it above the content; the opaque background stops content showing
through as it scrolls underneath. */ through as it scrolls underneath. */
.site-header { .site-header {
/* Brand-blue bar with a warm accent underline -- the strongest, most visible /* The brand gradient, blue into green -- the strongest, most visible cue that
cue that carries the friendly tone across every page. Uses its own header carries the school's colours across every page. Built on --color-header, so
token so it stays deep blue in dark mode too (see app.css). */ it stays deep in dark mode too (see app.css). No accent underline any more:
background: var(--color-header); the gradient already ends in green, and the active menu item carries the
border-bottom: 3px solid var(--color-accent); green stroke. */
background: var(--gradient-brand);
box-shadow: var(--shadow); box-shadow: var(--shadow);
position: sticky; position: sticky;
top: 0; top: 0;
@@ -42,22 +43,57 @@
padding: var(--space-3) 1.25rem; padding: var(--space-3) 1.25rem;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; align-items: center;
gap: var(--space-2) var(--space-4); gap: var(--space-2) var(--space-4);
} }
/* Signet and two-line lettering side by side. The lettering is a column of its
own, so the name and the school line up on the left edge. */
.brand { .brand {
font-size: 1.35rem; display: flex;
font-weight: 700; align-items: center;
color: #fff; gap: var(--space-2);
color: var(--color-on-header);
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
} }
.brand:hover {
color: var(--color-on-header);
}
/* The signet lives in its own component (Signet.razor), so its <svg> carries that
component's scope attribute, not this one's; ::deep reaches it from here. */
.brand ::deep .signet {
flex-shrink: 0;
width: 2.5rem;
height: 2.5rem;
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.brand-name {
font-size: 1.35rem;
font-weight: var(--font-weight-heading);
letter-spacing: var(--letter-spacing-heading);
}
.brand-school {
font-size: 0.85rem;
font-weight: 600;
}
/* The links are pills with their own padding (see app.css), so the gap between
them can stay small. */
.main-nav { .main-nav {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.25rem 1.25rem; align-items: center;
gap: var(--space-1) var(--space-2);
} }
/* The hamburger button and its state checkbox are hidden on wide screens; the /* The hamburger button and its state checkbox are hidden on wide screens; the
@@ -72,11 +108,12 @@
(b-xxxxx), so a scoped rule ".main-nav a" would not match them. See app.css. */ (b-xxxxx), so a scoped rule ".main-nav a" would not match them. See app.css. */
/* Content: at least tall enough that the footer sits at the bottom of the viewport /* Content: at least tall enough that the footer sits at the bottom of the viewport
on short pages. 100dvh - 4rem leaves room for the header (which sits above in on short pages. 100dvh - 5rem leaves room for the header with its signet (which
normal flow); dvh follows the mobile browser's shrinking toolbar. On long pages sits above in normal flow); dvh follows the mobile browser's shrinking toolbar.
the content simply grows past this and pushes the footer down naturally. */ On long pages the content simply grows past this and pushes the footer down
naturally. */
.content { .content {
min-height: calc(100dvh - 4rem); min-height: calc(100dvh - 5rem);
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
max-width: var(--content-width); max-width: var(--content-width);
@@ -84,39 +121,101 @@
padding: var(--space-4) 1.25rem var(--space-6); padding: var(--space-4) 1.25rem var(--space-6);
} }
/* Footer */ /* Footer: a dark brand-blue band with light text, so the page visibly ends here.
Its own tokens (--color-footer, --color-on-footer...) keep it dark in both
colour schemes, like the header. position:relative anchors the wave. */
.site-footer { .site-footer {
border-top: 1px solid var(--color-border); position: relative;
background: var(--color-surface); margin-top: var(--space-6);
color: var(--color-text-soft); background: var(--color-footer);
font-size: 0.9rem; color: var(--color-on-footer-soft);
font-size: 0.95rem;
} }
/* The wave sits just above the footer's top edge (bottom: 100%), drawn over the
page background in the footer colour, so the band seems to rise softly instead
of starting with a hard line. Fixed height, full width: on a phone it simply
flattens out. Overlapped by 1px so no hairline gap shows between wave and band
when the browser rounds. */
.footer-wave {
position: absolute;
bottom: calc(100% - 1px);
left: 0;
display: block;
width: 100%;
height: 2.5rem;
fill: var(--color-footer);
}
/* Three columns: brand, quick links, further pages. The brand gets a bit more room
because it holds the signet. A missing link column simply leaves its cell empty,
so the others keep their place. */
.footer-inner { .footer-inner {
max-width: var(--content-width); max-width: var(--content-width);
margin: 0 auto; margin: 0 auto;
padding: var(--space-3) 1.25rem; padding: var(--space-5) 1.25rem var(--space-4);
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: 1.5fr 1fr 1fr;
justify-content: space-between;
gap: var(--space-2) var(--space-4);
}
.footer-nav {
display: flex;
gap: var(--space-4); gap: var(--space-4);
} }
.footer-brand {
display: flex;
align-items: center;
align-self: start;
gap: var(--space-2);
color: var(--color-on-footer);
font-weight: 700;
}
.footer-brand ::deep .signet {
flex-shrink: 0;
width: 3rem;
height: 3rem;
}
/* Column headings label the lists; they are not a second page heading, so they
stay small and quiet. */
.footer-heading {
margin: 0 0 var(--space-2);
color: var(--color-on-footer);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.footer-nav ul {
list-style: none;
margin: 0;
padding: 0;
}
.footer-nav li + li {
margin-top: var(--space-1);
}
.footer-nav a { .footer-nav a {
color: var(--color-text-soft); color: var(--color-on-footer-soft);
text-decoration: none; text-decoration: none;
} }
.footer-nav a:hover { .footer-nav a:hover {
color: var(--color-brand); color: var(--color-on-footer);
text-decoration: underline; text-decoration: underline;
} }
/* Copyright: a thin rule separates it from the columns. The rule spans the content
width, not the whole window, so it lines up with the columns above. */
.footer-copyright {
box-sizing: border-box;
max-width: var(--content-width);
margin: 0 auto;
padding: var(--space-3) 1.25rem;
border-top: 1px solid var(--color-footer-border);
font-size: 0.85rem;
}
/* Mobile: on narrow screens the nav collapses behind a hamburger button. This is /* Mobile: on narrow screens the nav collapses behind a hamburger button. This is
pure CSS -- the #nav-toggle checkbox holds the open/closed state and its pure CSS -- the #nav-toggle checkbox holds the open/closed state and its
:checked state reveals the nav. No JavaScript, so it works under static SSR. */ :checked state reveals the nav. No JavaScript, so it works under static SSR. */
@@ -144,8 +243,9 @@
display: block; display: block;
width: 1.5rem; width: 1.5rem;
height: 2px; height: 2px;
background: #fff; /* on the blue header bar */ background: var(--color-on-header);
content: ""; content: "";
transition: transform var(--transition), background-color var(--transition);
} }
.nav-toggle-bar::before { .nav-toggle-bar::before {
@@ -156,6 +256,21 @@
transform: translateY(4px); /* 6px minus the bar's own 2px height */ transform: translateY(4px); /* 6px minus the bar's own 2px height */
} }
/* Open: the hamburger turns into an X. The middle bar fades out and the outer
two meet in the middle and cross. Only the icon changes shape; nothing around
it moves. The checkbox sits right before the label, hence the + combinator. */
.nav-toggle:checked + .nav-toggle-label .nav-toggle-bar {
background-color: transparent;
}
.nav-toggle:checked + .nav-toggle-label .nav-toggle-bar::before {
transform: rotate(45deg);
}
.nav-toggle:checked + .nav-toggle-label .nav-toggle-bar::after {
transform: translateY(-2px) rotate(-45deg); /* back up by its own height */
}
/* The nav takes its own row and is hidden until the checkbox is checked. */ /* The nav takes its own row and is hidden until the checkbox is checked. */
.main-nav { .main-nav {
flex-basis: 100%; flex-basis: 100%;
@@ -175,6 +290,11 @@
margin-top: 0.5rem; margin-top: 0.5rem;
} }
/* Mobile per-link spacing/separators for .main-nav a are in the global app.css /* Mobile per-link spacing for .main-nav a is in the global app.css too, for
too, for the same scoped-attribute reason as above. */ the same scoped-attribute reason as above. */
/* The footer columns stack: one column, brand on top. */
.footer-inner {
grid-template-columns: 1fr;
}
} }
@@ -0,0 +1,35 @@
namespace Elternbeirat.Web.Features.Layout;
/// <summary>
/// Works out the year shown in the footer's copyright line.
/// </summary>
/// <remarks>
/// The container runs on UTC, but visitors live in Germany. Taking the year
/// straight from the server clock would show the old year for the first hour
/// (two in summer time) of every new year, so the instant is converted to
/// Europe/Berlin first.
/// </remarks>
public static class FooterYear
{
/// <summary>
/// IANA id; resolves on every platform .NET supports, Windows included.
/// </summary>
private static readonly TimeZoneInfo Berlin =
TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
/// <summary>
/// Returns the calendar year in Berlin at the given instant.
/// </summary>
/// <param name="now">
/// The current instant, in any offset; usually
/// <see cref="TimeProvider.GetUtcNow"/>.
/// </param>
/// <returns>The year a visitor in Germany would see on their calendar.</returns>
/// <example>
/// <code>
/// FooterYear.For(new DateTimeOffset(2026, 12, 31, 23, 30, 0, TimeSpan.Zero)); // 2027
/// </code>
/// </example>
public static int For(DateTimeOffset now) =>
TimeZoneInfo.ConvertTime(now, Berlin).Year;
}
@@ -0,0 +1,17 @@
@*
The site's own small mark: a speech bubble (the board speaks for the parents)
holding a grown-up and a child. Inline SVG, so there is no extra request and
nothing from a third-party host. Decorative only -- the name next to it carries
the meaning, so it is hidden from screen readers. Colours come from tokens in
Signet.razor.css; the size is set by whoever places it (MainLayout.razor.css).
*@
<svg class="signet" viewBox="0 0 40 40" width="40" height="40" aria-hidden="true" focusable="false">
<path class="signet-bubble"
d="M8 4h24a6 6 0 0 1 6 6v15a6 6 0 0 1-6 6H18l-8 6v-6H8a6 6 0 0 1-6-6V10a6 6 0 0 1 6-6z" />
<g class="signet-figures">
<circle cx="15" cy="12" r="3.5" />
<path d="M9 26c0-5 2.7-8 6-8s6 3 6 8z" />
<circle cx="26" cy="16" r="2.6" />
<path d="M21.5 26c0-3.8 2-6 4.5-6s4.5 2.2 4.5 6z" />
</g>
</svg>
@@ -0,0 +1,11 @@
/* A white bubble with green figures. It sits on the header gradient and on the dark
footer, both of which always carry white text, so --color-on-header is right in
both places. The figures use the darker green so they stay distinct on the white
bubble (the lighter --color-accent would wash out). */
.signet-bubble {
fill: var(--color-on-header);
}
.signet-figures {
fill: var(--color-accent-dark);
}
+64 -18
View File
@@ -17,6 +17,23 @@
--color-brand is lightened for text/links. */ --color-brand is lightened for text/links. */
--color-header: #328fc6; --color-header: #328fc6;
/* Text and menu states on the header. White text, and two translucent white
washes for the menu pills: they lighten whatever part of the gradient sits
behind a link, so one value works from the blue end to the green end. The
active pill is the stronger one, so "where am I" outranks "where is the
mouse". */
--color-on-header: #fff;
--color-nav-hover: rgba(255, 255, 255, 0.12);
--color-nav-active: rgba(255, 255, 255, 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. /* 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 The -dark shade is darkened enough to keep small text and the FAQ marker
legible on a white card. */ legible on a white card. */
@@ -121,6 +138,12 @@
--color-header: #266f9c; /* the IGMH blue eased down, to sit calmly on dark */ --color-header: #266f9c; /* the IGMH blue eased down, to sit calmly on dark */
--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: #86c97d; /* the IGMH green nudged brighter for dark */
--color-accent-dark: #86c97d; /* no darkening needed on a dark surface */ --color-accent-dark: #86c97d; /* no darkening needed on a dark surface */
--color-accent-tint: #24332a; /* a dark green-grey: tinted, but the soft --color-accent-tint: #24332a; /* a dark green-grey: tinted, but the soft
@@ -253,37 +276,55 @@ h1:focus {
/* Header navigation links. These live here (global), not in MainLayout.razor.css: /* 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 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 ".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. */ 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 { .main-nav a {
color: rgba(255, 255, 255, 0.85); position: relative;
padding: var(--space-1) var(--space-3);
border-radius: 999px;
color: var(--color-on-header);
text-decoration: none; text-decoration: none;
padding: var(--space-1) 0; transition: background-color var(--transition);
border-bottom: 2px solid transparent;
transition: color var(--transition), border-color var(--transition);
} }
.main-nav a:hover { .main-nav a:hover {
color: #fff; color: var(--color-on-header);
background: var(--color-nav-hover);
} }
/* NavLink adds .active to the current route's link; a warm accent underline marks /* NavLink adds .active to the current route's link: a stronger pill plus a short
where the visitor is on the blue bar. */ 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 { .main-nav a.active {
color: #fff; background: var(--color-nav-active);
border-bottom-color: var(--color-accent);
} }
/* Mobile: stacked links inside the collapsed menu get row separators. Same global .main-nav a.active::after {
placement, same NavLink reason as above. Scoped to the narrow layout via the content: "";
media query so it does not affect the desktop row. */ 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) { @media (max-width: 40rem) {
.main-nav a { .main-nav a {
padding: 0.6rem 0; padding: 0.6rem var(--space-3);
border-bottom: 1px solid rgba(255, 255, 255, 0.15); border-radius: var(--radius-sm);
} }
.main-nav a.active { .main-nav a.active::after {
border-bottom-color: rgba(255, 255, 255, 0.15); right: auto;
width: 1.5rem;
} }
} }
@@ -600,12 +641,15 @@ h1:focus {
this is what turns a plain list of documents on /downloads into a download 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 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 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>. */ 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 > a[href$=".pdf" i],
.markdown-body .lone-link > p > a[href$=".pdf" i] { .markdown-body .lone-link > p > a[href$=".pdf" i] {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
box-sizing: border-box;
max-width: 42rem; max-width: 42rem;
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
@@ -692,6 +736,7 @@ h1:focus {
/* ::: aufruf -- a highlighted call to action: a tinted box with a brand-blue edge. /* ::: aufruf -- a highlighted call to action: a tinted box with a brand-blue edge.
Its links become buttons (rule above). */ Its links become buttons (rule above). */
.markdown-body .aufruf { .markdown-body .aufruf {
box-sizing: border-box; /* padding inside the 42rem, flush with the text */
margin: 0 0 var(--space-4); margin: 0 0 var(--space-4);
padding: var(--space-4); padding: var(--space-4);
max-width: 42rem; max-width: 42rem;
@@ -705,6 +750,7 @@ h1:focus {
says what matters. */ says what matters. */
.markdown-body .hinweis { .markdown-body .hinweis {
position: relative; position: relative;
box-sizing: border-box; /* padding inside the 42rem, flush with the text */
margin: 0 0 var(--space-4); margin: 0 0 var(--space-4);
padding: var(--space-3) var(--space-4) var(--space-3) 3.25rem; padding: var(--space-3) var(--space-4) var(--space-3) 3.25rem;
max-width: 42rem; max-width: 42rem;