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