-/// Shared by the full event list and the home-page events embed, so both render
-/// dates the same way.
+/// Shared by the full event list, the home-page events embed and the next-event
+/// card, so all render dates the same way: as the full date,
+/// the Sheet… methods and for the calendar sheet and the
+/// time line beside it, and for the list of past events.
///
/// This is the event counterpart to
/// , which formats a plain
@@ -27,6 +29,11 @@ public static class EventDateFormat
///
private const string ShortDate = "d. MMMM yyyy";
+ ///
+ /// Date format for the list of past events, which is grouped by year.
+ ///
+ private const string DayMonth = "d. MMMM";
+
///
/// Formats the date range of an event.
///
@@ -54,13 +61,136 @@ public static class EventDateFormat
(var start, null, var hasTime)
=> Stamp(start, LongDate, hasTime),
(var start, { } end, true) when end.Date == start.Date
- => $"{start.ToString(LongDate, Cultures.German)}, {start.ToString("HH:mm", Cultures.German)}–{end.ToString("HH:mm", Cultures.German)} Uhr",
+ => $"{start.ToString(LongDate, Cultures.German)}, {Clock(start)}–{Clock(end)} Uhr",
(var start, { } end, false) when end.Date == start.Date
=> Stamp(start, LongDate, withTime: false),
(var start, { } end, var hasTime)
=> $"{Stamp(start, LongDate, hasTime)} – {Stamp(end, ShortDate, hasTime)}",
};
+ ///
+ /// The short month for the top band of a calendar sheet.
+ ///
+ /// The event's start.
+ /// The German short month without its period, e.g. Okt or März.
+ ///
+ /// Returned in normal case; the sheet's CSS sets it in capitals. The period of the
+ /// abbreviation (Okt.) is dropped because on a sheet it reads as a stray dot.
+ ///
+ ///
+ public static string SheetMonth(DateTime value)
+ => value.ToString("MMM", Cultures.German).TrimEnd('.');
+
+ ///
+ /// The day of the month for the large number on a calendar sheet.
+ ///
+ /// The event's start.
+ /// The day without a leading zero, e.g. 8.
+ public static string SheetDay(DateTime value)
+ => value.Day.ToString(Cultures.German);
+
+ ///
+ /// The short weekday for the bottom of a calendar sheet.
+ ///
+ /// The event's start.
+ /// The German short weekday without its period, e.g. Do.
+ public static string SheetWeekday(DateTime value)
+ => value.ToString("ddd", Cultures.German).TrimEnd('.');
+
+ ///
+ /// The time line beside a calendar sheet: what the sheet itself cannot show.
+ ///
+ /// The event to format.
+ ///
+ /// Depending on and :
+ ///
+ /// - No end: 19:30 Uhr
+ /// - Same day: 19:30–21:00 Uhr
+ /// - Several days: 09:00 Uhr bis Sonntag, 11. Oktober 2026, 13:00 Uhr
+ /// - Several days, no time: bis Sonntag, 11. Oktober 2026
+ /// - One day, no time: an empty string; the sheet says it all.
+ ///
+ ///
+ ///
+ /// The sheet shows the start day only, so a multi-day event must name its last
+ /// day here, with the weekday, as the sheet does for the first one.
+ ///
+ public static string Span(Event @event)
+ => (@event.Start, @event.End, @event.HasTime) switch
+ {
+ (var start, { } end, var hasTime) when end.Date != start.Date
+ => (hasTime ? Clock(start) + " Uhr " : "") + "bis " + Stamp(end, LongDate, hasTime),
+ (var start, null, true)
+ => $"{Clock(start)} Uhr",
+ (var start, { } end, true)
+ => $"{Clock(start)}–{Clock(end)} Uhr",
+ _ => "",
+ };
+
+ ///
+ /// The day column of a past event in the folded-up list on the events page.
+ ///
+ /// The event to format.
+ ///
+ /// Day and full month, depending on :
+ ///
+ /// - One day: 13. November
+ /// - Several days in one month: 8.–9. Februar
+ /// - Across a month: 30. Januar – 2. Februar
+ /// - Across a year: 30. Dezember – 2. Januar 2027
+ ///
+ ///
+ ///
+ /// The list is grouped by the start year under its own small heading, so the
+ /// year is only named where the end leaves that year. The weekday is left out
+ /// to keep the column short; the time follows the title ().
+ ///
+ public static string PastDays(Event @event)
+ => (@event.Start, @event.End) switch
+ {
+ (var start, { } end) when end.Year != start.Year
+ => $"{start.ToString(DayMonth, Cultures.German)} – {end.ToString(ShortDate, Cultures.German)}",
+ (var start, { } end) when end.Month != start.Month
+ => $"{start.ToString(DayMonth, Cultures.German)} – {end.ToString(DayMonth, Cultures.German)}",
+ (var start, { } end) when end.Day != start.Day
+ => $"{start.Day.ToString(Cultures.German)}.–{end.ToString(DayMonth, Cultures.German)}",
+ (var start, _)
+ => start.ToString(DayMonth, Cultures.German),
+ };
+
+ ///
+ /// The time after the title of a past event in the folded-up list.
+ ///
+ /// The event to format.
+ ///
+ /// Depending on and :
+ ///
+ /// - No time: an empty string; nothing is shown.
+ /// - No end: 19:00 Uhr
+ /// - Same day: 19:00–21:00 Uhr
+ /// - Several days: the start time only, 09:00 Uhr
+ ///
+ ///
+ ///
+ /// For several days the day column already shows the span; a time range beside
+ /// it would read as the daily hours, so only the start time is given.
+ ///
+ public static string PastTime(Event @event)
+ => (@event.HasTime, @event.End) switch
+ {
+ (false, _) => "",
+ (true, { } end) when end.Date != @event.Start.Date => $"{Clock(@event.Start)} Uhr",
+ _ => Span(@event),
+ };
+
+ ///
+ /// Formats the clock time of a point in time.
+ ///
+ /// The date and time.
+ /// The time as e.g. 19:30.
+ private static string Clock(DateTime value)
+ => value.ToString("HH:mm", Cultures.German);
+
///
/// Formats a single point in time as a date with an optional time.
///
diff --git a/Elternbeirat.Web/Features/Events/EventList.razor b/Elternbeirat.Web/Features/Events/EventList.razor
index f165d5f..670ab73 100644
--- a/Elternbeirat.Web/Features/Events/EventList.razor
+++ b/Elternbeirat.Web/Features/Events/EventList.razor
@@ -21,10 +21,9 @@
}
else
{
-
- Alle Termine des Elternbeirats auf einen Blick. Sie können den Kalender auch
- abonnieren und in Ihrer Kalender-App automatisch
- aktuell halten.
+ @* The page body above is the intro; the page only adds the subscribe button. *@
+
+ Kalender abonnieren
Kommende Termine
@@ -38,41 +37,38 @@ else
}
+ @* Past events matter rarely, so they wait folded up and compact: grouped by year,
+ then day, title and (if the event has one) the time per row. *@
@if (_pastEvents.Count > 0)
{
- Vergangene Termine
-
+
}
}
diff --git a/Elternbeirat.Web/Features/Events/EventList.razor.cs b/Elternbeirat.Web/Features/Events/EventList.razor.cs
index 1967cbd..947b059 100644
--- a/Elternbeirat.Web/Features/Events/EventList.razor.cs
+++ b/Elternbeirat.Web/Features/Events/EventList.razor.cs
@@ -39,6 +39,10 @@ public partial class EventList
// 503 this sets on the response.
private bool _unavailable;
+ // Past events grouped by year for the small year headings. _pastEvents is most
+ // recent first, and GroupBy keeps that order, so the newest year comes first.
+ private IEnumerable> PastYears => _pastEvents.GroupBy(@event => @event.Start.Year);
+
// The page body rendered from Markdown to HTML, ready to emit as a MarkupString.
private MarkupString Body => (MarkupString)Markdown.ToHtml(_page?.Body);
diff --git a/Elternbeirat.Web/Features/Events/EventList.razor.css b/Elternbeirat.Web/Features/Events/EventList.razor.css
index f95fa43..2f4f073 100644
--- a/Elternbeirat.Web/Features/Events/EventList.razor.css
+++ b/Elternbeirat.Web/Features/Events/EventList.razor.css
@@ -1,2 +1,70 @@
-/* The .event-list card styles (including past-event styling) are shared with the
- home-page embed and live in the global app.css. Nothing page-specific here. */
+/* The card grid (.event-list) is shared with the home-page embed and lives in the
+ global app.css; the card itself is EventCard. Only the page's own parts are here. */
+
+.events-subscribe {
+ margin: var(--space-4) 0;
+}
+
+/* Past events: one folded-up card. Opened, a compact list grouped by year,
+ deliberately quieter than the upcoming cards above. The open/close triangle is
+ the shared .disclosure rule in app.css, the same as on the FAQ topics. */
+.past-events {
+ margin-top: var(--space-5);
+ padding: var(--space-3) var(--space-4);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius);
+ background: var(--color-surface);
+}
+
+.past-events-year {
+ margin: var(--space-4) 0 var(--space-1);
+ color: var(--color-text-soft);
+ font-size: 0.9rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+}
+
+.past-events ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+/* On a phone the day sits above the title: a column beside it would leave the
+ title too little room. */
+.past-events li {
+ display: grid;
+ padding: var(--space-2) 0;
+ border-top: 1px solid var(--color-border);
+}
+
+/* From tablet width on, day and title side by side. The day column has a fixed
+ width, so every title starts on the same line; it fits the longest single day,
+ "30. September". A span across months ("30. Januar – 2. Februar") wraps after
+ the dash inside the column. Same font size for both and baseline alignment, so
+ the two texts sit on one line. */
+@media (min-width: 40rem) {
+ .past-events li {
+ grid-template-columns: 8rem 1fr;
+ column-gap: var(--space-3);
+ align-items: baseline;
+ }
+}
+
+/* Tabular digits give every day the same width, so "1." and "10." line up. */
+.past-event-date {
+ color: var(--color-text-soft);
+ font-variant-numeric: tabular-nums;
+}
+
+.past-event-title {
+ color: var(--color-text);
+}
+
+/* The time trails the title, quieter, as an addition rather than part of the name. */
+.past-event-time {
+ margin-left: var(--space-2);
+ color: var(--color-text-soft);
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
diff --git a/Elternbeirat.Web/Features/Faqs/FaqList.razor b/Elternbeirat.Web/Features/Faqs/FaqList.razor
index 88eb523..0b5af54 100644
--- a/Elternbeirat.Web/Features/Faqs/FaqList.razor
+++ b/Elternbeirat.Web/Features/Faqs/FaqList.razor
@@ -27,7 +27,7 @@ else
{
@foreach (var topic in _topics)
{
-
+
@topic.Title
@if (!string.IsNullOrWhiteSpace(topic.Intro))
{
diff --git a/Elternbeirat.Web/Features/Home/Home.razor b/Elternbeirat.Web/Features/Home/Home.razor
index c850c93..1f70fbf 100644
--- a/Elternbeirat.Web/Features/Home/Home.razor
+++ b/Elternbeirat.Web/Features/Home/Home.razor
@@ -29,21 +29,24 @@
@if (_nextEvent is not null)
{
- @* The calendar sheet repeats the date as decoration, so it is hidden from
- screen readers; carries the full date for everyone. *@
+ @* As in EventCard: the calendar sheet shows the date and is hidden from
+ screen readers, who get the full date from instead. Visible
+ beside the sheet is only what it cannot show: the time, the end. *@
+ var span = EventDateFormat.Span(_nextEvent);
Nächster Termin
-
- @NextEventMonth
- @NextEventDay
-
+
@_nextEvent.Title
-
+
+ @if (span.Length > 0)
+ {
+ @span
+ }
@if (!string.IsNullOrWhiteSpace(_nextEvent.Location))
{
- @_nextEvent.Location
+ @_nextEvent.Location
}
diff --git a/Elternbeirat.Web/Features/Home/Home.razor.cs b/Elternbeirat.Web/Features/Home/Home.razor.cs
index 9be56da..38100c5 100644
--- a/Elternbeirat.Web/Features/Home/Home.razor.cs
+++ b/Elternbeirat.Web/Features/Home/Home.razor.cs
@@ -46,14 +46,6 @@ public partial class Home
private IReadOnlyList _embed = [];
private Event? _nextEvent;
- // Month and day for the calendar sheet. The sheet is decoration (the full date is
- // in next to it), so the short month drops its trailing period.
- private string NextEventMonth =>
- _nextEvent?.Start.ToString("MMM", Cultures.German).TrimEnd('.') ?? "";
-
- private string NextEventDay =>
- _nextEvent?.Start.Day.ToString(Cultures.German) ?? "";
-
protected override Task OnInitializedAsync()
{
var token = HttpContext?.RequestAborted ?? CancellationToken.None;
diff --git a/Elternbeirat.Web/Features/Home/Home.razor.css b/Elternbeirat.Web/Features/Home/Home.razor.css
index 336367d..143f199 100644
--- a/Elternbeirat.Web/Features/Home/Home.razor.css
+++ b/Elternbeirat.Web/Features/Home/Home.razor.css
@@ -136,38 +136,6 @@
align-items: flex-start;
}
-/* The calendar sheet: a blue band with the month over a large day number. The band
- uses the button blue, which keeps white text at >= 5.5:1 in both schemes (the
- green accent would fall below AA for text this small). */
-.calendar-sheet {
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- width: 4rem;
- overflow: hidden;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-sm);
- background: var(--color-bg);
- text-align: center;
-}
-
-.calendar-month {
- padding: 0.1rem 0;
- background: var(--color-button);
- color: var(--color-on-button);
- font-size: 0.8rem;
- font-weight: 700;
- text-transform: uppercase;
-}
-
-.calendar-day {
- padding: var(--space-1) 0;
- color: var(--color-heading);
- font-size: 1.75rem;
- font-weight: var(--font-weight-heading);
- line-height: 1.2;
-}
-
.next-event-details {
min-width: 0; /* lets a long title wrap instead of widening the card */
}
@@ -180,7 +148,9 @@
line-height: 1.3;
}
-.next-event-location {
+/* Time and place, one line each with their icon, green as on the event cards. The
+ icon belongs to Icon's own scope, so it is reached through ::deep. */
+.next-event-meta {
display: flex;
align-items: center;
gap: var(--space-1);
@@ -189,6 +159,10 @@
font-size: 0.9rem;
}
+.next-event-meta ::deep .icon {
+ color: var(--color-accent-dark);
+}
+
/* --- Tiles --------------------------------------------------------------------
One card per header page. Fixed columns instead of auto-fill: two on a phone,
three from tablet width, so the six pages always fill whole rows and never leave
diff --git a/Elternbeirat.Web/Features/Posts/PostCard.razor b/Elternbeirat.Web/Features/Posts/PostCard.razor
new file mode 100644
index 0000000..61bdcff
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostCard.razor
@@ -0,0 +1,13 @@
+@*
+ One post as a card that links to the post: date, title, a teaser from the body
+ and "Weiterlesen". The arrow is a hint where the card leads; it does not move.
+*@
+
+
+ @Value.Title
+ @if (Teaser.Length > 0)
+ {
+ @Teaser
+ }
+ Weiterlesen
+
diff --git a/Elternbeirat.Web/Features/Posts/PostCard.razor.cs b/Elternbeirat.Web/Features/Posts/PostCard.razor.cs
new file mode 100644
index 0000000..6564e31
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostCard.razor.cs
@@ -0,0 +1,46 @@
+using Elternbeirat.Contracts;
+using Elternbeirat.Web.Shared;
+using Microsoft.AspNetCore.Components;
+
+namespace Elternbeirat.Web.Features.Posts;
+
+///
+/// Renders one post as a card linking to the post, with date, title and teaser.
+///
+///
+/// Shared by the posts page and the home-page posts embed, so both show posts the
+/// same way. The teaser comes from the body (): posts
+/// have no summary field of their own.
+///
+///
+public partial class PostCard
+{
+ ///
+ /// The post to show.
+ ///
+ [Parameter]
+ [EditorRequired]
+ public Post Value { get; set; } = null!;
+
+ ///
+ /// for the large card that opens the posts page (the
+ /// newest post); for a regular card.
+ ///
+ [Parameter]
+ public bool Featured { get; set; }
+
+ ///
+ /// The card's classes; the featured card adds its modifier.
+ ///
+ private string CssClass => Featured ? "post-card post-card-featured" : "post-card";
+
+ ///
+ /// The address of the post.
+ ///
+ private string Href => $"/posts/{Value.Slug}";
+
+ ///
+ /// The teaser text; empty if the body has no paragraph text.
+ ///
+ private string Teaser => Markdown.Teaser(Value.Body);
+}
diff --git a/Elternbeirat.Web/Features/Posts/PostCard.razor.css b/Elternbeirat.Web/Features/Posts/PostCard.razor.css
new file mode 100644
index 0000000..7ef89d0
--- /dev/null
+++ b/Elternbeirat.Web/Features/Posts/PostCard.razor.css
@@ -0,0 +1,93 @@
+/* The post card: a white surface with a green stripe on the left as the eye's
+ anchor. The stripe is a border, so it follows the rounded corners. The whole card
+ is the link and fills its list cell, so the cards of a row are equally tall.
+ Hover = stronger shadow, nothing moves. */
+.post-card {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ height: 100%;
+ padding: var(--space-3) var(--space-4);
+ border-left: 4px solid var(--color-accent);
+ border-radius: var(--radius);
+ background: var(--color-bg);
+ box-shadow: var(--shadow);
+ color: var(--color-text);
+ text-decoration: none;
+ transition: box-shadow var(--transition);
+}
+
+.post-card:hover {
+ box-shadow: var(--shadow-hover);
+ color: var(--color-text);
+}
+
+.post-card:focus-visible {
+ outline: 3px solid var(--color-focus);
+ outline-offset: 2px;
+}
+
+/* Icon and date on one line. The icon belongs to Icon's own scope, so it is
+ reached through ::deep. */
+.post-date {
+ display: flex;
+ align-items: center;
+ gap: var(--space-1);
+}
+
+.post-date ::deep .icon {
+ color: var(--color-accent-dark);
+}
+
+.post-title {
+ color: var(--color-brand);
+ font-size: 1.15rem;
+ font-weight: 700;
+ line-height: 1.3;
+}
+
+/* Cut after three lines, so cards of a row stay close in height whatever the
+ editor wrote. The featured card lifts the limit (see below). */
+.post-teaser {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ line-clamp: 3;
+ overflow: hidden;
+ font-size: 0.95rem;
+}
+
+/* margin-top: auto pushes the link line to the bottom, so it sits on one level
+ across a row of cards with teasers of different length. */
+.post-more {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
+ margin-top: auto;
+ padding-top: var(--space-1);
+ color: var(--color-brand);
+ font-size: 0.9rem;
+ font-weight: 700;
+}
+
+/* The newest post on the posts page: more room, a larger title and the full
+ teaser. */
+.post-card-featured {
+ padding: var(--space-4) var(--space-5);
+}
+
+.post-card-featured .post-title {
+ font-size: var(--font-size-h3);
+}
+
+@media (min-width: 48rem) {
+ .post-card-featured .post-title {
+ font-size: 1.5rem;
+ }
+}
+
+.post-card-featured .post-teaser {
+ display: block;
+ font-size: 1rem;
+}
diff --git a/Elternbeirat.Web/Features/Posts/PostList.razor b/Elternbeirat.Web/Features/Posts/PostList.razor
index e7d15ed..1e49431 100644
--- a/Elternbeirat.Web/Features/Posts/PostList.razor
+++ b/Elternbeirat.Web/Features/Posts/PostList.razor
@@ -25,15 +25,12 @@ else if (_posts.Count == 0)
}
else
{
-
- @foreach (var post in _posts)
+ @* The newest post (GetPostsAsync sorts newest first) opens the page as a large
+ card across the full width; the rest follow in the grid. *@
+
+ @for (var i = 0; i < _posts.Count; i++)
{
- -
-
- @post.Title
-
-
-
+
}
}
diff --git a/Elternbeirat.Web/Features/Posts/PostList.razor.css b/Elternbeirat.Web/Features/Posts/PostList.razor.css
index 88fbef8..b7e0b0a 100644
--- a/Elternbeirat.Web/Features/Posts/PostList.razor.css
+++ b/Elternbeirat.Web/Features/Posts/PostList.razor.css
@@ -1,2 +1,12 @@
-/* The .post-list card styles are shared with the home-page embed and live in the
- global app.css. This page needs no extra styling of its own. */
+/* The card is PostCard and the base list grid lives in the global app.css, shared
+ with the home-page embed. Only the page's two-column layout is here: one column
+ on a phone, two from tablet width, with the newest post across both. */
+@media (min-width: 48rem) {
+ .post-list-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .post-list-featured {
+ grid-column: 1 / -1;
+ }
+}
diff --git a/Elternbeirat.Web/Program.cs b/Elternbeirat.Web/Program.cs
index 9a3e8c5..b25af41 100644
--- a/Elternbeirat.Web/Program.cs
+++ b/Elternbeirat.Web/Program.cs
@@ -69,6 +69,28 @@ app.MapGet("/events.ics", async (PocketBaseClient pocketBase, CancellationToken
return Results.Text(IcsCalendar.Build(events), "text/calendar; charset=utf-8");
});
+// One event as a calendar file ("In Kalender übernehmen" on the events page): the
+// same calendar as the feed, holding just this event. Unlike the feed it does not
+// fall back to an empty calendar when PocketBase is down -- a file without the event
+// the visitor clicked would be a silent failure, so it answers 503. An unknown or
+// non-public id is a 404.
+app.MapGet("/events/{id}.ics", async (string id, PocketBaseClient pocketBase, CancellationToken token) =>
+{
+ IReadOnlyList events;
+ try
+ {
+ events = await pocketBase.GetEventsAsync(token);
+ }
+ catch (PocketBaseUnavailableException)
+ {
+ return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
+ }
+
+ return events.FirstOrDefault(@event => @event.Id == id) is { } match
+ ? Results.Text(IcsCalendar.Build([match]), "text/calendar; charset=utf-8")
+ : Results.NotFound();
+});
+
// Status page for an outside monitor (Uptime Kuma) to check. One check tells all
// three states apart: no reply = the app is down, 503 = the app runs but PocketBase
// is down, 200 = both are fine. It talks to PocketBase directly, not through the
diff --git a/Elternbeirat.Web/Shared/Markdown.cs b/Elternbeirat.Web/Shared/Markdown.cs
index a676f21..4a78cdc 100644
--- a/Elternbeirat.Web/Shared/Markdown.cs
+++ b/Elternbeirat.Web/Shared/Markdown.cs
@@ -55,6 +55,12 @@ public static partial class Markdown
///
public const string LoneLinkClass = "lone-link";
+ ///
+ /// The default teaser length: about two lines on a card, enough to say what a
+ /// post is about without turning the list into a wall of text.
+ ///
+ public const int TeaserLength = 160;
+
private static readonly MarkdownPipeline Pipeline =
new MarkdownPipelineBuilder()
.UseCustomContainers()
@@ -120,6 +126,53 @@ public static partial class Markdown
? UpToSentenceEnd(PlainText(inline)).TrimEnd(':', ' ')
: "";
+ ///
+ /// Builds a short plain-text teaser from a Markdown text, e.g. for a post card.
+ ///
+ /// The editor's Markdown; may be .
+ ///
+ /// The longest teaser, in characters, before the ellipsis. Defaults to
+ /// .
+ ///
+ ///
+ /// The text of all paragraphs (including those in lists, quotes and :::
+ /// blocks), with all Markdown removed and whitespace collapsed. A longer text is
+ /// cut at the last word boundary within and ends
+ /// in …. An empty string when there is no paragraph text at all.
+ ///
+ ///
+ /// is zero or negative.
+ ///
+ ///
+ /// Headings are skipped: on a card the title already heads the teaser. Unlike
+ /// the teaser does not stop at the first sentence,
+ /// because a post often opens with a short line ("Liebe Eltern,") that says
+ /// nothing on its own.
+ ///
+ /// Punctuation left dangling at the cut (,, ., : …) is
+ /// dropped, so the teaser never ends in ,….
+ ///
+ ///
+ ///
+ ///
+ /// Markdown.Teaser("## Rückblick\n\nDer **Basar** war ein voller Erfolg.", 20);
+ /// // "Der Basar war ein…"
+ ///
+ ///
+ public static string Teaser(string? markdown, int maxLength = TeaserLength) =>
+ maxLength <= 0
+ ? throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, "The teaser needs room for at least one character.")
+ : string.IsNullOrWhiteSpace(markdown)
+ ? ""
+ : Shorten(
+ string.Join(
+ ' ',
+ Parse(markdown)
+ .Descendants()
+ .Select(paragraph => paragraph.Inline is { } inline ? PlainText(inline) : "")
+ .Where(text => text.Length > 0)),
+ maxLength);
+
///
/// Parses Markdown with the site's pipeline, for callers that take the document
/// apart before rendering it (see ).
@@ -192,6 +245,26 @@ public static partial class Markdown
private static string UpToSentenceEnd(string text) =>
SentenceEnd().Match(text) is { Success: true } end ? text[..(end.Index + 1)] : text;
+ ///
+ /// Cuts a text to a word boundary and marks the cut.
+ ///
+ /// Plain text.
+ /// The longest result before the ellipsis.
+ ///
+ /// The text unchanged if it fits; otherwise everything up to the last space
+ /// within , trailing punctuation removed, plus
+ /// …. A single word longer than is cut hard.
+ ///
+ ///
+ /// The space is searched one character past the limit: if the text breaks
+ /// exactly at , the last whole word still fits.
+ ///
+ private static string Shorten(string text, int maxLength) =>
+ text.Length <= maxLength
+ ? text
+ : (text[..(maxLength + 1)].LastIndexOf(' ') is var space and > 0 ? text[..space] : text[..maxLength])
+ .TrimEnd(' ', ',', ';', ':', '.', '-', '–') + "…";
+
///
/// Matches the end mark of a sentence: ., ! or ? before a
/// space or the end, unless it follows a lone letter (z.) or a number
diff --git a/Elternbeirat.Web/wwwroot/app.css b/Elternbeirat.Web/wwwroot/app.css
index cddaf32..6dce495 100644
--- a/Elternbeirat.Web/wwwroot/app.css
+++ b/Elternbeirat.Web/wwwroot/app.css
@@ -54,6 +54,11 @@
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
@@ -165,6 +170,7 @@
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 */
@@ -289,6 +295,22 @@ h1:focus {
outline: none;
}
+/* 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. */
@@ -400,12 +422,12 @@ h1:focus {
}
}
-/* --- Shared card styles for content lists --------------------------------------
- Posts, events and FAQs appear both on their own page (PostList/EventList/
- FaqList) AND embedded on the home page (the *Embed components), with the same
- class names. These rules live here (global) so both places share one look
- instead of duplicating the CSS per component. The card look: a white surface
- lifted off the tinted page background with a soft shadow and a gentle hover. */
+/* --- 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 {
@@ -416,29 +438,6 @@ h1:focus {
gap: var(--space-3);
}
-.post-list li,
-.event-list li {
- background: var(--color-bg);
- border-radius: var(--radius);
- box-shadow: var(--shadow);
- transition: box-shadow var(--transition);
-}
-
-.post-list li:hover,
-.event-list li:hover {
- box-shadow: var(--shadow-hover);
-}
-
-/* The whole card is one link. The fills the card and carries the padding (so
- the entire surface is clickable, not just the text), and resets link colour and
- underline so the content inside -- title, date, location -- keeps its own look. */
-.card-link {
- display: block;
- padding: var(--space-3) var(--space-4);
- color: inherit;
- text-decoration: none;
-}
-
/* 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
@@ -456,7 +455,10 @@ h1:focus {
box-shadow: var(--shadow-hover);
}
-.faq-topic > summary {
+/* A 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;
@@ -467,17 +469,17 @@ h1:focus {
gap: var(--space-2);
}
-.faq-topic > summary::-webkit-details-marker {
+.disclosure > summary::-webkit-details-marker {
display: none;
}
-.faq-topic > summary::before {
+.disclosure > summary::before {
content: "▸";
color: var(--color-accent-dark);
font-size: 0.9rem;
}
-.faq-topic[open] > summary::before {
+.disclosure[open] > summary::before {
content: "▾";
}
@@ -491,57 +493,6 @@ h1:focus {
color: var(--color-text-soft);
}
-/* Post and event cards are built the same way, so they read as one style on the
- home page where both appear: the title leads in the brand colour (the whole card
- is the link, so the title itself is plain text), the date sits quietly beneath. */
-.post-title,
-.event-title {
- display: block;
- font-size: 1.15rem;
- font-weight: 600;
- color: var(--color-brand);
-}
-
-/* The date's small, softened look comes from PostDate.razor.css / EventDate; here
- only the shared spacing: nudge it down from the title above. */
-.post-list time,
-.event-list time {
- margin-top: var(--space-1);
-}
-
-/* The location sits on its own quiet line beneath the date. */
-.event-location {
- display: block;
- margin-top: var(--space-1);
- color: var(--color-text-soft);
-}
-
-.event-note {
- margin: var(--space-2) 0 0;
- color: var(--color-text);
-}
-
-/* Past events (only on the full events page): same cards, quieter and flat. */
-.event-list-past li {
- box-shadow: none;
- border: 1px solid var(--color-border);
- background: var(--color-surface);
-}
-
-.event-list-past li:hover {
- box-shadow: none;
-}
-
-.event-list-past time {
- color: var(--color-text-soft);
- font-weight: 400;
-}
-
-.event-list-past .event-title {
- font-weight: 400;
- color: var(--color-text-soft);
-}
-
/* FAQ: each question is an expandable card with an accent +/– marker. */
.faq-item {
background: var(--color-bg);
diff --git a/docs/redaktion.md b/docs/redaktion.md
index f28e39f..7009429 100644
--- a/docs/redaktion.md
+++ b/docs/redaktion.md
@@ -178,6 +178,11 @@ der Startseite als Vorschau angeteasert.
- `slug` ist englisch, klein, ohne Umlaute (z. B. `new-sports-hall-opened`) —
dieselbe erzwungene Feldregel wie bei Seiten (siehe *Seiten → Slug-Regel wird
erzwungen*).
+- **Vorschautext auf der Karte:** Die Karte zeigt die ersten rund 160 Zeichen
+ des `body` als reinen Text (Überschriften und Formatierung fallen weg, gekürzt
+ am Wortende mit „…“). Ein eigenes Feld dafür gibt es nicht — beginne den Text
+ deshalb am besten mit einem Satz, der sagt, worum es geht. Auf `/posts`
+ erscheint der neueste Beitrag als große Karte über die volle Breite.
---
@@ -188,8 +193,23 @@ mehrtägige), `title`, `location` (optional), `note` (optional) und `public`.
Kein Slug.
Die Übersicht `/events` trennt automatisch in kommende und vergangene Termine;
-die Reihenfolge der Records spielt keine Rolle. Besucher können `/events.ics` in
-ihrer Kalender-App abonnieren — der Feed entsteht aus denselben Records.
+die Reihenfolge der Records spielt keine Rolle. Vergangene Termine stehen
+zugeklappt unter „Vergangene Termine anzeigen“. Besucher können `/events.ics` in
+ihrer Kalender-App abonnieren (Knopf „Kalender abonnieren“) — der Feed entsteht
+aus denselben Records. Jeder kommende Termin hat außerdem einen Link „In Kalender
+übernehmen“, der nur diesen einen Termin als Kalenderdatei liefert.
+
+So erscheint ein Termin auf seiner Karte:
+
+- Links ein **Kalenderblatt** mit Monat, Tag und Wochentag des `start`.
+- Daneben die **Uhrzeit**: nur der Beginn, mit `end` am selben Tag die Spanne
+ (`19:30–21:00 Uhr`), bei mehrtägigen Terminen „bis …“ mit dem letzten Tag. Ein
+ ganztägiger Termin (Uhrzeit `00:00`) ohne `end` zeigt keine Uhrzeit.
+- Darunter `location` und `note`. Auf der Startseite wird `note` nach zwei
+ Zeilen abgeschnitten, auf `/events` steht sie ganz.
+
+Einen Einleitungstext über der Liste schreibst du in den `body` der Seite mit
+dem Slug `events` (unter `pages`); die Website fügt keinen eigenen hinzu.
> **Trage einfach die Ortszeit ein.** Gib die Uhrzeit ein, die auf der Seite
> stehen soll (Europe/Berlin) — z. B. `08:00`. PocketBase speichert intern in UTC
diff --git a/pb/pb_migrations/1727200000_dev_seed.js b/pb/pb_migrations/1727200000_dev_seed.js
index 2afb43a..a4a9847 100644
--- a/pb/pb_migrations/1727200000_dev_seed.js
+++ b/pb/pb_migrations/1727200000_dev_seed.js
@@ -25,7 +25,7 @@ migrate((app) => {
,
posts: [{"body":"In fast allen Familien wird über die Handy-Nutzung diskutiert. Auch im schulischen Alltag ist das immer wieder ein Thema.\n\nSinnvolles und weniger Sinnvolles ist mit dem Handy möglich. Viele Jugendliche haben das Handy bis in die späten Abendstunden und über Nacht bei sich, was natürlich zur Nutzung verführt. Innerhalb der Familie dazu Regelungen zu finden, ist nicht einfach. Auch wir in der Schule kennen diese Gespräche.\n\nWir möchten Ihnen ein Angebot digitaler Elternmodule zu diesem Themenfeld vorstellen und kurz aus schulischer, sozialpädagogischer und strafrechtlicher Sicht zur Handynutzung informieren.\n\n**Dienstag, 18. Februar 2025 um 18 Uhr im Hörsaal 2.**\n\nBitte geben Sie kurz über die Klassenleitung Rückmeldung, dass Sie teilnehmen werden.","date":"2025-02-07 12:00:00.000Z","public":true,"slug":"elternabend-handy-nutzung","title":"Elternabend zur Handy-Nutzung"},{"body":"Am 8. Februar öffnete die IGMH ihre Türen für interessierte Eltern und Kinder, die sich einen Eindruck von unserer Schule verschaffen wollten. Besonders Eltern, die planen, ihre Kinder im kommenden Schuljahr anzumelden, konnten sich umfassend informieren und direkt einen Anmeldetermin vereinbaren.\n\n## Vielfältige Einblicke in das Schulleben\n\nAn verschiedenen Stationen lernten die Besucherinnen und Besucher das breite Angebot der IGMH kennen. Die Eliteschule des Sports präsentierte ihre Fördermöglichkeiten, und Schulhund Tonka ließ viele Kinderherzen höherschlagen. Auch der Elternbeirat und der Förderverein der IGMH informierten in der Aula über ihre Arbeit.\n\nFür die jungen Gäste gab es zahlreiche Mitmach-Aktionen:\n\n- Im römischen Zimmer konnten sie sich spielerisch mit der lateinischen Sprache beschäftigen.\n- Im Chemielabor wurden spannende Experimente durchgeführt.\n- Die Kletterwand war ein echter Publikumsmagnet, hier ging es unter professioneller Sicherung hoch hinaus.\n\n## Unterhaltung und kulinarische Highlights\n\n- Die Schülerband begeisterte das Publikum mit ihren Auftritten in der Aula.\n- Im Hörsaal 2 führten die 6. und 7. Klassen eine unterhaltsame Theateraufführung auf.\n- Für das leibliche Wohl war bestens gesorgt, ob im gemütlichen Schülercafé oder im Bistro Français.\n\nDer Tag der offenen Tür war ein voller Erfolg und lockte zahlreiche Besucherinnen und Besucher an. Die positive Resonanz zeigt, wie groß das Interesse an unserer Schule ist.","date":"2025-02-09 12:00:00.000Z","public":true,"slug":"tag-der-offenen-tuer-2025","title":"Rückblick: Tag der offenen Tür an der IGMH"},{"body":"Digitale Medien spielen eine zentrale Rolle im Alltag von Kindern und Jugendlichen, sei es das Smartphone, ein Tablet oder die Spielkonsole. Doch was machen junge Menschen eigentlich damit? Welcher Erwachsene weiß genau, was sich auf WhatsApp, Instagram oder Snapchat abspielt? Und was passiert auf TikTok? Wann wird ein Videospiel zur Sucht? Und welches Vorbild geben wir als Erwachsene ab, wenn auch unser eigener Alltag vom Smartphone bestimmt wird?\n\n## Digitaler Medienelternabend an der IGMH\n\nVor Kurzem fand an der IGMH ein digitaler Medienelternabend statt, der auf großes Interesse stieß. Frau Ruthig, stellvertretende Mittelstufenleiterin, führte in das Thema ein und beleuchtete die Herausforderungen der Mediennutzung.\n\nAnschließend erklärte Schulpolizist Herr Ebert die juristischen Aspekte. Dabei zeigte er auf, welche Möglichkeiten Eltern haben, wenn ihr Kind von Cybermobbing betroffen ist:\n\n- Gespräch mit Schule und Polizeibeamten\n- Gespräch mit dem Präventionsbeamten der Polizei\n- Durchführung eines Präventionstermins in der Klasse\n- Mit Einverständnis der Eltern des Täters: Gespräch mit dem Täter\n- Mit Einverständnis der Eltern des Täters: Gespräch mit Täter, Opfer, Eltern und Schule\n- Strafanzeige durch die Polizei\n\nWeiterführende Informationen zu Computerspielen finden Sie unter [usk.de](http://www.usk.de) (USK Unterhaltungssoftware) und [zavatar.de](http://www.zavatar.de).\n\n## Herausforderungen der Handynutzung aus sozialpädagogischer Sicht\n\nFrau Graske (Sozialpädagogin) stellte die Herausforderungen aus Sicht der Schulsozialarbeit dar und gab Eltern konkrete Tipps:\n\n**1. Bildschirmzeit im Blick behalten**\n\n**2. Handy am Abend außer Reichweite bringen.** Vereinbaren Sie einen festen Schlafplatz für das Handy und aktivieren Sie einen Ruhemodus.\n\n**3. Online-Kontakte des Kindes sensibel begleiten.** Lassen Sie sich Chat-Gruppen zeigen, schauen Sie gemeinsam das TikTok-Profil an und nutzen Sie Ratschläge von [Clemens hilft](https://www.clemenshilft.de/).\n\n**4. Bleiben Sie die wichtigste Ansprechperson für Ihr Kind.** Zeigen Sie, dass Ihr Kind Ihnen vertrauen kann, begründen Sie Ihre Maßnahmen und bleiben Sie offen für Fragen. Der [Mediennutzungsvertrag](https://www.mediennutzungsvertrag.de/) hilft bei klaren Absprachen.\n\n**5. Denken Sie auch an sich selbst.** Anerkennen Sie Ihren Einsatz, seien Sie nachsichtig mit sich, wenn Pläne nicht sofort aufgehen, und holen Sie sich bei Frustration Unterstützung.\n\n## Kontakt zur Schulsozialarbeit\n\nBei Fragen zur Handynutzung aus sozialpädagogischer Sicht können Sie sich an die Schulsozialarbeit der IGMH wenden (Raum CO.03).\n\n## Weitere hilfreiche Informationen\n\n1. [Klicksafe](https://www.klicksafe.de/), die EU-Initiative für mehr Sicherheit im Netz, unterstützt Eltern mit Informationen und Tipps zum kompetenten Umgang mit digitalen Medien.\n2. Der [Mediennutzungsvertrag](https://www.mediennutzungsvertrag.de/) hilft, gemeinsam mit dem Kind Regeln für die Mediennutzung festzulegen.","date":"2025-02-18 12:00:00.000Z","public":true,"slug":"digitale-medien-herausforderung","title":"Digitale Medien: Herausforderung für Jugendliche und Eltern"},{"body":"Kürzlich fand die Wahl des neuen Vorstands des Elternbeirats statt. Sie wurde notwendig, da mehrere Mitglieder aus dem Gremium ausgeschieden sind, unter anderem aufgrund eines Schulwechsels.\n\nLeider wurde die Wahl nur von wenigen Eltern besucht, was bedauerlich ist, da die Elternarbeit eine wichtige Rolle für unsere Schule spielt. Dennoch konnte die Wahl erfolgreich durchgeführt werden.\n\nGewählt wurde folgender Vorstand des Elternbeirats der IGMH:\n\n**Elternbeiratsvorsitz**\n\n- Dina Losert\n\n**Stellvertretende Elternbeiratsvorsitzende**\n\n- Bernhard Topschall\n- Maik Palm\n\n**Beisitzerinnen und Beisitzer**\n\n- Marielle Ianello\n- Gentiana Erol\n- Jenny Reger-Stilgenbauer\n- Michael Weber\n- Marion Leininger\n- Raul Chira\n\nAußerdem wurden die ständigen Mitglieder sowie deren Stellvertreterinnen und Stellvertreter für die Schulkonferenz gewählt. In der ersten Sitzung werden die konkreten Aufgaben verteilt, zum Beispiel die Vertretung im Mensarat oder im Förderverein.\n\nWir freuen uns auf eine engagierte und erfolgreiche Zusammenarbeit im Interesse aller Schülerinnen und Schüler.","date":"2025-02-26 12:00:00.000Z","public":true,"slug":"vorstand-gewaehlt-februar-2025","title":"Neuer Vorstand des Elternbeirats gewählt (Februar 2025)"},{"body":"Am 1. Oktober 2025 war es endlich so weit: Die IGMH feierte die Einweihung ihrer neuen Sporthalle. Oberbürgermeister Christian Specht übergab das moderne Gebäude im Wert von rund 23 Millionen Euro offiziell seiner Bestimmung. 3,85 Millionen Euro der Kosten wurden durch das Bundesprogramm „Sanierung kommunaler Einrichtungen\" gefördert.\n\nNach vielen Jahren des Wartens und Planens ist damit ein echtes Zukunftsprojekt für unsere Schule und die ganze Neckarstadt entstanden. Die alte Halle konnte aufgrund schwerer Bauschäden nicht mehr genutzt werden, so fiel die Entscheidung für einen kompletten Neubau.\n\nEntstanden ist eine helle, moderne und barrierefreie Sportanlage mit einer großen Dreifeldhalle und einer kleineren Einfeldhalle, die sowohl im Schulalltag als auch von Vereinen genutzt werden können. Eine Photovoltaikanlage, Dachbegrünung und moderne Wärmeschutztechnik sorgen dafür, dass auch das Thema Nachhaltigkeit nicht zu kurz kommt.\n\nAuch architektonisch ist die neue Halle ein Hingucker: viel Glas, ein elegantes Holztragwerk und eine dunkle Fassade, die sich harmonisch in den Herzogenriedpark einfügt. „Ein Leuchtturmprojekt für Bildung, Sport und Nachhaltigkeit\", betonte Oberbürgermeister Specht bei der Einweihung.\n\nDie Schülerinnen und Schüler dürfen sich auf ideale Bedingungen für Unterricht und Training freuen, und die IGMH kann stolz sein auf eine Sporthalle, die ihrem Namen als Eliteschule des Sports wirklich gerecht wird.","date":"2025-10-05 12:00:00.000Z","public":true,"slug":"neue-sporthalle-eroeffnet","title":"Neue Sporthalle feierlich eröffnet"},{"body":"Wir laden herzlich zur Vollversammlung des Elternbeirats der IGMH ein.\n\n**Datum:** Donnerstag, 13. November 2025\n**Uhrzeit:** 19:00 Uhr\n**Ort:** Hörsaal 2 der IGMH\n\n## Tagesordnung\n\n1. Begrüßung und Feststellung der Beschlussfähigkeit, Wahlleitung und Protokoll\n2. Vorstellung der Kandidatinnen und Kandidaten\n3. Wahl der Mitglieder des EBV-Vorstands\n4. Wahl der Mitglieder der Schulkonferenz\n5. Mitteilung über Änderung der Geschäftsordnung\n6. Schlusswort\n\n## Wir brauchen Sie\n\nZur Wahl stehen der Elternbeiratsvorstand, zwei stellvertretende Vorsitzende sowie Beisitzerinnen und Beisitzer. Als Beisitzerin oder Beisitzer nehmen Sie beispielsweise an den Sitzungen des Elternbeirats teil, die in der Regel alle ein bis zwei Monate stattfinden. Es gibt vielfältige Möglichkeiten, sich je nach zeitlicher Verfügbarkeit einzubringen. Jede Form der Unterstützung ist von großem Wert.\n\nFalls Sie Interesse haben, sich aufstellen zu lassen, oder Fragen zur Arbeit des Elternbeirats haben, melden Sie sich gerne vorab über die [Kontaktseite](/contact) oder sprechen Sie uns direkt am Wahlabend an.\n\nWir kommen hiermit einer Informationspflicht nach Punkt 9 der Geschäftsordnung nach:\n\n> Eine Änderung der Geschäftsordnung erfolgt durch Mehrheitsbeschluss des Vorstands. Die Information über die geänderte Geschäftsordnung erfolgt mit Veröffentlichung auf der Homepage des Elternbeirates und in der nächsten Elternbeiratssitzung.","date":"2025-10-24 12:00:00.000Z","public":true,"slug":"einladung-vollversammlung-2025","title":"Einladung zur Vollversammlung des Elternbeirats"},{"body":"Die Schule bietet ab sofort einen neuen Service an: Lost and Found, eine Fundstelle für verlorene Gegenstände.\n\nAb sofort gibt es eine digitale Möglichkeit, nach verlorenen Gegenständen zu suchen. Über 200 Fundsachen wurden fotografiert und in Bildergalerien nach Kategorien auf der Homepage der IGMH veröffentlicht.\n\nAlle Informationen und die Bilderstrecken finden Sie beim [Lost-and-Found-Service der IGMH](https://igmh.de/presse/fundsachen-bilder-durchklicken-verlorenes-wiederfinden).\n\nWer etwas auf den Fotos oder in der Beschreibung wiedererkennt, kann sich per E-Mail an [heike.scholz@igmh.de](mailto:heike.scholz@igmh.de) wenden.","date":"2025-10-29 12:00:00.000Z","public":true,"slug":"lost-and-found","title":"Lost and Found: Fundstelle für verlorene Gegenstände"},{"body":"Am 13. November 2025 fand die turnusmäßige Wahl des Elternbeirats der IGMH im Hörsaal 2 statt. Besetzt wurden der Vorstand, die Beisitzerinnen und Beisitzer, die Mitglieder der Schulkonferenz sowie zwei Kassenprüferinnen bzw. Kassenprüfer. Die Versammlung war mit 35 wahlberechtigten Elternvertreterinnen und Elternvertretern beschlussfähig.\n\nMit der Wahl beginnt die nächste Amtsperiode, in der die gewählten Mitglieder die Interessen der Eltern vertreten, die Zusammenarbeit mit der Schulleitung begleiten und Impulse für das Schulleben setzen. Im Anschluss wurden aktuelle Themen angesprochen, darunter die Pausenregelung, der Umgang mit Mobiltelefonen, der digitale Zugang zu Untis, die Situation der Schultoiletten sowie die Kommunikation zwischen Elternhaus und Schule.\n\n## Ergebnisse im Überblick\n\n**Vorstand Elternbeirat**\n\n- 1. Vorsitz: Maik Palm\n- Stellvertretung: Gentiana Erol\n- Stellvertretung: Raul Chira\n\n**Beisitzer**\n\n- Neu gewählt: Susanne Naumburg, Rebecca Goldmann, Isabell Vesper\n- Im Amt verbleibend: Mariella Ianello, Marion Leininger, Jenny Reger-Stilgenbauer, Michael Weber\n\n**Schulkonferenz**\n\n- Mitglied kraft Amtes: Maik Palm\n- Feste Sitze: Gentiana Erol, Tanja Engelhardt, Jenny Reger-Stilgenbauer\n- Ersatzmitglieder: Franziska Schaaf, Susanne Naumburg, Michael Weber, Cemile Ökzüz\n\n**Kassenprüfer**\n\n- Franziska Wieczorek\n- Michael Vokt\n\nDas ausführliche Protokoll steht im Downloadbereich zur Verfügung.","date":"2025-11-26 12:00:00.000Z","public":true,"slug":"vollversammlung-wahl-november-2025","title":"Vollversammlung des Elternbeirats: Wahl am 13. November 2025"},{"body":"Bitte unterstützen Sie das Schülercafé der IGMH mit Ihrer Stimme. Das Café soll verschönert und gemütlicher gestaltet werden, dafür zählt jede Stimme von Eltern und Unterstützern.\n\n**Hier können Sie abstimmen:** [zum Abstimmen beim Sparda-Impuls](https://www.spardaimpuls.de/projekte/68f9d6ec1d40df0c78f35285).\n\n## Dafür machen wir uns stark\n\nDas Schulcafé der IGMH ist ein Ort des Miteinanders. In den Mittagspausen verkaufen Schülerinnen und Schüler verschiedene gesunde Mittagssnacks, vom Sandwichtoast über Müsli bis zum Obstsalat, im Sommer darf es auch einmal ein Eis sein. Die helfenden Jugendlichen bekommen erste Einblicke in die gastronomische Arbeitswelt und engagieren sich für die Gemeinschaft.\n\nDie Überschüsse aus dem Verkauf fließen in ein Projekt, das Schülerinnen und Schüler bei der Finanzierung von Klassenfahrten unterstützt, die finanziell belastet sind und nicht von Bildung und Teilhabe profitieren können. Um dieses Projekt aufrechterhalten zu können, bewerben wir uns beim Sparda-Impuls.","date":"2025-11-12 12:00:00.000Z","public":true,"slug":"schulcafe-sparda-impuls-abstimmung","title":"Stimmen Sie für das Schulcafé beim Sparda-Impuls"},{"body":"Nachdem es anfangs recht schleppend mit den Stimmen voranging, begaben sich die Elternsprecher im wahrsten Sinne des Wortes auf Stimmenfang. Zwischenzeitlich belegten wir sogar kurzzeitig einen der ersten drei Plätze.\n\nDer Schlussspurt war ein regelrechter Krimi. Zwischen dem fünften und sechsten Platz lagen zeitweise nur drei Stimmen Unterschied. Am Ende erreichte das Projekt Schulcafé beim Sparda-Impuls-Wettbewerb einen hervorragenden fünften Platz mit 2.490 Stimmen.\n\nDank dieses großartigen Ergebnisses kann das Schulcafé nun mit den gewonnenen 3.000 Euro verschönert werden, sehr zur Freude aller Schülerinnen und Schüler der IGMH.\n\nDer Elternbeiratsvorstand gratuliert herzlich zum fünften Platz und bedankt sich bei allen Eltern, die engagiert Werbung für das Projekt gemacht haben, und natürlich bei allen, die abgestimmt und damit zum Erfolg beigetragen haben.\n\n## Kurz zusammengefasst\n\n- Projekt: Verschönerung des IGMH-Schulcafés\n- Wettbewerb: Sparda-Impuls\n- Ergebnis: 5. Platz\n- Stimmen: 2.490\n- Fördersumme: 3.000 Euro","date":"2025-12-04 12:00:00.000Z","public":true,"slug":"schulcafe-platz-5-sparda-impuls","title":"Das Schulcafé holt Platz 5 beim Sparda-Impuls-Wettbewerb"},{"body":"Am 9. Dezember fand die erste Sitzung des Elternbeiratsvorstands im Schuljahr 2025/26 statt. Im Mittelpunkt stand die Besetzung zentraler Aufgaben und Zuständigkeiten. Dabei wurde festgelegt, wer den Austausch mit dem Förderverein übernimmt, wer im Mensarat mitarbeitet und welche weiteren organisatorischen Aufgaben wahrgenommen werden.\n\n## Zusammenfassung\n\n- Förderverein: Mariella Ianello, Marion Leininger\n- Mensarat: Gentiana Erol, Raul Chira, Mariella Ianello, Maik Palm\n- Kasse und Protokoll: Marion Leininger\n- Homepage: Maik Palm\n- Schaukasten: Jenny Reger-Stilgenbauer\n\nDie Kasse wurde ordnungsgemäß an die zukünftige Kassenwartin übergeben. Die nächste Sitzung des Elternbeiratsvorstands ist für Ende Januar 2026 geplant.","date":"2025-12-12 12:00:00.000Z","public":true,"slug":"ebv-sitzung-dezember-2025","title":"1. EBV-Sitzung im Schuljahr 2025/26"},{"body":"Kreativität, Teamgeist und eine stimmungsvolle Atmosphäre: Auch in diesem Jahr war der Adventstüren-Contest ein voller Erfolg.\n\nDie Schülerinnen und Schüler hatten wieder die Gelegenheit, ihre Klassenzimmertür weihnachtlich zu dekorieren. Die Schülermitverantwortung (SMV) lobt den Contest jährlich aus und bietet den Klassen damit die Möglichkeit, ihre Klassenkasse aufzubessern.\n\nDer Förderverein der IGMH unterstützt die SMV dabei jedes Jahr und stellt das Preisgeld zur Verfügung. In diesem Jahr wurde das Preisgeld zusätzlich durch den Elternbeirat aufgestockt, der den Wettbewerb mit weiteren 280 Euro unterstützte.\n\nAm Mittwoch, den 17. Dezember, fand in der ersten großen Pause die feierliche Preisverleihung statt. Der Adventstüren-Contest sorgte auch in diesem Jahr für eine stimmungsvolle vorweihnachtliche Atmosphäre an der Schule.\n\n## Zusammenfassung\n\n- Die SMV organisiert jährlich den Adventstüren-Contest.\n- Der Förderverein stellt das Preisgeld bereit.\n- Der Elternbeirat unterstützt zusätzlich mit 280 Euro.\n- Preisverleihung: Mittwoch, 17. Dezember, in der 1. großen Pause.","date":"2025-12-18 12:00:00.000Z","public":true,"slug":"adventstueren-contest-2025","title":"Adventstüren-Contest"},{"body":"Am 10. März 2026 fand die zweite Sitzung des Elternbeiratsvorstands statt. Im Mittelpunkt standen der Mensabetrieb, Rückerstattungen des ehemaligen Caterers, Maßnahmen zur Mobbingprävention sowie organisatorische Fragen. Das Treffen verlief konstruktiv und lösungsorientiert.\n\n## Mensabetrieb und Austausch mit FMD\n\nFür den 18. März wurde ein Austausch mit dem Caterer FMD angekündigt. Fragen der Eltern zum Mensabetrieb sollten über die Elternsprecher gesammelt und gebündelt weitergeleitet werden. Deutlich wurde, dass die zweite Essensausgabeschlange für die siebten Klassen derzeit nicht wie vorgesehen funktioniert. Dieses Thema sollte gezielt angesprochen werden.\n\n## Rückerstattungen des ehemaligen Caterers\n\nBeim ehemaligen Caterer Ehrenfried haben noch nicht alle Eltern, die Beiträge überwiesen hatten, ihre Rückerstattung erhalten. Der Großteil der ausstehenden Beträge wurde jedoch bereits zurückgezahlt.\n\n## Mobbingprävention\n\nDa es vermehrt zu Mobbing- und Gewaltvorfällen kommt, soll sich der Vorstand künftig intensiver mit Mobbingprävention befassen. Konkrete Vorschläge sollen an die Schulleitung herangetragen werden, beispielsweise unter Einbeziehung externer Stellen wie dem Haus des Jugendrechts.\n\n## Themen für die Schulleitung\n\nMehrere offene Punkte werden an die Schulleitung herangetragen, darunter der Wunsch nach einem Schulfotografen, der aktuelle Stand beim Elternzugang zu WebUntis und die Wiederaufnahme der Quartalsgespräche mit der Schulleitung.\n\nDie Sitzung war von einem konstruktiven Austausch geprägt und setzte wichtige Impulse für die weitere Elternarbeit.","date":"2026-03-21 12:00:00.000Z","public":true,"slug":"ebv-sitzung-maerz-2026","title":"Zweite Sitzung des Elternbeiratsvorstands am 10. März 2026"},{"body":"Am 18. März fand ein Austausch mit Mitarbeitenden des Caterers FMD, Elternvertreterinnen und Elternvertretern, Lehrkräften sowie Schülerinnen und Schülern der SMV statt. Insgesamt war es ein sehr konstruktives Gespräch. Die Eltern hatten im Vorfeld die Möglichkeit, über ihre Elternvertretungen Fragen und Anregungen einzureichen.\n\n## Fragen und Anmerkungen der Eltern\n\n- Mehrere Eltern äußerten Fragen zu den erhöhten Preisen.\n- Einige Kinder berichten, dass sie trotz Vorbestellung keinen Nachtisch erhalten haben, obwohl dieser bezahlt wurde.\n- Positiv war, dass laktosefreie und vegetarische Menüs angeboten werden. Werden beide am selben Tag angeboten, ist die Auswahl jedoch eingeschränkt. Angeregt wurde ein abwechslungsreicherer Speiseplan.\n- Angeregt wurde, PayPal als zusätzliche Aufladefunktion für das Essenskonto zu prüfen.\n- Gewünscht wurden kleinere Snacks oder einfache Gerichte zum Barzahlen, etwa belegte Brötchen oder Brezeln.\n- Viele Eltern sprachen ausdrücklich ein Lob aus, dass sich das Essen insgesamt deutlich verbessert habe.\n\n## Rückmeldungen von FMD\n\nFMD erklärte, dass der Preisspielraum begrenzt sei, da die Preise im Vergleich zu anderen Caterern bereits im unteren Bereich lägen. Nach Vorgaben der Stadt gibt es nur einmal pro Woche ein Fleischgericht. Als tägliche Alternative gibt es weiterhin Spaghetti Bolognese, die nicht vorbestellbar sind, aber jeden Tag bar oder per Chip erhältlich.\n\nFalls Kinder einen vorbestellten Nachtisch nicht erhalten haben, sollen sie bitte aktiv danach fragen. Hierzu wird es zusätzlich einen Aushang geben. Zudem wird geprüft, ob künftig wieder eine Salatbar angeboten werden kann. PayPal wurde angesprochen, hier hat FMD als Teil der GBG-Gruppe keine alleinige Entscheidungsfreiheit, nimmt das Anliegen aber mit.\n\n## Kiosk und Snack-Angebot\n\nDer Kiosk bietet täglich ab 7:15 Uhr sowie in der Mittagszeit kleine Snacks an, zahlbar bar oder mit Chip. Dort liegt ein Wünschebuch aus, in das die Kinder gewünschte Produkte eintragen können. Ein mobiler Snack-Trolley ist aus personellen und hygienischen Gründen leider nicht umsetzbar.\n\n## Zusammenfassung\n\n- Der Austausch mit FMD verlief konstruktiv und offen.\n- Zentrale Themen waren Preise, Dessertausgabe, Speiseplan, Snack-Angebote und Pausenregelung.\n- Spaghetti Bolognese bleibt täglich verfügbar, auch wenn sie nicht vorbestellbar ist.\n- Eine Salatbar wird geprüft, PayPal als Zahlungsoption angeregt.\n- Die Fragen zur Pausenregelung werden in der Schulkonferenz weiter beraten.","date":"2026-03-22 12:00:00.000Z","public":true,"slug":"mensa-austausch-fmd-maerz-2026","title":"Mensa-Austausch mit FMD am 18. März 2026"}]
,
- events: [{"end":"","location":"","note":"Die neue Internetseite des Elternbeirats der IGMH löst die bisherige ab und ist ab diesem Tag unter der neuen Adresse erreichbar.","public":true,"start":"2026-10-14 22:00:00.000Z","title":"Neue Website des Elternbeirats geht online"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-02-07 23:00:00.000Z","title":"Tag der offenen Tür der IGMH"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-02-24 23:00:00.000Z","title":"Wahl des Elternbeiratsvorstands"},{"end":"","location":"Hörsaal 2 der IGMH","note":"","public":true,"start":"2025-11-13 18:00:00.000Z","title":"Vollversammlung des Elternbeirats mit Vorstandswahl"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-12-08 23:00:00.000Z","title":"1. Sitzung des Elternbeiratsvorstands"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-12-16 23:00:00.000Z","title":"Adventstüren-Contest"},{"end":"","location":"IGMH","note":"","public":true,"start":"2026-03-09 23:00:00.000Z","title":"2. Sitzung des Elternbeiratsvorstands"}]
+ events: [{"end":"","location":"","note":"Die neue Internetseite des Elternbeirats der IGMH löst die bisherige ab und ist ab diesem Tag unter der neuen Adresse erreichbar.","public":true,"start":"2026-10-14 22:00:00.000Z","title":"Neue Website des Elternbeirats geht online"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-02-07 23:00:00.000Z","title":"Tag der offenen Tür der IGMH"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-02-24 23:00:00.000Z","title":"Wahl des Elternbeiratsvorstands"},{"end":"","location":"Hörsaal 2 der IGMH","note":"","public":true,"start":"2025-11-13 18:00:00.000Z","title":"Vollversammlung des Elternbeirats mit Vorstandswahl"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-12-08 23:00:00.000Z","title":"1. Sitzung des Elternbeiratsvorstands"},{"end":"","location":"IGMH","note":"","public":true,"start":"2025-12-16 23:00:00.000Z","title":"Adventstüren-Contest"},{"end":"","location":"IGMH","note":"","public":true,"start":"2026-03-09 23:00:00.000Z","title":"2. Sitzung des Elternbeiratsvorstands"},{"end":"2026-11-21 12:00:00.000Z","location":"Schulhof der IGMH","note":"Kuchenspenden bitte bis Freitag bei der Klassenleitung anmelden.","public":true,"start":"2026-11-21 08:00:00.000Z","title":"Herbstbasar"},{"end":"2026-12-11 11:00:00.000Z","location":"Jugendherberge Heidelberg","note":"","public":true,"start":"2026-12-09 07:00:00.000Z","title":"Klausurtagung des Elternbeiratsvorstands"},{"end":"2026-07-02 22:00:00.000Z","location":"","note":"","public":true,"start":"2026-06-28 22:00:00.000Z","title":"Projektwoche"}]
,
// Topics own the grouping and the order of the FAQ page; a question links to
// its topic below via the "topic" relation, resolved by slug at seed time.