feat: Scorecard Portrait redesign — Matrix mit Achsen-Labels, flex-grow gegen Slack

User-Feedback nach Browser-Inspektion: 'Bei mir sieht das immer noch
nicht so aus' — und tatsächlich, das vorherige Layout hatte zwei
sichtbare Probleme:

1. justify-content: space-between auf portrait-body verteilte den
   Slack-Raum nicht symmetrisch, sondern haeufte ihn unten zwischen
   Matrix-Block und Begruendung an. Folge: ~270 px Luecke zwischen
   diesen Sektionen.

2. Die Matrix war 'stilisiert' nur in Form (5×5 Farb-Grid) — aber
   ohne Achsen-Beschriftungen muessten Buerger:innen wissen was A1,
   B2 etc. bedeuten. Kommt nicht an.

Redesign:
- Layout-Strategie: portrait-matrix-block bekommt flex-grow:1 und
  absorbiert allen verbleibenden vertikalen Platz; Matrix bleibt
  zentriert. Andere Sektionen sitzen in natuerlicher Hoehe oben/
  unten. Kein space-between.

- Matrix stilisiert mit Achsen:
  · Spalten-Header: Wuerde / Solidaritaet / Nachhaltigkeit /
    Gerechtigkeit / Transparenz (Brand-Color, Mono-Caps)
  · Zeilen-Header: A·Lieferant:innen, B·Finanzen, C·Verwaltung,
    D·Buerger:innen, E·Gesellschaft & Natur
  · Cells in 88×88 Quadraten, gap 4 px
  · Legende horizontal unter der Matrix statt seitlich

- Begruendung: line-clamp 5, sitzt am Boden, mit Trennlinie und Sublabel.

- Cache-Control: no-store auf /v2/scorecard, damit Browser nach
  Layout-Aenderungen nicht die alte HTML-Variante zeigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 13:32:44 +02:00
parent 099fbd0fb0
commit d470e03caf
2 changed files with 90 additions and 66 deletions

View File

@ -3515,7 +3515,7 @@ async def scorecard_template(
} }
width, height = dimensions.get(format, dimensions["og"]) width, height = dimensions.get(format, dimensions["og"])
return templates.TemplateResponse("v2/screens/scorecard.html", { response = templates.TemplateResponse("v2/screens/scorecard.html", {
"request": request, "request": request,
"assessment": assessment, "assessment": assessment,
"bundesland": bundesland, "bundesland": bundesland,
@ -3526,6 +3526,10 @@ async def scorecard_template(
"width": width, "width": width,
"height": height, "height": height,
}) })
# No-cache fuer die Live-Preview, sonst zeigt der Browser nach Layout-
# Aenderungen die alte HTML-Variante an.
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
return response
async def _render_scorecard_pdf( async def _render_scorecard_pdf(

View File

@ -154,29 +154,17 @@
/* ── portrait Layout — ausbalancierte vertikale Komposition. /* ── portrait Layout — ausbalancierte vertikale Komposition.
Sektions-Hierarchie (top → bottom): Statt justify-content: space-between (verteilt Slack-Raum unkontrol-
1. Header-Bar (kicker + meta) ~50 px liert) absorbiert .portrait-matrix-block per flex-grow:1 die ueber-
2. Title (kompakt, 3 Zeilen max) ~150 px schuessige Hoehe. Matrix bleibt visuell zentriert, andere Sektionen
3. Fraktionen-Pills ~40 px in natuerlicher Groesse oben/unten. */
4. Score-Hero (full-width Farbband) ~280 px
5. Matrix-Block (Grid + Legende) ~480 px
6. Begründung (4 Zeilen) ~140 px
7. Footer (absolut, ~50 px vom unteren Rand)
Summe ~1190 + Padding 58 + Gaps ~30 = 1280 / 1350 → ~70 px Slack,
gleichmaessig per justify-content space-between auf die fuenf
inneren Sektionen verteilt.
Visuelle Hierarchie: Score-Hero ist optisch prominent (groesster
Farbblock + groesste Zahl), Matrix als datenreiches Zentrum, Title
deskriptiv aber zurueckhaltend, Begruendung als Ausklang. */
.portrait-body { .portrait-body {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; gap: 14px;
gap: 18px; margin-top: 16px;
margin-top: 18px; padding-bottom: 32px;
padding-bottom: 28px;
} }
.portrait-title { .portrait-title {
@ -253,65 +241,83 @@
line-height: 1.08; line-height: 1.08;
} }
/* ── Matrix-Block: Grid links, Legende rechts, vertikal zentriert ── */ /* ── Matrix-Block: Grid mit beschrifteten Achsen, vertikal zentriert.
flex-grow:1 absorbiert allen verbleibenden Slack der Karte. */
.portrait-matrix-block { .portrait-matrix-block {
flex-grow: 1;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 28px; justify-content: center;
gap: 12px;
} }
.portrait-matrix { .portrait-matrix-grid {
/* 5 Werte als Spalten + 1 Spalte fuer die Zeilen-Labels */
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: 130px repeat(5, 88px);
grid-template-rows: repeat(5, 1fr); grid-template-rows: 36px repeat(5, 88px);
gap: 4px; gap: 4px;
width: 480px; align-items: stretch;
height: 480px;
flex-shrink: 0;
/* Kein weisser Frame — der Karten-Hintergrund (Gradient) wird
durch die gap-Zwischenraeume sichtbar, die Cells stehen klar
als Farbflaechen heraus. */
} }
.portrait-matrix-legend { .portrait-matrix-grid .col-label {
flex: 1;
font-family: 'Source Code Pro', monospace; font-family: 'Source Code Pro', monospace;
font-size: 12pt; font-size: 9.5pt;
color: #444;
line-height: 1.55;
}
.portrait-matrix-legend .l-title {
font-weight: 700; font-weight: 700;
text-transform: uppercase;
color: #009DA5; color: #009DA5;
margin-bottom: 14px; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.04em;
font-size: 12pt; display: flex;
align-items: end;
justify-content: center;
padding-bottom: 4px;
text-align: center;
line-height: 1.1;
} }
.portrait-matrix-legend ul { .portrait-matrix-grid .row-label {
list-style: none; font-family: 'Source Code Pro', monospace;
padding: 0; font-size: 10pt;
margin: 0; font-weight: 700;
} color: #009DA5;
.portrait-matrix-legend li { text-transform: uppercase;
letter-spacing: 0.04em;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; padding-right: 8px;
margin-bottom: 7px; line-height: 1.15;
} }
.portrait-matrix-legend .swatch { .portrait-matrix-grid .corner {
width: 22px; height: 22px; border-radius: 2px; flex-shrink: 0; /* leere Ecke oben links */
border: 1px solid rgba(0,0,0,0.08); }
.portrait-legend-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px 18px;
font-family: 'Source Code Pro', monospace;
font-size: 10.5pt;
color: #444;
margin-top: 4px;
}
.portrait-legend-row .legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.portrait-legend-row .swatch {
width: 18px; height: 18px; border-radius: 2px; flex-shrink: 0;
} }
.portrait-summary { .portrait-summary {
font-size: 13pt; font-size: 13pt;
line-height: 1.55; line-height: 1.5;
color: #333; color: #333;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 4; -webkit-line-clamp: 5;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
border-top: 1px solid rgba(0,0,0,0.08); border-top: 1px solid rgba(0,0,0,0.08);
padding-top: 14px; padding-top: 14px;
flex-shrink: 0;
} }
.portrait-summary-label { .portrait-summary-label {
display: block; display: block;
@ -369,8 +375,25 @@
</div> </div>
<div class="portrait-matrix-block"> <div class="portrait-matrix-block">
<div class="portrait-matrix matrix"> <div class="portrait-matrix-grid matrix">
{% for r in ['A','B','C','D','E'] %} {# Top row: leere Ecke + 5 Werte-Spalten-Labels #}
<div class="corner"></div>
<div class="col-label">Würde</div>
<div class="col-label">Solidari­tät</div>
<div class="col-label">Nach­haltig­keit</div>
<div class="col-label">Gerech­tigkeit</div>
<div class="col-label">Trans­parenz</div>
{# Folgende 5 Zeilen: Beruehrungsgruppen-Label + 5 Cells #}
{% set rows = [
('A', 'Lieferant:­innen'),
('B', 'Finanzen'),
('C', 'Verwal­tung'),
('D', 'Bürger:­innen'),
('E', 'Gesell­schaft & Natur'),
] %}
{% for r, r_label in rows %}
<div class="row-label">{{ r }} · {{ r_label }}</div>
{% for c in ['1','2','3','4','5'] %} {% for c in ['1','2','3','4','5'] %}
{% set cell = matrix_lookup.get(r ~ c, {}) %} {% set cell = matrix_lookup.get(r ~ c, {}) %}
{% set rt = cell.get('rating', 0) %} {% set rt = cell.get('rating', 0) %}
@ -378,15 +401,12 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> </div>
<div class="portrait-matrix-legend"> <div class="portrait-legend-row">
<div class="l-title">GWÖ-Matrix 5×5</div> <span class="legend-item"><span class="swatch" style="background:#889E33;"></span>++ stark fördernd</span>
<ul> <span class="legend-item"><span class="swatch" style="background:#cddaa1;"></span>+ fördernd</span>
<li><span class="swatch" style="background:#889E33;"></span>++ stark fördernd</li> <span class="legend-item"><span class="swatch" style="background:#d8d8d2;"></span>○ neutral</span>
<li><span class="swatch" style="background:#cddaa1;"></span>+ fördernd</li> <span class="legend-item"><span class="swatch" style="background:#efc9c3;"></span> widersprechend</span>
<li><span class="swatch" style="background:#d8d8d2;"></span>○ neutral</li> <span class="legend-item"><span class="swatch" style="background:#9A2A2A;"></span> stark widerspr.</span>
<li><span class="swatch" style="background:#efc9c3;"></span> widersprechend</li>
<li><span class="swatch" style="background:#9A2A2A;"></span> stark widersprechend</li>
</ul>
</div> </div>
</div> </div>