diff --git a/Elternbeirat.Web.Tests/FooterYearTests.cs b/Elternbeirat.Web.Tests/FooterYearTests.cs
new file mode 100644
index 0000000..803852b
--- /dev/null
+++ b/Elternbeirat.Web.Tests/FooterYearTests.cs
@@ -0,0 +1,27 @@
+using Elternbeirat.Web.Features.Layout;
+
+namespace Elternbeirat.Web.Tests;
+
+///
+/// Unit tests for the footer's copyright year. Pure logic, no PocketBase needed.
+///
+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);
+ }
+}
diff --git a/Elternbeirat.Web.Tests/NavBuilderTests.cs b/Elternbeirat.Web.Tests/NavBuilderTests.cs
index f12c2e1..b433b9a 100644
--- a/Elternbeirat.Web.Tests/NavBuilderTests.cs
+++ b/Elternbeirat.Web.Tests/NavBuilderTests.cs
@@ -43,6 +43,15 @@ public class NavBuilderTests
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]
public void Home_page_maps_to_root_every_other_slug_to_its_path()
{
diff --git a/Elternbeirat.Web.Tests/RouteSmokeTests.cs b/Elternbeirat.Web.Tests/RouteSmokeTests.cs
index 32079dc..ca9ba7c 100644
--- a/Elternbeirat.Web.Tests/RouteSmokeTests.cs
+++ b/Elternbeirat.Web.Tests/RouteSmokeTests.cs
@@ -115,6 +115,30 @@ public sealed partial class RouteSmokeTests : IDisposable
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