From a5a68ed122d5ad33994e87004601bc33fee35bbc Mon Sep 17 00:00:00 2001 From: tleininger Date: Thu, 1 Oct 2026 15:55:28 +0200 Subject: [PATCH 1/3] Add ::: team block rendering board members as person cards with initials avatars --- Elternbeirat.Web.Tests/PocketBaseFixture.cs | 11 +- Elternbeirat.Web.Tests/RouteSmokeTests.cs | 22 ++ Elternbeirat.Web.Tests/TeamMemberTests.cs | 161 +++++++++ Elternbeirat.Web/Shared/Markdown.cs | 54 ++- Elternbeirat.Web/Shared/TeamMember.cs | 372 ++++++++++++++++++++ Elternbeirat.Web/wwwroot/app.css | 163 +++++++++ docs/redaktion.md | 40 +++ pb/pb_migrations/1727200000_dev_seed.js | 2 +- 8 files changed, 815 insertions(+), 10 deletions(-) create mode 100644 Elternbeirat.Web.Tests/TeamMemberTests.cs create mode 100644 Elternbeirat.Web/Shared/TeamMember.cs diff --git a/Elternbeirat.Web.Tests/PocketBaseFixture.cs b/Elternbeirat.Web.Tests/PocketBaseFixture.cs index f3b9ddb..f3d632c 100644 --- a/Elternbeirat.Web.Tests/PocketBaseFixture.cs +++ b/Elternbeirat.Web.Tests/PocketBaseFixture.cs @@ -235,7 +235,16 @@ public sealed class PocketBaseFixture : IAsyncLifetime }); await CreateRecordAsync(http, "pages", new { - title = "Vorstandsteam", body = "# Vorstand", location = "header", + // The body uses the "::: team" block like production: one card with the + // full convention (bold name, role, italic duties, note) in a large block, + // and one item without the convention, which must become a plain card. + title = "Vorstandsteam", + body = "## Vorsitz\n\n::: team gross\n" + + "- **Jenny Reger-Stilgenbauer** (Vorsitzende) \n *Schulkonferenz, Mensarat* \n Mitglied im Gesamtelternbeirat.\n" + + ":::\n\n## Beisitz\n\n::: team\n" + + "- Özlem Ünal, Beisitz\n" + + ":::", + location = "header", order = 2, slug = "board", embed = Array.Empty(), @public = true, }); await CreateRecordAsync(http, "pages", new diff --git a/Elternbeirat.Web.Tests/RouteSmokeTests.cs b/Elternbeirat.Web.Tests/RouteSmokeTests.cs index 8b86734..33999fb 100644 --- a/Elternbeirat.Web.Tests/RouteSmokeTests.cs +++ b/Elternbeirat.Web.Tests/RouteSmokeTests.cs @@ -198,6 +198,28 @@ public sealed partial class RouteSmokeTests : IDisposable html.ShouldContain("
"); } + [Fact] + public async Task Board_renders_team_block_as_person_cards() + { + // The board body in the fixture holds a "::: team gross" block with one card + // following the convention and a "::: team" block with one that does not. + // Assert the cards survive the whole path: PocketBase, page, Markdown, HTML. + var client = _factory.CreateClient(); + + var html = await client.GetStringAsync(new Uri("/board", UriKind.Relative)); + + html.ShouldContain("
"); + html.ShouldContain("JR"); + html.ShouldContain("Jenny Reger-Stilgenbauer"); + html.ShouldContain("Vorsitzende"); + html.ShouldContain("Schulkonferenz"); + html.ShouldContain("Mensarat"); + html.ShouldContain("Mitglied im Gesamtelternbeirat."); + html.ShouldContain("
  • Özlem Ünal, Beisitz
  • "); + html.ShouldContain("

    Beisitz

    "); // the group headings stay normal headings + } + [Fact] public async Task Posts_and_events_render_their_page_heading_and_body() { diff --git a/Elternbeirat.Web.Tests/TeamMemberTests.cs b/Elternbeirat.Web.Tests/TeamMemberTests.cs new file mode 100644 index 0000000..231d82c --- /dev/null +++ b/Elternbeirat.Web.Tests/TeamMemberTests.cs @@ -0,0 +1,161 @@ +using Elternbeirat.Web.Shared; +using Markdig.Syntax; + +namespace Elternbeirat.Web.Tests; + +/// +/// Unit tests for the person cards of the "::: team" block: initials, avatar colour, +/// reading the list-item convention and the rendered card. Pure logic, no PocketBase +/// needed. +/// +public class TeamMemberTests +{ + [Theory] + [InlineData("Jenny Reger-Stilgenbauer", "JR")] // a double name is one word + [InlineData("Özlem Ünal", "ÖÜ")] // umlauts kept, not folded to O/U + [InlineData("ömer şahin", "ÖŞ")] // lower case is raised + [InlineData("Isabell", "I")] // a single word gives one letter + [InlineData("Anna Maria Müller", "AM")] // first and last word only + [InlineData(" Maik Palm ", "MP")] // extra whitespace ignored + [InlineData("", "")] + [InlineData("123", "")] // no letter, no initials + public void Initials_are_first_and_last_word(string name, string expected) => + TeamMember.InitialsOf(name).ShouldBe(expected); + + [Fact] + public void Color_is_stable_and_in_range() + { + // Pinned values: the colour must not change between restarts or releases, so a + // change to the hash shows up here instead of recolouring the board. + TeamMember.ColorOf("Maik Palm").ShouldBe(TeamMember.ColorOf(" Maik Palm ")); + TeamMember.ColorOf("Maik Palm").ShouldBe(4); + TeamMember.ColorOf("Gentiana Erol").ShouldBe(5); + + new[] { "Maik Palm", "Gentiana Erol", "Raul Chira", "Marion Leininger", "Isabell Vesper", "" } + .ShouldAllBe(name => TeamMember.ColorOf(name) >= 1 && TeamMember.ColorOf(name) <= TeamMember.ColorCount); + } + + [Fact] + public void Item_with_role_duties_and_note_is_read_in_full() + { + var (member, note) = Read("- **Maik Palm** (Vorsitzender) \n *Schulkonferenz, Mensarat , Homepage* \n Mitglied im Schulbeirat."); + + member.Name.ShouldBe("Maik Palm"); + member.Role.ShouldBe("Vorsitzender"); + member.Duties.ShouldBe(["Schulkonferenz", "Mensarat", "Homepage"]); + Markdown.PlainText(note).ShouldBe("Mitglied im Schulbeirat."); + } + + [Fact] + public void Item_without_role_still_reads_the_duties() + { + var (member, note) = Read("- **Jenny Reger-Stilgenbauer** \n *Schulkonferenz, Schaukasten*"); + + member.Role.ShouldBeNull(); + member.Duties.ShouldBe(["Schulkonferenz", "Schaukasten"]); + note.ShouldBeEmpty(); + } + + [Fact] + public void Item_with_only_a_name_has_no_role_and_no_duties() + { + var (member, note) = Read("- **Isabell Vesper**"); + + member.Name.ShouldBe("Isabell Vesper"); + member.Role.ShouldBeNull(); + member.Duties.ShouldBeEmpty(); + note.ShouldBeEmpty(); + } + + [Fact] + public void Soft_line_breaks_count_like_hard_ones() + { + // Editors forget the two trailing spaces; the card must not depend on them. + var (member, _) = Read("- **Raul Chira** (2. Stellvertreter)\n *Mensarat*"); + + member.Role.ShouldBe("2. Stellvertreter"); + member.Duties.ShouldBe(["Mensarat"]); + } + + [Fact] + public void Second_line_that_is_not_only_italic_becomes_the_note() + { + var (member, note) = Read("- **Rebecca Goldmann** \n Neu im Team, *herzlich willkommen*."); + + member.Duties.ShouldBeEmpty(); + Markdown.PlainText(note).ShouldBe("Neu im Team, herzlich willkommen."); + } + + [Theory] + [InlineData("- Michael Weber, Beisitz")] // no bold at all + [InlineData("- Beisitz: **Michael Weber**")] // bold, but not at the start + [InlineData("- *Michael Weber*")] // italic, not bold + public void Item_without_convention_is_not_read(string markdown) => + TeamMember.Read(FirstParagraph(markdown)).ShouldBeNull(); + + [Fact] + public void Team_block_renders_person_cards() + { + var html = Markdown.ToHtml( + "::: team\n- **Maik Palm** (Vorsitzender) \n *Schulkonferenz, Mensarat* \n Siehe [Kontakt](/contact).\n:::"); + + html.ShouldContain("
    "); + html.ShouldContain($"
  • "); + html.ShouldContain("MP"); + html.ShouldContain("Maik Palm"); + html.ShouldContain("Vorsitzender"); + html.ShouldContain("Schulkonferenz"); + html.ShouldContain("Mensarat"); + html.ShouldContain("Siehe Kontakt."); // links survive + html.ShouldNotContain("(Vorsitzender)"); + } + + [Fact] + public void Item_without_convention_renders_as_plain_card_with_its_text() + { + var html = Markdown.ToHtml("::: team\n- Michael Weber, Beisitz\n:::"); + + html.ShouldContain($"
  • Michael Weber, Beisitz
  • "); + html.ShouldNotContain("team-avatar"); + } + + [Fact] + public void Editor_text_in_a_card_is_escaped() + { + var html = Markdown.ToHtml("::: team\n- **Name** (Rolle \"x\")\n:::"); + + html.ShouldNotContain(""); + html.ShouldContain("<b>Name</b>"); + } + + [Theory] + [InlineData("::: team gross")] + [InlineData("::: team groß")] + [InlineData("::: team GROSS")] + public void Large_modifier_adds_its_class(string opening) => + Markdown.ToHtml($"{opening}\n- **Maik Palm**\n:::").ShouldContain("
    "); + + [Fact] + public void Mistyped_modifier_keeps_the_grid() + { + // A typo in the modifier costs the larger size, not the cards. + var html = Markdown.ToHtml("::: team grss\n- **Maik Palm**\n:::"); + + html.ShouldContain("
    "); + html.ShouldContain("team-card"); + } + + [Fact] + public void Lists_outside_a_team_block_are_untouched() + { + var html = Markdown.ToHtml("- **Maik Palm** (Vorsitzender)"); + + html.ShouldBe("
      \n
    • Maik Palm (Vorsitzender)
    • \n
    \n"); + } + + private static (TeamMember Member, Markdig.Syntax.Inlines.Inline[] Note) Read(string markdown) => + TeamMember.Read(FirstParagraph(markdown)) ?? throw new ShouldAssertException("The item was not read as a person."); + + private static ParagraphBlock FirstParagraph(string markdown) => + Markdown.Parse(markdown).Descendants().First(); +} diff --git a/Elternbeirat.Web/Shared/Markdown.cs b/Elternbeirat.Web/Shared/Markdown.cs index 4a78cdc..f87c5f8 100644 --- a/Elternbeirat.Web/Shared/Markdown.cs +++ b/Elternbeirat.Web/Shared/Markdown.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.RegularExpressions; using Markdig; +using Markdig.Extensions.CustomContainers; using Markdig.Renderers.Html; using Markdig.Syntax; using Markdig.Syntax.Inlines; @@ -20,8 +21,8 @@ namespace Elternbeirat.Web.Shared; /// Custom containers (::: name … :::) turn a block /// into <div class="name">. That is how editors use the /// design blocks (kennzahlen, aufruf, kacheln, -/// hinweis) without writing HTML. An unknown name just yields a -/// div without styling, so a typo never breaks a page. +/// hinweis, team) without writing HTML. An unknown name +/// just yields a div without styling, so a typo never breaks a page. /// /// /// @@ -45,8 +46,14 @@ namespace Elternbeirat.Web.Shared; /// the text around an element), but the stylesheet needs exactly that to show a /// lone mailto: link as a button and a lone PDF link as a file card. /// +/// +/// The list items of a ::: team block become person cards +/// (): the initials avatar and the split into name, +/// role and duties need the text itself, which CSS cannot read. +/// /// /// +/// public static partial class Markdown { /// @@ -183,22 +190,24 @@ public static partial class Markdown Markdig.Markdown.Parse(markdown, Pipeline); /// - /// Renders a document from to HTML, marking lone links on - /// the way, exactly as does. + /// Renders a document from to HTML, building the team cards + /// and marking lone links on the way, exactly as does. /// /// The parsed document; changed in place. /// The rendered HTML. internal static string Render(MarkdownDocument document) => - Markdig.Markdown.ToHtml(MarkLoneLinks(document), Pipeline); + Markdig.Markdown.ToHtml(MarkLoneLinks(BuildTeamCards(document)), Pipeline); /// /// Flattens inline Markdown to the text a reader sees: emphasis and link /// markup dropped, link text kept, images left out, whitespace collapsed. /// - /// The inline content, e.g. of a paragraph or heading. + /// + /// The inline content, e.g. of a paragraph or heading, or a part of it. + /// /// The visible text, trimmed. - internal static string PlainText(ContainerInline inline) => - Whitespace().Replace(AppendText(new StringBuilder(), inline).ToString(), " ").Trim(); + internal static string PlainText(IEnumerable inlines) => + Whitespace().Replace(inlines.Aggregate(new StringBuilder(), AppendText).ToString(), " ").Trim(); /// /// Whether an inline carries no visible content (whitespace or a line break). @@ -279,6 +288,35 @@ public static partial class Markdown [GeneratedRegex(@"\s+")] private static partial Regex Whitespace(); + /// + /// Turns the list items of every ::: team block into person cards. + /// + /// The parsed document; changed in place. + /// The same , for chaining. + /// + /// Only lists directly in the block count; a nested list inside a card stays a + /// list. ::: team gross adds the class + /// to the block, because Markdig only + /// turns the block name into a class, not the words after it. + /// + private static MarkdownDocument BuildTeamCards(MarkdownDocument document) + { + foreach (var team in document.Descendants().Where(TeamMember.IsTeam).ToList()) + { + if (TeamMember.IsLarge(team)) + { + team.GetAttributes().AddClass(TeamMember.LargeModifier); + } + + foreach (var item in team.OfType().SelectMany(list => list.OfType())) + { + TeamMember.ToCard(item); + } + } + + return document; + } + /// /// Adds to every paragraph that holds nothing but /// one link. diff --git a/Elternbeirat.Web/Shared/TeamMember.cs b/Elternbeirat.Web/Shared/TeamMember.cs new file mode 100644 index 0000000..8bd0943 --- /dev/null +++ b/Elternbeirat.Web/Shared/TeamMember.cs @@ -0,0 +1,372 @@ +using System.Globalization; +using Markdig.Extensions.CustomContainers; +using Markdig.Renderers.Html; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +namespace Elternbeirat.Web.Shared; + +/// +/// One person in a ::: team block, read from a list item that follows the +/// convention the board page has always used. +/// +/// The person's name: the bold start of the item. +/// +/// The role after the name, without its parentheses, or +/// when the item has none (most of the "Beisitz" members). +/// +/// +/// The responsibilities from the italic second line, split at commas; empty when +/// there is no such line. +/// +/// +/// The convention, one list item per person: +/// +/// - **Maik Palm** (Vorsitzender) +/// *Schulkonferenz, Mensarat, Homepage* +/// Mitglied im Gesamtelternbeirat. +/// +/// The bold name becomes the card's heading, the role a chip, every duty a small +/// tag and any further line the note. Line breaks may be hard (two spaces) or soft: +/// editors forget the two spaces, and the card should not depend on them. +/// +/// The board stays Markdown in the page body on purpose: a dedicated +/// collection would be cleaner, but there is no way yet to bring a schema +/// change to production (Gitea #39). Reading the convention costs the editors +/// nothing they do not already write. +/// +/// +/// An item that does not start with a bold name is not an error: it becomes a +/// plain card showing its text as written (). +/// +/// +/// +public sealed record TeamMember(string Name, string? Role, IReadOnlyList Duties) +{ + /// + /// The block name editors type: ::: team. + /// + public const string ContainerName = "team"; + + /// + /// The modifier for larger cards, ::: team gross; also the class it adds + /// to the block. + /// + /// + /// A word after the block name rather than a block of its own + /// (::: team-gross): the block stays team, so a typo in the + /// modifier only costs the larger size, never the whole grid. groß is + /// accepted too; gross is the documented form because it types the same + /// on every keyboard. + /// + public const string LargeModifier = "gross"; + + /// + /// The class on every card, i.e. on every list item in a ::: team block. + /// + public const string CardClass = "team-card"; + + /// + /// The extra class on a card whose item does not follow the convention. + /// + public const string PlainCardClass = "team-card-plain"; + + /// + /// How many avatar colours there are; app.css defines + /// --color-avatar-1 to --color-avatar-5 to match. + /// + public const int ColorCount = 5; + + /// + /// FNV-1a offset basis and prime (32 bit), see . + /// + private const uint FnvOffset = 2166136261; + + private const uint FnvPrime = 16777619; + + /// + /// The letters on the avatar, see . + /// + public string Initials => InitialsOf(Name); + + /// + /// The avatar colour, from 1 to , see + /// . + /// + public int Color => ColorOf(Name); + + /// + /// Builds the initials shown on a person's avatar. + /// + /// The person's name. + /// + /// The first letter of the first and of the last word, upper case; one letter + /// for a single word; an empty string when the name holds no letter at all. + /// + /// + /// A double name counts as one word, so it adds one letter, not two: two + /// letters are what fits the circle, and they are what people expect from a + /// name like Reger-Stilgenbauer. Letters outside A–Z (Ö, + /// Ş) are kept as they are, and leading punctuation is skipped. + /// + /// + /// + /// TeamMember.InitialsOf("Jenny Reger-Stilgenbauer"); // "JR" + /// TeamMember.InitialsOf("Özlem Ünal"); // "ÖÜ" + /// TeamMember.InitialsOf("Isabell"); // "I" + /// + /// + public static string InitialsOf(string name) => + name.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries) switch + { + [] => "", + [var only] => Initial(only), + [var first, .., var last] => Initial(first) + Initial(last), + }; + + /// + /// Picks a person's avatar colour from the name. + /// + /// The person's name. + /// A number from 1 to . + /// + /// The same name always gets the same colour, on every request and after every + /// restart, so a person does not change colour between visits. That is why this + /// is a fixed hash (FNV-1a) and not , which + /// .NET randomises per process. Surrounding whitespace does not count. + /// + /// FNV-1a alone spreads short names badly over its low bits, which the + /// remainder by reads: four of the six "Beisitz" + /// members shared one colour. The MurmurHash3 finaliser () + /// stirs all bits into the low ones first. + /// + /// + public static int ColorOf(string name) => + (int)(Mix(Fnv(name.Trim())) % ColorCount) + 1; + + /// + /// Reads a person from the first paragraph of a list item. + /// + /// The paragraph to read; not changed. + /// + /// The person and the inlines that form the note (everything after the name + /// line and, if present, the duties line; possibly empty), or + /// when the paragraph does not start with a bold name. + /// + /// + /// The duties line is only recognised when it is nothing but one italic span; + /// any other second line is part of the note, so no text is lost. + /// + internal static (TeamMember Member, Inline[] Note)? Read(ParagraphBlock paragraph) => + Lines(paragraph).ToArray() is [var head, .. var rest] + && Content(head) is [EmphasisInline { DelimiterCount: 2 } name, .. var role] + && Markdown.PlainText(name) is { Length: > 0 } nameText + ? rest is [var second, .. var after] && Content(second) is [EmphasisInline { DelimiterCount: 1 } duties] + ? (new TeamMember(nameText, RoleOf(role), DutiesOf(duties)), [.. after.SelectMany(line => line)]) + : (new TeamMember(nameText, RoleOf(role), []), [.. rest.SelectMany(line => line)]) + : null; + + /// + /// Turns one list item of a ::: team block into a person card. + /// + /// The list item; changed in place. + /// + /// The first paragraph is rebuilt from new inlines: raw HTML inlines for the + /// tags and literal inlines for the text, so Markdig escapes everything the + /// editor wrote. The note keeps its original inlines, so links and emphasis in it + /// still render. Any further blocks in the item stay untouched below. + /// + internal static void ToCard(ListItemBlock item) + { + var attributes = item.GetAttributes(); + attributes.AddClass(CardClass); + + if (item.FirstOrDefault() is ParagraphBlock { Inline: not null } paragraph && Read(paragraph) is { } card) + { + foreach (var inline in card.Note) + { + inline.Remove(); + } + + paragraph.Inline = card.Member.Head() + .Concat(card.Note.Length > 0 ? [Open("span", "team-note"), .. card.Note, Close("span")] : []) + .Aggregate(new ContainerInline(), (container, inline) => container.AppendChild(inline)); + } + else + { + attributes.AddClass(PlainCardClass); + } + } + + /// + /// Whether a ::: block is a team block. + /// + /// The block to inspect. + /// for ::: team, with or without modifier. + internal static bool IsTeam(CustomContainer container) => + string.Equals(container.Info, ContainerName, StringComparison.OrdinalIgnoreCase); + + /// + /// Whether a team block asks for larger cards (::: team gross). + /// + /// The team block. + /// + /// when one of the words after the name is + /// gross or groß, in any case. + /// + internal static bool IsLarge(CustomContainer container) => + (container.Arguments ?? "") + .Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries) + .Any(word => word.Equals(LargeModifier, StringComparison.OrdinalIgnoreCase) + || word.Equals("groß", StringComparison.OrdinalIgnoreCase)); + + /// + /// The card's head: avatar, name, role chip and duty tags, as inlines. + /// + /// The inlines, in reading order; missing parts are left out. + /// + /// The avatar is hidden from screen readers: it only repeats the name next to + /// it. + /// + private Inline[] Head() => + [ + .. Initials is { Length: > 0 } initials + ? [Open("span", $"team-avatar team-avatar-{Color.ToString(CultureInfo.InvariantCulture)}", " aria-hidden=\"true\""), new LiteralInline(initials), Close("span")] + : Array.Empty(), + .. Element("team-name", Name, "strong"), + .. Role is { } role ? Element("team-role", role) : [], + .. Duties.Count > 0 + ? [Open("span", "team-duties"), .. Duties.SelectMany(Duty), Close("span")] + : Array.Empty(), + ]; + + /// + /// The 32-bit FNV-1a hash of a text's UTF-16 code units. + /// + /// The text. + /// The hash. + private static uint Fnv(string text) => + unchecked(text.Aggregate(FnvOffset, (hash, letter) => (hash ^ letter) * FnvPrime)); + + /// + /// The MurmurHash3 finaliser (fmix32): spreads every input bit over all + /// output bits. + /// + /// The hash to mix. + /// The mixed hash. + private static uint Mix(uint hash) => + unchecked(XorShift(XorShift(XorShift(hash, 16) * 0x85EBCA6B, 13) * 0xC2B2AE35, 16)); + + /// + /// Folds the high bits of a value into its low bits. + /// + /// The value. + /// How far to shift right before the XOR. + /// XOR shifted right. + private static uint XorShift(uint value, int shift) => value ^ (value >> shift); + + /// + /// One duty tag, led by a space so screen readers keep the tags apart. + /// + /// The duty. + /// The space and the tag. + private static Inline[] Duty(string duty) => + [new LiteralInline(" "), .. Element("team-duty", duty)]; + + /// + /// An element with escaped text inside. + /// + /// The value of the class attribute; written as is. + /// The text; escaped by Markdig on rendering. + /// The element name. + /// Opening tag, text and closing tag. + private static Inline[] Element(string classes, string text, string tag = "span") => + [Open(tag, classes), new LiteralInline(text), Close(tag)]; + + /// + /// An opening tag with a class, as a raw HTML inline. + /// + /// The element name. + /// The value of the class attribute; written as is. + /// + /// Further attributes, written as is after the class, with a leading space. + /// + /// The tag. + /// + /// Only ever called with constants from this class, never with editor text: + /// nothing here is escaped. + /// + private static HtmlInline Open(string tag, string classes, string attributes = "") => + new($"<{tag} class=\"{classes}\"{attributes}>"); + + /// + /// A closing tag, as a raw HTML inline. + /// + /// The element name. + /// The tag. + private static HtmlInline Close(string tag) => new($""); + + /// + /// The first letter of a word, upper case. + /// + /// One word of a name. + /// The letter, or an empty string if the word has none. + private static string Initial(string word) => + word.FirstOrDefault(char.IsLetter) is var letter and not '\0' + ? letter.ToString().ToUpper(Cultures.German) + : ""; + + /// + /// The text after the name, without separators and parentheses. + /// + /// The inlines after the bold name on the first line. + /// The role, or if nothing is left. + private static string? RoleOf(IEnumerable inlines) => + Markdown.PlainText(inlines).Trim(' ', ',', ':', ';', '-', '–') is var text + && (text.StartsWith('(') && text.EndsWith(')') ? text[1..^1].Trim() : text) is { Length: > 0 } role + ? role + : null; + + /// + /// Splits the italic duties line at its commas. + /// + /// The italic span. + /// The duties, trimmed, without empty entries. + private static string[] DutiesOf(EmphasisInline duties) => + Markdown.PlainText(duties).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + /// + /// The inlines of a line that carry content: no whitespace, no line break. + /// + /// One line from . + /// The content inlines, in order. + private static Inline[] Content(IEnumerable line) => + [.. line.Where(inline => !Markdown.IsBlank(inline))]; + + /// + /// Splits a paragraph's inlines into lines. + /// + /// The paragraph. + /// + /// The lines; each keeps the line break that ends it, so the note can be moved + /// with its breaks intact. + /// + private static IEnumerable> Lines(ParagraphBlock paragraph) + { + var line = new List(); + foreach (var inline in paragraph.Inline?.ToList() ?? []) + { + line.Add(inline); + if (inline is LineBreakInline) + { + yield return line; + line = []; + } + } + + if (line.Count > 0) + { + yield return line; + } + } +} diff --git a/Elternbeirat.Web/wwwroot/app.css b/Elternbeirat.Web/wwwroot/app.css index 6dce495..8f9582d 100644 --- a/Elternbeirat.Web/wwwroot/app.css +++ b/Elternbeirat.Web/wwwroot/app.css @@ -74,6 +74,21 @@ --color-button-light-hover: #eaf3f9; --color-on-button-light: #1f5477; + /* Initials avatars on the person cards (::: team, see TeamMember.cs). Five + soft tints, each with a deep text shade of the same hue, so a board of ten + people looks lively but calm. The colour is picked from the name, so it means + nothing -- it only helps tell the cards apart. Every pair >= 5.3:1. */ + --color-avatar-1: #dbeaf5; /* blue */ + --color-on-avatar-1: #1f5477; + --color-avatar-2: #dcefd8; /* green */ + --color-on-avatar-2: #2f6b33; + --color-avatar-3: #fbeec5; /* warm yellow */ + --color-on-avatar-3: #7a5a00; + --color-avatar-4: #f8dfe0; /* rose */ + --color-on-avatar-4: #8e2f3a; + --color-avatar-5: #e7e0f3; /* violet */ + --color-on-avatar-5: #553a8a; + /* Keyboard focus ring (:focus-visible). Its own token so it can stay strong on the light page (dark blue, 8:1 on white) and flip to a light blue on the dark one; on the gradient the hero switches it to white. */ @@ -181,6 +196,19 @@ --color-button-light-hover: #d3dee6; --color-on-button-light: #1a3d57; /* 9:1 on the eased white */ + /* Avatars: the tints turn into deep shades, the text into light ones; each + pair >= 7:1, so the circles do not glare on the dark cards. */ + --color-avatar-1: #1f3a4f; + --color-on-avatar-1: #a3d5f4; + --color-avatar-2: #24402a; + --color-on-avatar-2: #a6dc9e; + --color-avatar-3: #4a3d14; + --color-on-avatar-3: #f3d98a; + --color-avatar-4: #4d2a2f; + --color-on-avatar-4: #f4b8bf; + --color-avatar-5: #3a3150; + --color-on-avatar-5: #cdbdf0; + --color-focus: #a3d5f4; /* light blue: a dark ring would vanish here */ --color-heading: #c9ced6; /* soft white, kept a step brighter than the @@ -853,6 +881,141 @@ h1:focus { line-height: 1.15; } +/* ::: team -- one person card per list item (Markdown.cs/TeamMember.cs build the + avatar, name, role chip, duty tags and note). A grid like the kacheln, a little + wider so a long double name still fits its line. Each "## Vorsitz" etc. above + stays a normal heading; every group is a block of its own. */ +.markdown-body .team > ul { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: var(--space-3); + max-width: none; + margin: 0 0 var(--space-4); + padding: 0; + list-style: none; +} + +.markdown-body .team li + li { + margin-top: 0; +} + +/* The card: avatar in a column of its own, everything else stacked beside it. + align-content keeps a short card's text at the top when a long neighbour in the + same row stretches it. Hover only deepens the shadow -- nothing moves. */ +.markdown-body .team-card { + display: grid; + grid-template-columns: auto 1fr; + column-gap: var(--space-3); + align-content: start; + padding: var(--space-3) var(--space-4); + border-radius: var(--radius); + background: var(--color-bg); + box-shadow: var(--shadow); + transition: box-shadow var(--transition); +} + +.markdown-body .team-card:hover { + box-shadow: var(--shadow-hover); +} + +/* In a loose list (blank lines between items) Markdig wraps the card's text in a +

    ; display: contents lets its parts join the card's grid all the same. */ +.markdown-body .team-card > p:first-child { + display: contents; +} + +/* Everything but the avatar goes into the text column -- including whatever else + an editor puts into the item (a second paragraph, a sub-list). */ +.markdown-body .team-card > :not(.team-avatar), +.markdown-body .team-card > p:first-child > :not(.team-avatar) { + grid-column: 2; +} + +/* A card without the convention (no bold name) is just its text. */ +.markdown-body .team-card-plain { + display: block; +} + +.markdown-body .team-avatar { + grid-row: 1 / span 4; + display: grid; + place-items: center; + width: 3rem; + height: 3rem; + border-radius: 50%; + font-weight: var(--font-weight-heading); + letter-spacing: 0.02em; +} + +.markdown-body .team-avatar-1 { background: var(--color-avatar-1); color: var(--color-on-avatar-1); } +.markdown-body .team-avatar-2 { background: var(--color-avatar-2); color: var(--color-on-avatar-2); } +.markdown-body .team-avatar-3 { background: var(--color-avatar-3); color: var(--color-on-avatar-3); } +.markdown-body .team-avatar-4 { background: var(--color-avatar-4); color: var(--color-on-avatar-4); } +.markdown-body .team-avatar-5 { background: var(--color-avatar-5); color: var(--color-on-avatar-5); } + +.markdown-body .team-name { + color: var(--color-heading); + font-size: 1.15rem; + line-height: 1.3; +} + +/* The role as a chip under the name; justify-self keeps it as wide as its text, + not the column. */ +.markdown-body .team-role { + justify-self: start; + margin-top: var(--space-1); + padding: 0.1rem var(--space-2); + border-radius: 999px; + background: var(--color-brand-tint); + color: var(--color-brand-dark); + font-size: 0.85rem; + font-weight: 700; +} + +/* Duties as small outlined tags: quieter than the role chip, because the role + says who someone is and the duties only what they look after. */ +.markdown-body .team-duties { + display: flex; + flex-wrap: wrap; + gap: var(--space-1); + margin-top: var(--space-2); +} + +.markdown-body .team-duty { + padding: 0.05rem var(--space-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-soft); + font-size: 0.8rem; +} + +.markdown-body .team-note { + margin-top: var(--space-2); + color: var(--color-text-soft); + font-size: 0.95rem; +} + +/* ::: team gross -- the chair: wider cards, a bigger avatar and name, and a green + edge so the first group reads as the lead of the page. */ +.markdown-body .team.gross > ul { + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); +} + +.markdown-body .team.gross .team-card { + padding: var(--space-4); + border-top: 4px solid var(--color-accent); +} + +.markdown-body .team.gross .team-avatar { + width: 4rem; + height: 4rem; + font-size: 1.3rem; +} + +.markdown-body .team.gross .team-name { + font-size: 1.35rem; +} + /* 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 { diff --git a/docs/redaktion.md b/docs/redaktion.md index 7009429..550518a 100644 --- a/docs/redaktion.md +++ b/docs/redaktion.md @@ -309,6 +309,46 @@ Die Beitrittserklärung füllen Sie online beim Förderverein aus. ::: ``` +**Team** — eine Personenliste wird zu Karten mit Initialen-Kreis. Jeder +Listenpunkt ist eine Person: + +```markdown +## Vorsitz + +::: team gross +- **Maik Palm** (Vorsitzender) + *Schulkonferenz, Mensarat, Homepage* + Mitglied im Gesamtelternbeirat und im Schulbeirat der Stadt Mannheim. +- **Gentiana Erol** (1. Stellvertreterin) + *Schulkonferenz, Kommunikation Hausmeister, Mensarat* +::: + +## Beisitz + +::: team +- **Isabell Vesper** +- **Susanne Naumburg** + *Stellvertretende Schulkonferenz* +::: +``` + +- Der **fette Name** am Anfang wird die Überschrift der Karte, die **Rolle in + Klammern** dahinter ein farbiges Etikett. Beides außer dem Namen ist optional. +- Die **kursive** zweite Zeile sind die Zuständigkeiten, durch Komma getrennt; + jede wird ein kleines Schildchen. +- Jede **weitere Zeile** erscheint als kleiner Text darunter (Links gehen). +- Zeilenumbruch innerhalb einer Person: zwei Leerzeichen am Zeilenende, die + Folgezeilen mit zwei Leerzeichen einrücken (wie oben). +- Initialen und Farbe des Kreises entstehen **automatisch aus dem Namen**; es gibt + bewusst keine Fotos (siehe `docs/recht.md`). +- `::: team gross` macht die Karten größer und hebt sie hervor, gedacht für den + Vorsitz. Ein vertipptes Wort nach `team` schadet nicht: Die Karten erscheinen + dann in normaler Größe. +- Die Überschriften (`## Vorsitz`, `## Beisitz` …) stehen **außerhalb** der + Blöcke; jede Gruppe ist ein eigener `::: team`-Block. +- Beginnt ein Listenpunkt nicht mit einem fetten Namen, wird er eine schlichte + Karte mit genau dem Text, der dasteht. + **Hinweis** — eine dezente Info-Box mit Info-Symbol: ```markdown diff --git a/pb/pb_migrations/1727200000_dev_seed.js b/pb/pb_migrations/1727200000_dev_seed.js index a4a9847..4e2607f 100644 --- a/pb/pb_migrations/1727200000_dev_seed.js +++ b/pb/pb_migrations/1727200000_dev_seed.js @@ -21,7 +21,7 @@ migrate((app) => { const schema = JSON.parse(toString($os.readFile("/pb_migrations/collections_schema.json"))) const records = { - pages: [{"body":"Der Elternbeirat der IGMH vertritt die Interessen aller Eltern gegenüber der Schulleitung und dem Kollegium. Wir treffen uns regelmäßig, bringen Anliegen aus der Elternschaft ein und begleiten das Schulleben aktiv.\n\nDer Vorstand wurde am 13. November 2025 gewählt.\n\n## Vorsitz\n\n- **Maik Palm** (Vorsitzender) \n *Schulkonferenz, Mensarat, Homepage* \n Mitglied im Gesamtelternbeirat und im Schulbeirat der Stadt Mannheim.\n- **Gentiana Erol** (1. Stellvertreterin) \n *Schulkonferenz, Kommunikation Hausmeister, Mensarat*\n- **Raul Chira** (2. Stellvertreter) \n *Mensarat*\n\n## Schriftführung\n\n- **Marion Leininger** \n *Kasse, Schriftführung, Kontakt Förderverein*\n\n## Beisitz\n\n- **Jenny Reger-Stilgenbauer** \n *Schulkonferenz, Schaukasten*\n- **Mariella Ianello** \n *Mensarat, Kontakt Förderverein*\n- **Isabell Vesper**\n- **Susanne Naumburg** \n *Stellvertretende Schulkonferenz*\n- **Rebecca Goldmann**\n- **Michael Weber** \n *Stellvertretende Schulkonferenz*\n\n## So erreichen Sie uns\n\nAm schnellsten per Mail über unsere [Kontaktseite](/contact). Zu unseren öffentlichen Sitzungen sind alle Eltern herzlich eingeladen. Die Termine finden Sie unter [Termine](/events).","embed":[],"location":"header","order":2,"public":true,"slug":"board","title":"Vorstandsteam"},{"body":"Der Förderverein **„Freunde der IGMH e.V.\"** unterstützt die Schule seit 1975 bei Anschaffungen und Projekten, die aus dem regulären Schulbudget nicht finanziert werden können.\n\n::: kennzahlen\n- **seit 1975** im Einsatz für die IGMH\n- **über 500** Mitglieder\n- **ab 10 €** Jahresbeitrag\n:::\n\n::: hinweis\nDer Förderverein ist ein eigenständiger, gemeinnützig anerkannter Verein und nicht Teil des Elternbeirats.\n:::\n\n## Was der Verein fördert\n\n::: kacheln\n- **Pädagogik** Bessere Rahmenbedingungen für die Arbeit an der Schule\n- **Projekte** Musische, sportliche und kulturelle Vorhaben\n- **Ausstattung** Anschaffungen, die allen Schülerinnen und Schülern zugutekommen\n- **Chancengerechtigkeit** Damit kein Kind aus finanziellen Gründen zurückbleibt\n:::\n\n## Was bereits gefördert wurde\n\n- Rennräder, Bälle, Trikots sowie Sport- und Spielgeräte für AGs\n- Zuschüsse für Chorwochenenden und Kanutage\n- Unterstützung der Theater-AG\n- Veranstaltungstechnik und Musikinstrumente, zum Beispiel DJ-Mischpult und Klavier\n- Vor- und Mitfinanzierung der Schülerhandbücher\n- Kletterwand, Außenmöbel in den Innenhöfen, Sitzbänke im Schulhaus und Wasserspender im Mensabereich\n- Laptops und Tablets für den Unterricht\n\n## Mitglied werden\n\nMitglied werden kann, wer die Arbeit der Schule unterstützen möchte. Eine Mitgliedschaft ist bereits ab einem Jahresbeitrag von **10 €** möglich.\n\n::: aufruf\nDie Beitrittserklärung füllen Sie online beim Förderverein aus.\n\n[Zur Beitrittserklärung](https://easyverein.com/public/FDI/applicationform/21612)\n:::\n\n## Spenden\n\nSpenden sind jederzeit willkommen und werden auf Wunsch mit einer Zuwendungsbestätigung quittiert. Der Verein ist als gemeinnützig anerkannt. Wie Sie spenden können, erfahren Sie beim Förderverein.\n\n## Kontakt zum Förderverein\n\n**Freunde der IGMH e.V.** \nHerzogenriedstraße 50 \n68169 Mannheim \n(Fach 165)\n\nE-Mail: [foerderverein@igmh.de](mailto:foerderverein@igmh.de) \nWebsite: [foerderverein.igmh.de](https://foerderverein.igmh.de)","embed":[],"location":"header","order":3,"public":true,"slug":"patrons","title":"Förderverein"},{"body":"Auf dieser Seite haben wir die Fragen zusammengetragen, die uns am häufigsten erreichen.\n\nIhre Frage ist nicht dabei? Schreiben Sie uns über die [Kontaktseite](/contact) – wir helfen gern weiter.","embed":[],"location":"header","order":6,"public":true,"slug":"faqs","title":"FAQs"},{"body":"Formulare und Dokumente zum Herunterladen:\n\n- [Aufnahmeantrag Förderverein](/dokumente/aufnahmeantrag-foerderverein.pdf)\n- [Einzugsermächtigung](/dokumente/einzugsermaechtigung.pdf)\n- [Erstattungsformular Klassenkasse](/dokumente/erstattungsformular-klassenkasse.pdf)\n\n::: hinweis\nFehlt ein Dokument? Melden Sie sich über die [Kontaktseite](/contact).\n:::","embed":[],"location":"header","order":7,"public":true,"slug":"downloads","title":"Downloads"},{"body":"Haben Sie eine Frage, eine Anregung oder möchten Sie sich einbringen? Der Elternbeirat der IGMH freut sich über jede Nachricht.\n\n[kontakt@elternbeirat-igmh.info](mailto:kontakt@elternbeirat-igmh.info)\n\nIn der Regel melden wir uns innerhalb weniger Tage zurück.\n\n::: hinweis\nBei dringenden schulischen Angelegenheiten wenden Sie sich bitte direkt an das Sekretariat der IGMH.\n:::","embed":[],"location":"footer","order":1,"public":true,"slug":"contact","title":"Kontakt"},{"body":"# Impressum\n\nAngaben gemäß § 5 DDG\n\n**Elternbeirat der IGMH**\nVertreten durch den Vorstand\n\nKontakt: siehe [Kontaktseite](/contact)\n\nVerantwortlich für den Inhalt nach § 18 Abs. 2 MStV: der Vorstand des Elternbeirats.","embed":[],"location":"footer","order":2,"public":true,"slug":"imprint","title":"Impressum"},{"body":"# Datenschutz\n\nDer Schutz Ihrer Daten ist uns wichtig. Diese Website kommt ohne externe Dienste aus: keine Analysewerkzeuge, keine eingebundenen Karten oder Videos, keine Schriften von fremden Servern.\n\n## Server-Logs\n\nBeim Aufruf werden technisch notwendige Zugriffsdaten (IP-Adresse, Zeitpunkt, aufgerufene Seite) kurzzeitig verarbeitet, um den Betrieb sicherzustellen. Eine Zusammenführung mit anderen Daten findet nicht statt.\n\n## Ihre Rechte\n\nFragen zum Datenschutz richten Sie bitte an die [Kontaktseite](/contact).","embed":[],"location":"footer","order":3,"public":true,"slug":"privacy","title":"Datenschutz"},{"body":"# Willkommen beim Elternbeirat der IGMH\n\nHier finden Sie aktuelle Informationen, Antworten auf häufige Fragen und Unterlagen rund um die Elternarbeit an der IGMH.\n\n[Kommende Termine](/events)\n[Kontakt aufnehmen](/contact)","embed":["posts","events"],"location":"header","order":1,"public":true,"slug":"home","title":"Start"},{"body":"Was tut sich am Elternbeirat und an der IGMH? Hier finden Sie Rückblicke auf Veranstaltungen, Ankündigungen und Berichte aus den Sitzungen des Elternbeirats.","embed":[],"location":"header","order":4,"public":true,"slug":"posts","title":"Beiträge"},{"body":"Wann kommt der Elternbeirat zusammen, und was steht rund um die IGMH an? Hier finden Sie die Termine auf einen Blick.","embed":[],"location":"header","order":5,"public":true,"slug":"events","title":"Termine"}] + pages: [{"body":"Der Elternbeirat der IGMH vertritt die Interessen aller Eltern gegenüber der Schulleitung und dem Kollegium. Wir treffen uns regelmäßig, bringen Anliegen aus der Elternschaft ein und begleiten das Schulleben aktiv.\n\nDer Vorstand wurde am 13. November 2025 gewählt.\n\n## Vorsitz\n\n::: team gross\n- **Maik Palm** (Vorsitzender) \n *Schulkonferenz, Mensarat, Homepage* \n Mitglied im Gesamtelternbeirat und im Schulbeirat der Stadt Mannheim.\n- **Gentiana Erol** (1. Stellvertreterin) \n *Schulkonferenz, Kommunikation Hausmeister, Mensarat*\n- **Raul Chira** (2. Stellvertreter) \n *Mensarat*\n:::\n\n## Schriftführung\n\n::: team\n- **Marion Leininger** \n *Kasse, Schriftführung, Kontakt Förderverein*\n:::\n\n## Beisitz\n\n::: team\n- **Jenny Reger-Stilgenbauer** \n *Schulkonferenz, Schaukasten*\n- **Mariella Ianello** \n *Mensarat, Kontakt Förderverein*\n- **Isabell Vesper**\n- **Susanne Naumburg** \n *Stellvertretende Schulkonferenz*\n- **Rebecca Goldmann**\n- **Michael Weber** \n *Stellvertretende Schulkonferenz*\n:::\n\n## So erreichen Sie uns\n\nAm schnellsten per Mail über unsere [Kontaktseite](/contact). Zu unseren öffentlichen Sitzungen sind alle Eltern herzlich eingeladen. Die Termine finden Sie unter [Termine](/events).","embed":[],"location":"header","order":2,"public":true,"slug":"board","title":"Vorstandsteam"},{"body":"Der Förderverein **„Freunde der IGMH e.V.\"** unterstützt die Schule seit 1975 bei Anschaffungen und Projekten, die aus dem regulären Schulbudget nicht finanziert werden können.\n\n::: kennzahlen\n- **seit 1975** im Einsatz für die IGMH\n- **über 500** Mitglieder\n- **ab 10 €** Jahresbeitrag\n:::\n\n::: hinweis\nDer Förderverein ist ein eigenständiger, gemeinnützig anerkannter Verein und nicht Teil des Elternbeirats.\n:::\n\n## Was der Verein fördert\n\n::: kacheln\n- **Pädagogik** Bessere Rahmenbedingungen für die Arbeit an der Schule\n- **Projekte** Musische, sportliche und kulturelle Vorhaben\n- **Ausstattung** Anschaffungen, die allen Schülerinnen und Schülern zugutekommen\n- **Chancengerechtigkeit** Damit kein Kind aus finanziellen Gründen zurückbleibt\n:::\n\n## Was bereits gefördert wurde\n\n- Rennräder, Bälle, Trikots sowie Sport- und Spielgeräte für AGs\n- Zuschüsse für Chorwochenenden und Kanutage\n- Unterstützung der Theater-AG\n- Veranstaltungstechnik und Musikinstrumente, zum Beispiel DJ-Mischpult und Klavier\n- Vor- und Mitfinanzierung der Schülerhandbücher\n- Kletterwand, Außenmöbel in den Innenhöfen, Sitzbänke im Schulhaus und Wasserspender im Mensabereich\n- Laptops und Tablets für den Unterricht\n\n## Mitglied werden\n\nMitglied werden kann, wer die Arbeit der Schule unterstützen möchte. Eine Mitgliedschaft ist bereits ab einem Jahresbeitrag von **10 €** möglich.\n\n::: aufruf\nDie Beitrittserklärung füllen Sie online beim Förderverein aus.\n\n[Zur Beitrittserklärung](https://easyverein.com/public/FDI/applicationform/21612)\n:::\n\n## Spenden\n\nSpenden sind jederzeit willkommen und werden auf Wunsch mit einer Zuwendungsbestätigung quittiert. Der Verein ist als gemeinnützig anerkannt. Wie Sie spenden können, erfahren Sie beim Förderverein.\n\n## Kontakt zum Förderverein\n\n**Freunde der IGMH e.V.** \nHerzogenriedstraße 50 \n68169 Mannheim \n(Fach 165)\n\nE-Mail: [foerderverein@igmh.de](mailto:foerderverein@igmh.de) \nWebsite: [foerderverein.igmh.de](https://foerderverein.igmh.de)","embed":[],"location":"header","order":3,"public":true,"slug":"patrons","title":"Förderverein"},{"body":"Auf dieser Seite haben wir die Fragen zusammengetragen, die uns am häufigsten erreichen.\n\nIhre Frage ist nicht dabei? Schreiben Sie uns über die [Kontaktseite](/contact) – wir helfen gern weiter.","embed":[],"location":"header","order":6,"public":true,"slug":"faqs","title":"FAQs"},{"body":"Formulare und Dokumente zum Herunterladen:\n\n- [Aufnahmeantrag Förderverein](/dokumente/aufnahmeantrag-foerderverein.pdf)\n- [Einzugsermächtigung](/dokumente/einzugsermaechtigung.pdf)\n- [Erstattungsformular Klassenkasse](/dokumente/erstattungsformular-klassenkasse.pdf)\n\n::: hinweis\nFehlt ein Dokument? Melden Sie sich über die [Kontaktseite](/contact).\n:::","embed":[],"location":"header","order":7,"public":true,"slug":"downloads","title":"Downloads"},{"body":"Haben Sie eine Frage, eine Anregung oder möchten Sie sich einbringen? Der Elternbeirat der IGMH freut sich über jede Nachricht.\n\n[kontakt@elternbeirat-igmh.info](mailto:kontakt@elternbeirat-igmh.info)\n\nIn der Regel melden wir uns innerhalb weniger Tage zurück.\n\n::: hinweis\nBei dringenden schulischen Angelegenheiten wenden Sie sich bitte direkt an das Sekretariat der IGMH.\n:::","embed":[],"location":"footer","order":1,"public":true,"slug":"contact","title":"Kontakt"},{"body":"# Impressum\n\nAngaben gemäß § 5 DDG\n\n**Elternbeirat der IGMH**\nVertreten durch den Vorstand\n\nKontakt: siehe [Kontaktseite](/contact)\n\nVerantwortlich für den Inhalt nach § 18 Abs. 2 MStV: der Vorstand des Elternbeirats.","embed":[],"location":"footer","order":2,"public":true,"slug":"imprint","title":"Impressum"},{"body":"# Datenschutz\n\nDer Schutz Ihrer Daten ist uns wichtig. Diese Website kommt ohne externe Dienste aus: keine Analysewerkzeuge, keine eingebundenen Karten oder Videos, keine Schriften von fremden Servern.\n\n## Server-Logs\n\nBeim Aufruf werden technisch notwendige Zugriffsdaten (IP-Adresse, Zeitpunkt, aufgerufene Seite) kurzzeitig verarbeitet, um den Betrieb sicherzustellen. Eine Zusammenführung mit anderen Daten findet nicht statt.\n\n## Ihre Rechte\n\nFragen zum Datenschutz richten Sie bitte an die [Kontaktseite](/contact).","embed":[],"location":"footer","order":3,"public":true,"slug":"privacy","title":"Datenschutz"},{"body":"# Willkommen beim Elternbeirat der IGMH\n\nHier finden Sie aktuelle Informationen, Antworten auf häufige Fragen und Unterlagen rund um die Elternarbeit an der IGMH.\n\n[Kommende Termine](/events)\n[Kontakt aufnehmen](/contact)","embed":["posts","events"],"location":"header","order":1,"public":true,"slug":"home","title":"Start"},{"body":"Was tut sich am Elternbeirat und an der IGMH? Hier finden Sie Rückblicke auf Veranstaltungen, Ankündigungen und Berichte aus den Sitzungen des Elternbeirats.","embed":[],"location":"header","order":4,"public":true,"slug":"posts","title":"Beiträge"},{"body":"Wann kommt der Elternbeirat zusammen, und was steht rund um die IGMH an? Hier finden Sie die Termine auf einen Blick.","embed":[],"location":"header","order":5,"public":true,"slug":"events","title":"Termine"}] , 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"}] , -- 2.54.0 From 602f2582570a559d7ed51e1d31b4b60d8883e37a Mon Sep 17 00:00:00 2001 From: tleininger Date: Thu, 1 Oct 2026 16:04:12 +0200 Subject: [PATCH 2/3] Unify team grid, limit avatars to brand colours, keep duty tags on one line --- Elternbeirat.Web.Tests/TeamMemberTests.cs | 23 ++++-- Elternbeirat.Web/Shared/TeamMember.cs | 30 +++++--- Elternbeirat.Web/wwwroot/app.css | 85 +++++++++-------------- docs/redaktion.md | 6 +- 4 files changed, 72 insertions(+), 72 deletions(-) diff --git a/Elternbeirat.Web.Tests/TeamMemberTests.cs b/Elternbeirat.Web.Tests/TeamMemberTests.cs index 231d82c..2ba42f4 100644 --- a/Elternbeirat.Web.Tests/TeamMemberTests.cs +++ b/Elternbeirat.Web.Tests/TeamMemberTests.cs @@ -28,13 +28,28 @@ public class TeamMemberTests // Pinned values: the colour must not change between restarts or releases, so a // change to the hash shows up here instead of recolouring the board. TeamMember.ColorOf("Maik Palm").ShouldBe(TeamMember.ColorOf(" Maik Palm ")); - TeamMember.ColorOf("Maik Palm").ShouldBe(4); - TeamMember.ColorOf("Gentiana Erol").ShouldBe(5); + TeamMember.ColorOf("Maik Palm").ShouldBe(2); + TeamMember.ColorOf("Gentiana Erol").ShouldBe(2); + TeamMember.ColorOf("Raul Chira").ShouldBe(1); new[] { "Maik Palm", "Gentiana Erol", "Raul Chira", "Marion Leininger", "Isabell Vesper", "" } .ShouldAllBe(name => TeamMember.ColorOf(name) >= 1 && TeamMember.ColorOf(name) <= TeamMember.ColorCount); } + [Fact] + public void Board_uses_both_colours_about_evenly() + { + // Two colours only make sense if both show up; the real board (ten names) + // must not land mostly on one. + string[] board = + [ + "Maik Palm", "Gentiana Erol", "Raul Chira", "Marion Leininger", "Jenny Reger-Stilgenbauer", + "Mariella Ianello", "Isabell Vesper", "Susanne Naumburg", "Rebecca Goldmann", "Michael Weber", + ]; + + board.Count(name => TeamMember.ColorOf(name) == 1).ShouldBeInRange(4, 6); + } + [Fact] public void Item_with_role_duties_and_note_is_read_in_full() { @@ -101,7 +116,7 @@ public class TeamMemberTests html.ShouldContain("

    "); html.ShouldContain($"
  • "); - html.ShouldContain("MP"); + html.ShouldContain("MP"); html.ShouldContain("Maik Palm"); html.ShouldContain("Vorsitzender"); html.ShouldContain("Schulkonferenz"); @@ -138,7 +153,7 @@ public class TeamMemberTests [Fact] public void Mistyped_modifier_keeps_the_grid() { - // A typo in the modifier costs the larger size, not the cards. + // A typo in the modifier costs the highlight, not the cards. var html = Markdown.ToHtml("::: team grss\n- **Maik Palm**\n:::"); html.ShouldContain("
    "); diff --git a/Elternbeirat.Web/Shared/TeamMember.cs b/Elternbeirat.Web/Shared/TeamMember.cs index 8bd0943..c90875f 100644 --- a/Elternbeirat.Web/Shared/TeamMember.cs +++ b/Elternbeirat.Web/Shared/TeamMember.cs @@ -49,13 +49,14 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList public const string ContainerName = "team"; /// - /// The modifier for larger cards, ::: team gross; also the class it adds - /// to the block. + /// The modifier for the chair, ::: team gross; also the class it adds to + /// the block. The cards keep the shared raster; only the role chip is + /// highlighted. /// /// /// A word after the block name rather than a block of its own /// (::: team-gross): the block stays team, so a typo in the - /// modifier only costs the larger size, never the whole grid. groß is + /// modifier only costs the highlight, never the whole grid. groß is /// accepted too; gross is the documented form because it types the same /// on every keyboard. /// @@ -73,9 +74,14 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList /// /// How many avatar colours there are; app.css defines - /// --color-avatar-1 to --color-avatar-5 to match. + /// --color-avatar-1 (brand blue) and --color-avatar-2 (brand + /// green) to match. /// - public const int ColorCount = 5; + /// + /// Only the two brand colours: more hues made the board look busy and pulled it + /// away from the IGMH palette. + /// + public const int ColorCount = 2; /// /// FNV-1a offset basis and prime (32 bit), see . @@ -135,14 +141,16 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList /// is a fixed hash (FNV-1a) and not , which /// .NET randomises per process. Surrounding whitespace does not count. /// - /// FNV-1a alone spreads short names badly over its low bits, which the - /// remainder by reads: four of the six "Beisitz" - /// members shared one colour. The MurmurHash3 finaliser () - /// stirs all bits into the low ones first. + /// FNV-1a alone spreads short names badly, so the MurmurHash3 finaliser + /// () stirs all bits first. The colour is then taken from + /// the high bits (multiply by , keep the top + /// 32 bits) rather than a remainder, which would read only the low bits: + /// with two colours that put eight of the ten board members on the same + /// one. /// /// public static int ColorOf(string name) => - (int)(Mix(Fnv(name.Trim())) % ColorCount) + 1; + (int)(((ulong)Mix(Fnv(name.Trim())) * ColorCount) >> 32) + 1; /// /// Reads a person from the first paragraph of a list item. @@ -207,7 +215,7 @@ public sealed record TeamMember(string Name, string? Role, IReadOnlyList string.Equals(container.Info, ContainerName, StringComparison.OrdinalIgnoreCase); /// - /// Whether a team block asks for larger cards (::: team gross). + /// Whether a team block asks for the chair highlight (::: team gross). /// /// The team block. /// diff --git a/Elternbeirat.Web/wwwroot/app.css b/Elternbeirat.Web/wwwroot/app.css index 8f9582d..be1b83f 100644 --- a/Elternbeirat.Web/wwwroot/app.css +++ b/Elternbeirat.Web/wwwroot/app.css @@ -74,20 +74,16 @@ --color-button-light-hover: #eaf3f9; --color-on-button-light: #1f5477; - /* Initials avatars on the person cards (::: team, see TeamMember.cs). Five - soft tints, each with a deep text shade of the same hue, so a board of ten - people looks lively but calm. The colour is picked from the name, so it means - nothing -- it only helps tell the cards apart. Every pair >= 5.3:1. */ - --color-avatar-1: #dbeaf5; /* blue */ - --color-on-avatar-1: #1f5477; - --color-avatar-2: #dcefd8; /* green */ + /* Initials avatars on the person cards (::: team, see TeamMember.cs): only the + two brand colours, blue and green tint, so the board stays in the IGMH palette. + The colour is picked from the name, so it means nothing -- it only helps tell + the cards apart. The tints follow their tokens into dark mode; the green text + needs its own shade, because --color-accent-dark is too light for small text + on the tint (3.6:1). Blue 7.2:1, green 5.9:1. */ + --color-avatar-1: var(--color-brand-tint); + --color-on-avatar-1: var(--color-brand-dark); + --color-avatar-2: var(--color-accent-tint); --color-on-avatar-2: #2f6b33; - --color-avatar-3: #fbeec5; /* warm yellow */ - --color-on-avatar-3: #7a5a00; - --color-avatar-4: #f8dfe0; /* rose */ - --color-on-avatar-4: #8e2f3a; - --color-avatar-5: #e7e0f3; /* violet */ - --color-on-avatar-5: #553a8a; /* Keyboard focus ring (:focus-visible). Its own token so it can stay strong on the light page (dark blue, 8:1 on white) and flip to a light blue on the dark @@ -196,18 +192,9 @@ --color-button-light-hover: #d3dee6; --color-on-button-light: #1a3d57; /* 9:1 on the eased white */ - /* Avatars: the tints turn into deep shades, the text into light ones; each - pair >= 7:1, so the circles do not glare on the dark cards. */ - --color-avatar-1: #1f3a4f; - --color-on-avatar-1: #a3d5f4; - --color-avatar-2: #24402a; - --color-on-avatar-2: #a6dc9e; - --color-avatar-3: #4a3d14; - --color-on-avatar-3: #f3d98a; - --color-avatar-4: #4d2a2f; - --color-on-avatar-4: #f4b8bf; - --color-avatar-5: #3a3150; - --color-on-avatar-5: #cdbdf0; + /* Avatar green text: the brightened accent, 6.7:1 on the dark green tint + (the blue pair follows its tokens, 8.6:1). */ + --color-on-avatar-2: var(--color-accent); --color-focus: #a3d5f4; /* light blue: a dark ring would vanish here */ @@ -882,12 +869,16 @@ h1:focus { } /* ::: team -- one person card per list item (Markdown.cs/TeamMember.cs build the - avatar, name, role chip, duty tags and note). A grid like the kacheln, a little - wider so a long double name still fits its line. Each "## Vorsitz" etc. above - stays a normal heading; every group is a block of its own. */ + avatar, name, role chip, duty tags and note). Each "## Vorsitz" etc. above stays + a normal heading; every group is a block of its own. One raster for all of them, + so the cards line up across groups: a group of one person gets the same card + width as a group of six, not a stretched one. 17rem fits a long double name. + align-items: start lets every card be as tall as its content -- a card without + duties is not stretched to its neighbour's height. */ .markdown-body .team > ul { display: grid; - grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); + align-items: start; gap: var(--space-3); max-width: none; margin: 0 0 var(--space-4); @@ -900,8 +891,7 @@ h1:focus { } /* The card: avatar in a column of its own, everything else stacked beside it. - align-content keeps a short card's text at the top when a long neighbour in the - same row stretches it. Hover only deepens the shadow -- nothing moves. */ + Hover only deepens the shadow -- nothing moves. */ .markdown-body .team-card { display: grid; grid-template-columns: auto 1fr; @@ -949,9 +939,6 @@ h1:focus { .markdown-body .team-avatar-1 { background: var(--color-avatar-1); color: var(--color-on-avatar-1); } .markdown-body .team-avatar-2 { background: var(--color-avatar-2); color: var(--color-on-avatar-2); } -.markdown-body .team-avatar-3 { background: var(--color-avatar-3); color: var(--color-on-avatar-3); } -.markdown-body .team-avatar-4 { background: var(--color-avatar-4); color: var(--color-on-avatar-4); } -.markdown-body .team-avatar-5 { background: var(--color-avatar-5); color: var(--color-on-avatar-5); } .markdown-body .team-name { color: var(--color-heading); @@ -973,7 +960,9 @@ h1:focus { } /* Duties as small outlined tags: quieter than the role chip, because the role - says who someone is and the duties only what they look after. */ + says who someone is and the duties only what they look after. A tag never breaks + inside ("Stellvertretende Schulkonferenz" stays one piece); the row wraps between + tags instead. */ .markdown-body .team-duties { display: flex; flex-wrap: wrap; @@ -982,6 +971,7 @@ h1:focus { } .markdown-body .team-duty { + white-space: nowrap; padding: 0.05rem var(--space-2); border: 1px solid var(--color-border); border-radius: var(--radius-sm); @@ -995,25 +985,12 @@ h1:focus { font-size: 0.95rem; } -/* ::: team gross -- the chair: wider cards, a bigger avatar and name, and a green - edge so the first group reads as the lead of the page. */ -.markdown-body .team.gross > ul { - grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); -} - -.markdown-body .team.gross .team-card { - padding: var(--space-4); - border-top: 4px solid var(--color-accent); -} - -.markdown-body .team.gross .team-avatar { - width: 4rem; - height: 4rem; - font-size: 1.3rem; -} - -.markdown-body .team.gross .team-name { - font-size: 1.35rem; +/* ::: team gross -- the chair. Same raster and card as every other group, so the + page keeps one rhythm; only the role chip stands out, filled in the button blue + instead of the pale tint (white on it 5.4:1, in dark mode too). */ +.markdown-body .team.gross .team-role { + background: var(--color-button); + color: var(--color-on-button); } /* Tables (pipe tables): not in the content yet, styled ahead of time. As a block diff --git a/docs/redaktion.md b/docs/redaktion.md index 550518a..80d427d 100644 --- a/docs/redaktion.md +++ b/docs/redaktion.md @@ -341,9 +341,9 @@ Listenpunkt ist eine Person: Folgezeilen mit zwei Leerzeichen einrücken (wie oben). - Initialen und Farbe des Kreises entstehen **automatisch aus dem Namen**; es gibt bewusst keine Fotos (siehe `docs/recht.md`). -- `::: team gross` macht die Karten größer und hebt sie hervor, gedacht für den - Vorsitz. Ein vertipptes Wort nach `team` schadet nicht: Die Karten erscheinen - dann in normaler Größe. +- `::: team gross` ist für den Vorsitz gedacht: Die Karten bleiben gleich groß, + nur das Rollen-Etikett wird kräftig blau hervorgehoben. Ein vertipptes Wort + nach `team` schadet nicht: Die Etiketten erscheinen dann einfach normal. - Die Überschriften (`## Vorsitz`, `## Beisitz` …) stehen **außerhalb** der Blöcke; jede Gruppe ist ein eigener `::: team`-Block. - Beginnt ein Listenpunkt nicht mit einem fetten Namen, wird er eine schlichte -- 2.54.0 From 0257b2e30b5b13c45c7b23df13598aae513792f9 Mon Sep 17 00:00:00 2001 From: tleininger Date: Thu, 1 Oct 2026 16:11:43 +0200 Subject: [PATCH 3/3] Stretch team cards to equal row height and enlarge duty tags --- Elternbeirat.Web/wwwroot/app.css | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Elternbeirat.Web/wwwroot/app.css b/Elternbeirat.Web/wwwroot/app.css index be1b83f..eb658f2 100644 --- a/Elternbeirat.Web/wwwroot/app.css +++ b/Elternbeirat.Web/wwwroot/app.css @@ -873,12 +873,11 @@ h1:focus { a normal heading; every group is a block of its own. One raster for all of them, so the cards line up across groups: a group of one person gets the same card width as a group of six, not a stretched one. 17rem fits a long double name. - align-items: start lets every card be as tall as its content -- a card without - duties is not stretched to its neighbour's height. */ + The cards of a row share one height (the grid's default stretch), so a row reads + as a calm band; the card itself keeps its content at the top (align-content). */ .markdown-body .team > ul { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); - align-items: start; gap: var(--space-3); max-width: none; margin: 0 0 var(--space-4); @@ -976,7 +975,7 @@ h1:focus { border: 1px solid var(--color-border); border-radius: var(--radius-sm); color: var(--color-text-soft); - font-size: 0.8rem; + font-size: 0.85rem; } .markdown-body .team-note { -- 2.54.0