feat: Scorecard Multi-Format (Cloud-Design ZIP-2) — 4 Layouts mit Switcher
User hat die zweite ZIP geliefert: 'Scorecard Formate.html' mit Spec fuer drei zusaetzliche Formate. Plus Anmerkung: 'doppelte Borders' und 'Export viel zu gross'. Vier Formate jetzt im selben Template scorecard_portrait.html: - format=portrait (DEFAULT) → 1080×1350 · 4:5 · IG-Feed - format=square → 1080×1080 · 1:1 · IG/LinkedIn - format=story → 1080×1920 · 9:16 · Story/Reels - format=wide → 1920×1080 · 16:9 · OG/Slide/Twitter Wide hat 2-spaltigen Body-Aufbau (Story-Spalte links, Daten-Spalte rechts, Header+Footer ueber volle Breite), die anderen drei nutzen das gemeinsame 1-spaltige Body-Markup. Aller Formate teilen sich die Daten-Aggregation (Chips, Fraktions-Bars, Beschluss). Bug-Fixes aus dem User-Feedback: 1. 'Doppelte Borders um die Partei und Field-Chips' — die 1.5-px- Borders im Cloud-Design wurden von WeasyPrint als zwei einzelne 1-px-Linien gerendert (Subpixel-Bug bei fractional border-widths). Alle 1.5px → 2px (integer). 2. 'Export viel zu gross' — der Download-Button hatte scale=2 als Default → 2160×2700 PNG-Pixel. Fuer IG-Upload reicht 1080×1350 exakt (Instagram skaliert hochgeladene Bilder ohnehin). Default jetzt scale=1, der ?scale=2-Param bleibt verfuegbar fuer Retina. 3. Statusleiste mit Format-Switcher: vier Pills (4:5 Feed / 1:1 Square / 9:16 Story / 16:9 Wide), aktuelles Format hervorgehoben. Klick wechselt URL-format-Param. Plus PNG- und PDF-Download-Buttons, die das aktuelle Format mitfuehren. main.py: dimensions-Mapping um story+wide erweitert in scorecard_template UND _render_scorecard_pdf. Format-Validation ebenfalls erweitert. format-Variable an's Template durchgeschleift (damit der Template-Switch fuer card-portrait/square/story/wide funktioniert). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
443c9b0874
commit
4569f3335f
37
app/main.py
37
app/main.py
@ -3537,8 +3537,10 @@ async def scorecard_template(
|
|||||||
"og": (1200, 630),
|
"og": (1200, 630),
|
||||||
"square": (1080, 1080),
|
"square": (1080, 1080),
|
||||||
"portrait": (1080, 1350),
|
"portrait": (1080, 1350),
|
||||||
|
"story": (1080, 1920),
|
||||||
|
"wide": (1920, 1080),
|
||||||
}
|
}
|
||||||
width, height = dimensions.get(format, dimensions["og"])
|
width, height = dimensions.get(format, dimensions["portrait"])
|
||||||
|
|
||||||
# ─── Cloud-Design (portrait) braucht aggregierte Hilfsdaten ──────
|
# ─── Cloud-Design (portrait) braucht aggregierte Hilfsdaten ──────
|
||||||
# Chips: Top-3 positiv bewerteter Felder mit Code + Wert-Kurzname + Symbol
|
# Chips: Top-3 positiv bewerteter Felder mit Code + Wert-Kurzname + Symbol
|
||||||
@ -3636,12 +3638,13 @@ async def scorecard_template(
|
|||||||
|
|
||||||
score_color_band = "good" if score >= 7 else "mid" if score >= 4 else "low"
|
score_color_band = "good" if score >= 7 else "mid" if score >= 4 else "low"
|
||||||
|
|
||||||
# Template-Wahl: portrait nutzt das Cloud-Design (eigene Datei),
|
# Template-Wahl: portrait/square/story/wide nutzen das Cloud-Design
|
||||||
# square/og bleiben beim alten Layout.
|
# (eine Template-Datei, format-Variable schaltet Card-Klasse).
|
||||||
|
# Nur og bleibt beim alten Layout (fuer OG-Meta-Tag-Crawler).
|
||||||
template_name = (
|
template_name = (
|
||||||
"v2/screens/scorecard_portrait.html"
|
"v2/screens/scorecard.html"
|
||||||
if format == "portrait"
|
if format == "og"
|
||||||
else "v2/screens/scorecard.html"
|
else "v2/screens/scorecard_portrait.html"
|
||||||
)
|
)
|
||||||
|
|
||||||
response = templates.TemplateResponse(template_name, {
|
response = templates.TemplateResponse(template_name, {
|
||||||
@ -3659,6 +3662,7 @@ async def scorecard_template(
|
|||||||
"beschluss": beschluss,
|
"beschluss": beschluss,
|
||||||
"antrag_typ": (row.get("typ") or "Antrag"),
|
"antrag_typ": (row.get("typ") or "Antrag"),
|
||||||
"wahlperiode": wahlperiode,
|
"wahlperiode": wahlperiode,
|
||||||
|
"format": format,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
})
|
})
|
||||||
@ -3678,9 +3682,15 @@ async def _render_scorecard_pdf(
|
|||||||
from weasyprint import HTML, CSS
|
from weasyprint import HTML, CSS
|
||||||
from .models import Assessment
|
from .models import Assessment
|
||||||
|
|
||||||
if format not in ("og", "square", "portrait"):
|
if format not in ("og", "square", "portrait", "story", "wide"):
|
||||||
raise HTTPException(status_code=400, detail="format muss 'og', 'square' oder 'portrait' sein")
|
raise HTTPException(status_code=400, detail="format muss 'og', 'square', 'portrait', 'story' oder 'wide' sein")
|
||||||
dimensions = {"og": (1200, 630), "square": (1080, 1080), "portrait": (1080, 1350)}
|
dimensions = {
|
||||||
|
"og": (1200, 630),
|
||||||
|
"square": (1080, 1080),
|
||||||
|
"portrait": (1080, 1350),
|
||||||
|
"story": (1080, 1920),
|
||||||
|
"wide": (1920, 1080),
|
||||||
|
}
|
||||||
width, height = dimensions[format]
|
width, height = dimensions[format]
|
||||||
|
|
||||||
drucksache = validate_drucksache(drucksache)
|
drucksache = validate_drucksache(drucksache)
|
||||||
@ -3703,10 +3713,10 @@ async def _render_scorecard_pdf(
|
|||||||
elif score >= 5: score_color = "#bf6c10"
|
elif score >= 5: score_color = "#bf6c10"
|
||||||
else: score_color = "#9a2a2a"
|
else: score_color = "#9a2a2a"
|
||||||
|
|
||||||
# Portrait nutzt das Cloud-Design-Template; square/og das alte.
|
# Cloud-Design-Template fuer portrait/square/story/wide; og bleibt
|
||||||
# Fuer Portrait brauchen wir die gleichen Aggregat-Daten wie der
|
# beim alten Layout. Cloud-Design braucht die gleichen Aggregat-
|
||||||
# HTML-Render (Chips, Fraktions-Bars, Beschluss).
|
# Daten wie der HTML-Render (Chips, Fraktions-Bars, Beschluss).
|
||||||
if format == "portrait":
|
if format in ("portrait", "square", "story", "wide"):
|
||||||
werte_kurz = {
|
werte_kurz = {
|
||||||
"1": "Würde", "2": "Solidarität", "3": "Nachhaltigkeit",
|
"1": "Würde", "2": "Solidarität", "3": "Nachhaltigkeit",
|
||||||
"4": "Soz. Gerechtigkeit", "5": "Transparenz",
|
"4": "Soz. Gerechtigkeit", "5": "Transparenz",
|
||||||
@ -3785,6 +3795,7 @@ async def _render_scorecard_pdf(
|
|||||||
antrag_typ=(row.get("typ") or "Antrag"),
|
antrag_typ=(row.get("typ") or "Antrag"),
|
||||||
wahlperiode=wahlperiode,
|
wahlperiode=wahlperiode,
|
||||||
score_color_band=score_color_band,
|
score_color_band=score_color_band,
|
||||||
|
format=format, # damit das Template den Card-Klassen-Switch macht
|
||||||
width=width, height=height,
|
width=width, height=height,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
{# scorecard_portrait.html — 1080×1350 Hochkant-Scorecard (Instagram 4:5)
|
{# scorecard_portrait.html — Cloud-Design Multi-Format-Scorecard.
|
||||||
|
|
||||||
Designvorgabe: Claude Design, ZIP "GWÖ Antrag Score Card.zip" vom 2026-05-07.
|
Vier Formate mit eigenen Klassen + Größen, alle aus der Spec
|
||||||
Übernommen 1:1 in HTML/CSS, Jinja-Variablen ersetzen die Beispiel-Inhalte.
|
"GWÖ Antrag Score Card-2.zip" (Claude Design, 2026-05-07):
|
||||||
|
|
||||||
Komprimierung gegenüber der Profi-Detail-Ansicht (siehe Spec-Sheet):
|
format=portrait (Default) → card-portrait → 1080×1350 · 4:5 · IG-Feed
|
||||||
- 5×5-Matrix → Top-3 Schwerpunkt-Chips
|
format=square → card-square → 1080×1080 · 1:1 · IG/LinkedIn-Feed
|
||||||
- Zitate, Verbesserungen, Kommentare, News alle raus
|
format=story → card-story → 1080×1920 · 9:16 · Story/Reels
|
||||||
- Stärken/Schwächen-Fließtext → 1-Satz-Zusammenfassung
|
format=wide → card-wide → 1920×1080 · 16:9 · OG/Slide
|
||||||
- Fraktions-Bewertung als Balken-Grid (5 Spalten) statt langer Liste
|
|
||||||
- Beschluss als invertierte schwarze Bar am Ende
|
Wide hat 2-spaltigen Body (Story-Spalte links, Daten-Spalte rechts);
|
||||||
|
die anderen drei haben 1-spaltigen Body (linear gestapelt).
|
||||||
|
|
||||||
|
WeasyPrint rendert nur was der format-Param sagt. Browser-Vorschau
|
||||||
|
skaliert per JS auf 90 % der Viewport-Hoehe (mit Width-Fallback) und
|
||||||
|
zeigt eine Statusleiste mit Format-Switcher und Download-Button.
|
||||||
#}
|
#}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
@ -24,30 +29,268 @@
|
|||||||
--muted:#6b6660;
|
--muted:#6b6660;
|
||||||
--accent:#0a5d3f;
|
--accent:#0a5d3f;
|
||||||
--accent-warn:#b04a2f;
|
--accent-warn:#b04a2f;
|
||||||
--grid:#d9d2c5;
|
--score-bg:#0a5d3f;
|
||||||
--score-bg: {% if score_color_band == 'good' %}#0a5d3f{% elif score_color_band == 'mid' %}#6b6660{% else %}#b04a2f{% endif %};
|
|
||||||
}
|
}
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
html,body{background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;}
|
html,body{background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;}
|
||||||
/* Print/PDF-Defaults — WeasyPrint nutzt media=print und sieht NUR
|
body{ width:{{ width }}px;height:{{ height }}px;overflow:hidden; }
|
||||||
diese Werte. Body == Card-Groesse, ohne Wrapper. */
|
|
||||||
body{
|
|
||||||
width:{{ width }}px;height:{{ height }}px;overflow:hidden;
|
|
||||||
}
|
|
||||||
.card-viewport { width: 100%; height: 100%; }
|
|
||||||
|
|
||||||
|
/* ===== Generische Card-Bauelemente — Größen kommen je Format-Klasse ===== */
|
||||||
.card{
|
.card{
|
||||||
width:1080px;height:1350px;
|
|
||||||
background:var(--paper);color:var(--ink);
|
background:var(--paper);color:var(--ink);
|
||||||
position:relative;overflow:hidden;
|
position:relative;overflow:hidden;
|
||||||
display:grid;grid-template-rows: 88px 1fr 96px;
|
}
|
||||||
|
.head{
|
||||||
|
border-bottom:2px solid var(--rule);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
font-family:'JetBrains Mono',monospace;
|
||||||
|
font-weight:500;text-transform:uppercase;
|
||||||
|
}
|
||||||
|
.head .brand{display:flex;align-items:center;gap:12px}
|
||||||
|
.head .dot{background:var(--accent);border-radius:50%}
|
||||||
|
.head .meta{display:flex;color:var(--muted)}
|
||||||
|
.ids{font-family:'JetBrains Mono',monospace;color:var(--muted);letter-spacing:.06em}
|
||||||
|
.ids strong{color:var(--ink);font-weight:600}
|
||||||
|
.party-pill{
|
||||||
|
display:inline-flex;align-items:center;gap:10px;
|
||||||
|
border:2px solid var(--rule);border-radius:999px;
|
||||||
|
font-weight:700;letter-spacing:.04em;
|
||||||
|
}
|
||||||
|
.party-pill::before{content:"";border-radius:50%;background:#d44}
|
||||||
|
h1.title{font-weight:800;letter-spacing:-.025em;text-wrap:balance}
|
||||||
|
.lede{color:var(--ink);text-wrap:pretty}
|
||||||
|
.scorewrap{
|
||||||
|
display:grid;align-items:stretch;
|
||||||
|
border-top:2px solid var(--rule);border-bottom:2px solid var(--rule);
|
||||||
|
}
|
||||||
|
.score{
|
||||||
|
background:var(--score-bg);color:var(--paper);
|
||||||
|
display:flex;flex-direction:column;justify-content:center;align-items:center;
|
||||||
|
border-radius:8px;
|
||||||
|
}
|
||||||
|
.score .num{font-weight:800;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums}
|
||||||
|
.score .num small{opacity:.7;font-weight:600}
|
||||||
|
.score .label{font-family:'JetBrains Mono',monospace;text-transform:uppercase;letter-spacing:.1em;opacity:.85}
|
||||||
|
.verdict{display:flex;flex-direction:column;justify-content:center}
|
||||||
|
.verdict .kicker{font-family:'JetBrains Mono',monospace;text-transform:uppercase;letter-spacing:.12em;color:var(--muted)}
|
||||||
|
.verdict .vtext{font-weight:700;line-height:1.1;letter-spacing:-.01em}
|
||||||
|
.verdict .fields{display:flex;flex-wrap:wrap}
|
||||||
|
.field-chip{
|
||||||
|
font-family:'JetBrains Mono',monospace;font-weight:600;
|
||||||
|
border:2px solid var(--rule);border-radius:4px;background:transparent;
|
||||||
|
}
|
||||||
|
.fractions{display:grid;grid-template-columns:repeat({{ fraktionen_count }},1fr);border:2px solid var(--rule)}
|
||||||
|
.frac{
|
||||||
|
border-right:2px solid var(--rule);
|
||||||
|
display:flex;flex-direction:column;align-items:center;text-align:center;
|
||||||
|
background:var(--paper);
|
||||||
|
}
|
||||||
|
.frac:last-child{border-right:none}
|
||||||
|
.frac .name{font-family:'JetBrains Mono',monospace;font-weight:700;letter-spacing:.05em}
|
||||||
|
.frac .bar{width:100%;background:#e6dfd1;border-radius:2px;overflow:hidden}
|
||||||
|
.frac .bar > span{display:block;height:100%;background:var(--accent)}
|
||||||
|
.frac.weak .bar > span{background:var(--accent-warn)}
|
||||||
|
.frac .val{font-family:'JetBrains Mono',monospace;font-weight:600}
|
||||||
|
.frac .vote{font-weight:700;letter-spacing:.08em;text-transform:uppercase}
|
||||||
|
.vote.ja{color:var(--accent)}
|
||||||
|
.vote.nein{color:var(--accent-warn)}
|
||||||
|
.vote.enth{color:var(--muted)}
|
||||||
|
.vote.unbekannt{color:var(--muted);opacity:.6}
|
||||||
|
.decision{
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
background:var(--ink);color:var(--paper);border-radius:6px;
|
||||||
|
}
|
||||||
|
.decision .left{font-family:'JetBrains Mono',monospace;letter-spacing:.1em;text-transform:uppercase;opacity:.7}
|
||||||
|
.decision .right{font-weight:700}
|
||||||
|
.decision .right .x{color:#ff6a4a;margin-right:10px}
|
||||||
|
.decision .right .ok{color:#7fd9a8;margin-right:10px}
|
||||||
|
.foot{
|
||||||
|
border-top:2px solid var(--rule);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
font-family:'JetBrains Mono',monospace;letter-spacing:.04em;color:var(--muted);
|
||||||
|
}
|
||||||
|
.foot .url{color:var(--ink);font-weight:600}
|
||||||
|
.foot .qr{
|
||||||
|
background:repeating-conic-gradient(var(--ink) 0 25%, var(--paper) 0 50%) 50%/16px 16px;
|
||||||
|
border:2px solid var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Browser-Preview — skaliert die Card auf Viewport-Breite, scrollbar
|
/* ===== A · 4:5 Portrait — 1080×1350 (DEFAULT, IG-Feed) ===== */
|
||||||
wenn die Hoehe nicht reicht. WeasyPrint ignoriert @media screen.
|
.card-portrait{
|
||||||
Die eigentliche Skalierung wird per JS gesetzt (CSS calc/scale
|
width:1080px;height:1350px;
|
||||||
mit Unit-Mismatch geht nicht), CSS-Defaults sorgen nur fuer das
|
display:grid;grid-template-rows: 88px 1fr 96px;
|
||||||
Drumherum. */
|
}
|
||||||
|
.card-portrait .head{padding:0 56px;font-size:18px;letter-spacing:.04em}
|
||||||
|
.card-portrait .head .dot{width:14px;height:14px}
|
||||||
|
.card-portrait .head .meta{gap:24px}
|
||||||
|
.card-portrait .body{padding:40px 56px 32px;display:flex;flex-direction:column;gap:28px;min-height:0}
|
||||||
|
.card-portrait .topline{display:flex;justify-content:space-between;align-items:flex-start;gap:32px}
|
||||||
|
.card-portrait .ids{font-size:20px}
|
||||||
|
.card-portrait .party-pill{padding:8px 18px;font-size:20px}
|
||||||
|
.card-portrait .party-pill::before{width:12px;height:12px}
|
||||||
|
.card-portrait h1.title{font-size:78px;line-height:.95}
|
||||||
|
.card-portrait .lede{font-size:26px;line-height:1.35;max-width:880px}
|
||||||
|
.card-portrait .scorewrap{grid-template-columns:320px 1fr;gap:32px;padding:24px 0}
|
||||||
|
.card-portrait .score{padding:18px}
|
||||||
|
.card-portrait .score .num{font-size:120px}
|
||||||
|
.card-portrait .score .num small{font-size:42px}
|
||||||
|
.card-portrait .score .label{font-size:15px;margin-top:6px}
|
||||||
|
.card-portrait .verdict{gap:14px}
|
||||||
|
.card-portrait .verdict .kicker{font-size:15px}
|
||||||
|
.card-portrait .verdict .vtext{font-size:34px}
|
||||||
|
.card-portrait .verdict .fields{gap:8px;margin-top:4px}
|
||||||
|
.card-portrait .field-chip{font-size:16px;padding:4px 10px}
|
||||||
|
.card-portrait .frac{padding:16px 14px;gap:8px}
|
||||||
|
.card-portrait .frac .name{font-size:17px}
|
||||||
|
.card-portrait .frac .bar{height:8px}
|
||||||
|
.card-portrait .frac .val{font-size:18px}
|
||||||
|
.card-portrait .frac .vote{font-size:14px}
|
||||||
|
.card-portrait .decision{padding:18px 22px}
|
||||||
|
.card-portrait .decision .left{font-size:16px}
|
||||||
|
.card-portrait .decision .right{font-size:24px}
|
||||||
|
.card-portrait .foot{padding:0 56px;font-size:16px}
|
||||||
|
.card-portrait .foot .qr{width:64px;height:64px}
|
||||||
|
|
||||||
|
/* ===== B · 1:1 Square — 1080×1080 (IG/LinkedIn-Feed) ===== */
|
||||||
|
.card-square{
|
||||||
|
width:1080px;height:1080px;
|
||||||
|
display:grid;grid-template-rows:80px 1fr 80px;
|
||||||
|
}
|
||||||
|
.card-square .head{padding:0 48px;font-size:16px;letter-spacing:.04em}
|
||||||
|
.card-square .head .dot{width:13px;height:13px}
|
||||||
|
.card-square .head .meta{gap:20px}
|
||||||
|
.card-square .body{padding:32px 48px 24px;display:flex;flex-direction:column;gap:20px;min-height:0}
|
||||||
|
.card-square .topline{display:flex;justify-content:space-between;align-items:center;gap:24px}
|
||||||
|
.card-square .ids{font-size:17px}
|
||||||
|
.card-square .party-pill{padding:6px 16px;font-size:18px}
|
||||||
|
.card-square .party-pill::before{width:11px;height:11px}
|
||||||
|
.card-square h1.title{font-size:64px;line-height:.95}
|
||||||
|
.card-square .lede{font-size:21px;line-height:1.35;max-width:880px}
|
||||||
|
.card-square .scorewrap{grid-template-columns:280px 1fr;gap:24px;padding:18px 0}
|
||||||
|
.card-square .score{padding:14px}
|
||||||
|
.card-square .score .num{font-size:96px}
|
||||||
|
.card-square .score .num small{font-size:36px}
|
||||||
|
.card-square .score .label{font-size:13px;margin-top:4px}
|
||||||
|
.card-square .verdict{gap:10px}
|
||||||
|
.card-square .verdict .kicker{font-size:13px}
|
||||||
|
.card-square .verdict .vtext{font-size:28px}
|
||||||
|
.card-square .verdict .fields{gap:6px;margin-top:2px}
|
||||||
|
.card-square .field-chip{font-size:14px;padding:3px 9px}
|
||||||
|
.card-square .frac{padding:12px 10px;gap:6px}
|
||||||
|
.card-square .frac .name{font-size:15px}
|
||||||
|
.card-square .frac .bar{height:7px}
|
||||||
|
.card-square .frac .val{font-size:16px}
|
||||||
|
.card-square .frac .vote{font-size:12px}
|
||||||
|
.card-square .decision{padding:14px 18px}
|
||||||
|
.card-square .decision .left{font-size:14px}
|
||||||
|
.card-square .decision .right{font-size:21px}
|
||||||
|
.card-square .foot{padding:0 48px;font-size:14px}
|
||||||
|
.card-square .foot .qr{width:54px;height:54px}
|
||||||
|
|
||||||
|
/* ===== C · 9:16 Story — 1080×1920 (Story/Reels) ===== */
|
||||||
|
.card-story{
|
||||||
|
width:1080px;height:1920px;
|
||||||
|
display:grid;grid-template-rows:104px 1fr 112px;
|
||||||
|
}
|
||||||
|
.card-story .head{padding:0 64px;font-size:20px}
|
||||||
|
.card-story .head .dot{width:16px;height:16px}
|
||||||
|
.card-story .head .meta{gap:28px}
|
||||||
|
.card-story .body{padding:80px 64px 56px;display:flex;flex-direction:column;gap:44px;min-height:0}
|
||||||
|
.card-story .topline{display:flex;justify-content:space-between;align-items:flex-start;gap:32px}
|
||||||
|
.card-story .ids{font-size:24px}
|
||||||
|
.card-story .party-pill{padding:10px 22px;font-size:24px}
|
||||||
|
.card-story .party-pill::before{width:14px;height:14px}
|
||||||
|
.card-story h1.title{font-size:108px;line-height:.92}
|
||||||
|
.card-story .lede{font-size:34px;line-height:1.32;max-width:920px}
|
||||||
|
/* Story: Score-Block VERTIKAL gestapelt */
|
||||||
|
.card-story .scorewrap{grid-template-columns:1fr;gap:0;padding:32px 0}
|
||||||
|
.card-story .score{padding:36px 24px;border-radius:10px;margin-bottom:28px}
|
||||||
|
.card-story .score .num{font-size:200px}
|
||||||
|
.card-story .score .num small{font-size:64px}
|
||||||
|
.card-story .score .label{font-size:20px;margin-top:8px}
|
||||||
|
.card-story .verdict{gap:18px;text-align:center;align-items:center}
|
||||||
|
.card-story .verdict .kicker{font-size:20px}
|
||||||
|
.card-story .verdict .vtext{font-size:48px;text-align:center}
|
||||||
|
.card-story .verdict .fields{gap:10px;justify-content:center}
|
||||||
|
.card-story .field-chip{font-size:20px;padding:6px 14px}
|
||||||
|
.card-story .frac{padding:24px 14px;gap:12px}
|
||||||
|
.card-story .frac .name{font-size:22px}
|
||||||
|
.card-story .frac .bar{height:10px}
|
||||||
|
.card-story .frac .val{font-size:24px}
|
||||||
|
.card-story .frac .vote{font-size:18px}
|
||||||
|
.card-story .decision{padding:24px 28px}
|
||||||
|
.card-story .decision .left{font-size:20px}
|
||||||
|
.card-story .decision .right{font-size:30px}
|
||||||
|
.card-story .foot{padding:0 64px;font-size:18px}
|
||||||
|
.card-story .foot .qr{width:76px;height:76px}
|
||||||
|
|
||||||
|
/* ===== D · 16:9 Wide — 1920×1080 (OG/Slide/Twitter) ===== */
|
||||||
|
.card-wide{
|
||||||
|
width:1920px;height:1080px;
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:760px 1fr;
|
||||||
|
grid-template-rows:80px 1fr 80px;
|
||||||
|
grid-template-areas:
|
||||||
|
"head head"
|
||||||
|
"left right"
|
||||||
|
"foot foot";
|
||||||
|
}
|
||||||
|
.card-wide .head{
|
||||||
|
grid-area:head;
|
||||||
|
padding:0 56px;font-size:18px;letter-spacing:.04em;
|
||||||
|
}
|
||||||
|
.card-wide .head .dot{width:14px;height:14px}
|
||||||
|
.card-wide .head .meta{gap:24px}
|
||||||
|
.card-wide .left{
|
||||||
|
grid-area:left;
|
||||||
|
padding:48px 40px 40px 56px;
|
||||||
|
display:flex;flex-direction:column;gap:24px;
|
||||||
|
border-right:2px solid var(--rule);
|
||||||
|
}
|
||||||
|
.card-wide .topline{display:flex;justify-content:space-between;align-items:center;gap:24px}
|
||||||
|
.card-wide .ids{font-size:18px}
|
||||||
|
.card-wide .party-pill{padding:7px 16px;font-size:18px}
|
||||||
|
.card-wide .party-pill::before{width:11px;height:11px}
|
||||||
|
.card-wide h1.title{font-size:72px;line-height:.94}
|
||||||
|
.card-wide .lede{font-size:22px;line-height:1.35;max-width:660px}
|
||||||
|
.card-wide .scorewrap{
|
||||||
|
grid-template-columns:240px 1fr;gap:24px;padding:20px 0;
|
||||||
|
margin-top:auto;
|
||||||
|
}
|
||||||
|
.card-wide .score{padding:14px}
|
||||||
|
.card-wide .score .num{font-size:96px}
|
||||||
|
.card-wide .score .num small{font-size:36px}
|
||||||
|
.card-wide .score .label{font-size:13px;margin-top:4px}
|
||||||
|
.card-wide .verdict{gap:10px}
|
||||||
|
.card-wide .verdict .kicker{font-size:13px}
|
||||||
|
.card-wide .verdict .vtext{font-size:28px}
|
||||||
|
.card-wide .verdict .fields{gap:6px;margin-top:2px}
|
||||||
|
.card-wide .field-chip{font-size:14px;padding:3px 9px}
|
||||||
|
.card-wide .right{
|
||||||
|
grid-area:right;
|
||||||
|
padding:48px 56px 40px 48px;
|
||||||
|
display:flex;flex-direction:column;gap:28px;justify-content:space-between;
|
||||||
|
}
|
||||||
|
.card-wide .right .right-head{
|
||||||
|
font-family:'JetBrains Mono',monospace;
|
||||||
|
font-size:14px;letter-spacing:.12em;text-transform:uppercase;
|
||||||
|
color:var(--muted);
|
||||||
|
}
|
||||||
|
.card-wide .frac{padding:18px 14px;gap:10px}
|
||||||
|
.card-wide .frac .name{font-size:18px}
|
||||||
|
.card-wide .frac .bar{height:9px}
|
||||||
|
.card-wide .frac .val{font-size:20px}
|
||||||
|
.card-wide .frac .vote{font-size:14px}
|
||||||
|
.card-wide .decision{padding:18px 24px}
|
||||||
|
.card-wide .decision .left{font-size:15px}
|
||||||
|
.card-wide .decision .right{font-size:24px;font-weight:700}
|
||||||
|
.card-wide .foot{
|
||||||
|
grid-area:foot;
|
||||||
|
padding:0 56px;font-size:14px;
|
||||||
|
}
|
||||||
|
.card-wide .foot .qr{width:56px;height:56px}
|
||||||
|
|
||||||
|
/* ===== Browser-Preview — Skalierung + Toolbar ===== */
|
||||||
@media screen {
|
@media screen {
|
||||||
html, body {
|
html, body {
|
||||||
background: #2a2724 !important;
|
background: #2a2724 !important;
|
||||||
@ -56,7 +299,7 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px 20px 80px; /* bottom-padding fuer die fixed Toolbar */
|
padding: 20px 20px 80px;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -64,190 +307,133 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
.card-viewport {
|
.card-viewport {
|
||||||
width: 1080px;
|
width: {{ width }}px;
|
||||||
height: 1350px;
|
height: {{ height }}px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 30px 80px rgba(0,0,0,0.5);
|
box-shadow: 0 30px 80px rgba(0,0,0,0.5);
|
||||||
background: var(--paper);
|
background: var(--paper);
|
||||||
}
|
}
|
||||||
.card {
|
.card { transform-origin: top left; position: absolute; top: 0; left: 0; }
|
||||||
transform-origin: top left;
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Statusleiste am unteren Viewport-Rand mit Download-Button.
|
|
||||||
Nur im Browser sichtbar; PDF bekommt sie nicht. */
|
|
||||||
.screen-toolbar {
|
.screen-toolbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0; left: 0; right: 0;
|
bottom: 0; left: 0; right: 0;
|
||||||
background: var(--ink);
|
background: var(--ink); color: var(--paper);
|
||||||
color: var(--paper);
|
padding: 10px 20px;
|
||||||
padding: 12px 24px;
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
display: flex;
|
gap: 16px; z-index: 1000; flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
z-index: 1000;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
box-shadow: 0 -8px 24px rgba(0,0,0,0.3);
|
box-shadow: 0 -8px 24px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
.screen-toolbar .label {
|
.screen-toolbar .label { opacity: 0.75; letter-spacing: 0.04em; }
|
||||||
opacity: 0.75;
|
.screen-toolbar .label strong { color: var(--paper); font-weight: 600; opacity: 1; }
|
||||||
letter-spacing: 0.04em;
|
.screen-toolbar .group { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
||||||
}
|
.format-pill {
|
||||||
.screen-toolbar .label strong {
|
padding: 6px 12px;
|
||||||
color: var(--paper);
|
border-radius: 3px;
|
||||||
font-weight: 600;
|
font-family: 'JetBrains Mono', monospace; font-size: 11px;
|
||||||
opacity: 1;
|
font-weight: 700; letter-spacing: 0.04em;
|
||||||
}
|
text-decoration: none; cursor: pointer;
|
||||||
.screen-toolbar .actions {
|
background: transparent; color: var(--paper);
|
||||||
display: flex;
|
border: 2px solid rgba(245, 241, 234, 0.3);
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.screen-download {
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--paper);
|
|
||||||
padding: 8px 18px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-decoration: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.screen-download:hover {
|
.format-pill:hover { background: rgba(245, 241, 234, 0.1); border-color: var(--paper); }
|
||||||
filter: brightness(1.1);
|
.format-pill.active { background: var(--paper); color: var(--ink); border-color: var(--paper); }
|
||||||
|
.screen-download {
|
||||||
|
background: var(--accent); color: var(--paper);
|
||||||
|
padding: 8px 18px; border-radius: 4px;
|
||||||
|
font-family: 'JetBrains Mono', monospace; font-size: 13px;
|
||||||
|
font-weight: 700; text-decoration: none; border: none; cursor: pointer;
|
||||||
|
letter-spacing: 0.04em; text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
.screen-download:hover { filter: brightness(1.1); }
|
||||||
.screen-download.secondary {
|
.screen-download.secondary {
|
||||||
background: transparent;
|
background: transparent; color: var(--paper);
|
||||||
color: var(--paper);
|
border: 2px solid rgba(245, 241, 234, 0.3);
|
||||||
border: 1.5px solid rgba(245, 241, 234, 0.3);
|
|
||||||
}
|
}
|
||||||
.screen-download.secondary:hover {
|
.screen-download.secondary:hover {
|
||||||
background: rgba(245, 241, 234, 0.1);
|
background: rgba(245, 241, 234, 0.1); border-color: var(--paper);
|
||||||
border-color: var(--paper);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media print { .screen-toolbar { display: none !important; } }
|
@media print { .screen-toolbar { display: none !important; } }
|
||||||
|
|
||||||
/* Kopfleiste */
|
|
||||||
.head{
|
|
||||||
border-bottom:2px solid var(--rule);
|
|
||||||
padding:0 56px;
|
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
|
||||||
font-family:'JetBrains Mono',monospace;
|
|
||||||
font-size:18px;font-weight:500;letter-spacing:.04em;
|
|
||||||
text-transform:uppercase;
|
|
||||||
}
|
|
||||||
.head .brand{display:flex;align-items:center;gap:14px}
|
|
||||||
.head .dot{width:14px;height:14px;background:var(--accent);border-radius:50%}
|
|
||||||
.head .meta{display:flex;gap:24px;color:var(--muted)}
|
|
||||||
|
|
||||||
/* Body */
|
|
||||||
.body{padding:40px 56px 32px;display:flex;flex-direction:column;gap:28px;min-height:0}
|
|
||||||
|
|
||||||
.topline{display:flex;justify-content:space-between;align-items:flex-start;gap:32px}
|
|
||||||
.ids{font-family:'JetBrains Mono',monospace;font-size:20px;color:var(--muted);letter-spacing:.06em}
|
|
||||||
.ids strong{color:var(--ink);font-weight:600}
|
|
||||||
.party-pill{
|
|
||||||
display:inline-flex;align-items:center;gap:10px;
|
|
||||||
padding:8px 18px;border:2px solid var(--rule);border-radius:999px;
|
|
||||||
font-weight:700;font-size:20px;letter-spacing:.04em;
|
|
||||||
}
|
|
||||||
.party-pill::before{content:"";width:12px;height:12px;border-radius:50%;background:#d44}
|
|
||||||
|
|
||||||
h1.title{
|
|
||||||
font-family:'Inter',sans-serif;
|
|
||||||
font-weight:800;font-size:78px;line-height:.95;
|
|
||||||
letter-spacing:-.025em;text-wrap:balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lede{
|
|
||||||
font-size:26px;line-height:1.35;color:var(--ink);
|
|
||||||
max-width:880px;text-wrap:pretty;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scorewrap{
|
|
||||||
display:grid;grid-template-columns: 320px 1fr;gap:32px;align-items:stretch;
|
|
||||||
border-top:2px solid var(--rule);border-bottom:2px solid var(--rule);
|
|
||||||
padding:24px 0;
|
|
||||||
}
|
|
||||||
.score{
|
|
||||||
background:var(--score-bg);color:var(--paper);
|
|
||||||
display:flex;flex-direction:column;justify-content:center;align-items:center;
|
|
||||||
padding:18px;border-radius:8px;
|
|
||||||
}
|
|
||||||
.score .num{font-size:120px;font-weight:800;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums}
|
|
||||||
.score .num small{font-size:42px;opacity:.7;font-weight:600}
|
|
||||||
.score .label{font-family:'JetBrains Mono',monospace;font-size:15px;text-transform:uppercase;letter-spacing:.1em;margin-top:6px;opacity:.85}
|
|
||||||
|
|
||||||
.verdict{display:flex;flex-direction:column;justify-content:center;gap:14px}
|
|
||||||
.verdict .kicker{font-family:'JetBrains Mono',monospace;font-size:15px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted)}
|
|
||||||
.verdict .vtext{font-size:34px;font-weight:700;line-height:1.1;letter-spacing:-.01em}
|
|
||||||
.verdict .fields{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
|
|
||||||
.field-chip{
|
|
||||||
font-family:'JetBrains Mono',monospace;font-size:16px;font-weight:600;
|
|
||||||
border:1.5px solid var(--rule);padding:4px 10px;border-radius:4px;background:transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fraktionen Grid */
|
|
||||||
.fractions{display:grid;grid-template-columns:repeat({{ fraktionen_count }},1fr);gap:0;border:1.5px solid var(--rule)}
|
|
||||||
.frac{
|
|
||||||
padding:16px 14px;border-right:1.5px solid var(--rule);
|
|
||||||
display:flex;flex-direction:column;gap:8px;align-items:center;text-align:center;
|
|
||||||
background:var(--paper);
|
|
||||||
}
|
|
||||||
.frac:last-child{border-right:none}
|
|
||||||
.frac .name{font-family:'JetBrains Mono',monospace;font-size:17px;font-weight:700;letter-spacing:.05em}
|
|
||||||
.frac .bar{height:8px;width:100%;background:#e6dfd1;border-radius:2px;overflow:hidden}
|
|
||||||
.frac .bar > span{display:block;height:100%;background:var(--accent)}
|
|
||||||
.frac.weak .bar > span{background:var(--accent-warn)}
|
|
||||||
.frac .val{font-family:'JetBrains Mono',monospace;font-size:18px;font-weight:600}
|
|
||||||
.frac .vote{font-size:14px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
|
|
||||||
.vote.ja{color:var(--accent)}
|
|
||||||
.vote.nein{color:var(--accent-warn)}
|
|
||||||
.vote.enth{color:var(--muted)}
|
|
||||||
.vote.unbekannt{color:var(--muted);opacity:.6}
|
|
||||||
|
|
||||||
/* Beschluss */
|
|
||||||
.decision{
|
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
|
||||||
padding:18px 22px;background:var(--ink);color:var(--paper);
|
|
||||||
border-radius:6px;
|
|
||||||
}
|
|
||||||
.decision .left{font-family:'JetBrains Mono',monospace;font-size:16px;letter-spacing:.1em;text-transform:uppercase;opacity:.7}
|
|
||||||
.decision .right{font-size:24px;font-weight:700}
|
|
||||||
.decision .right .x{color:#ff6a4a;margin-right:10px}
|
|
||||||
.decision .right .ok{color:#7fd9a8;margin-right:10px}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.foot{
|
|
||||||
border-top:2px solid var(--rule);
|
|
||||||
padding:0 56px;
|
|
||||||
display:flex;align-items:center;justify-content:space-between;
|
|
||||||
font-family:'JetBrains Mono',monospace;font-size:16px;letter-spacing:.04em;
|
|
||||||
color:var(--muted);
|
|
||||||
}
|
|
||||||
.foot .url{color:var(--ink);font-weight:600}
|
|
||||||
.foot .qr{width:64px;height:64px;background:
|
|
||||||
repeating-conic-gradient(var(--ink) 0 25%, var(--paper) 0 50%) 50%/16px 16px;
|
|
||||||
border:2px solid var(--ink);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="card-viewport">
|
<div class="card-viewport">
|
||||||
<div class="card">
|
{# ===== Wide-Variante hat eigene Body-Struktur (2-spaltig) ===== #}
|
||||||
|
{% if format == 'wide' %}
|
||||||
|
<div class="card card-wide">
|
||||||
|
<header class="head">
|
||||||
|
<div class="brand"><span class="dot"></span>GWÖ-Antragsprüfer · Matrix 2.0</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span>{{ bundesland }}{% if wahlperiode %} · {{ wahlperiode.split('-')[-1] if '-' in wahlperiode else 'WP' ~ wahlperiode }}{% endif %}</span>
|
||||||
|
<span>Drs. {{ assessment.drucksache }}</span>
|
||||||
|
{% if assessment.datum %}<span>{{ assessment.datum }}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section class="left">
|
||||||
|
<div class="topline">
|
||||||
|
<div class="ids"><strong>{{ antrag_typ or 'Antrag' }}</strong>{% if assessment.datum %} · eingebracht {{ assessment.datum }}{% endif %}</div>
|
||||||
|
{% if assessment.fraktionen %}<div class="party-pill">{{ assessment.fraktionen[0] }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<h1 class="title">{{ assessment.title|truncate(80, end="…") }}</h1>
|
||||||
|
{% if assessment.antrag_zusammenfassung %}
|
||||||
|
<p class="lede">{{ assessment.antrag_zusammenfassung|truncate(180, end="…") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="scorewrap">
|
||||||
|
<div class="score">
|
||||||
|
<div class="num">{{ "%.1f"|format(assessment.gwoe_score) }}<small>/10</small></div>
|
||||||
|
<div class="label">GWÖ-Score</div>
|
||||||
|
</div>
|
||||||
|
<div class="verdict">
|
||||||
|
<div class="kicker">Empfehlung</div>
|
||||||
|
<div class="vtext">{{ assessment.empfehlung.value }}</div>
|
||||||
|
{% if matrix_chips %}
|
||||||
|
<div class="fields">
|
||||||
|
{% for chip in matrix_chips %}<span class="field-chip">{{ chip.code }} {{ chip.symbol }}</span>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="right">
|
||||||
|
<div class="right-head">Programm-Treue · Fraktionen · Abstimmung</div>
|
||||||
|
{% if fraktionen_bars %}
|
||||||
|
<div class="fractions">
|
||||||
|
{% for f in fraktionen_bars %}
|
||||||
|
<div class="frac{% if f.weak %} weak{% endif %}">
|
||||||
|
<div class="name">{{ f.name }}</div>
|
||||||
|
<div class="bar"><span style="width:{{ f.bar_pct }}%"></span></div>
|
||||||
|
<div class="val">{{ f.score_text }}</div>
|
||||||
|
<div class="vote {{ f.vote_class }}">{{ f.vote_label }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if beschluss %}
|
||||||
|
<div class="decision">
|
||||||
|
<div class="left">Beschluss Plenum</div>
|
||||||
|
<div class="right">
|
||||||
|
<span class="{{ 'ok' if beschluss.is_positive else 'x' }}">{{ '✓' if beschluss.is_positive else '✗' }}</span>
|
||||||
|
{{ beschluss.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
<footer class="foot">
|
||||||
|
<div><span class="url">gwoe.toppyr.de</span> · /antrag/{{ assessment.drucksache }}</div>
|
||||||
|
<div>CC BY 4.0 · Matrix 2.0</div>
|
||||||
|
<div class="qr" aria-hidden="true"></div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
{# ===== Portrait / Square / Story — gemeinsame 1-spaltige Body-Struktur ===== #}
|
||||||
|
{% set card_cls = 'card-' + (format if format in ['portrait','square','story'] else 'portrait') %}
|
||||||
|
<div class="card {{ card_cls }}">
|
||||||
<header class="head">
|
<header class="head">
|
||||||
<div class="brand"><span class="dot"></span>GWÖ-Antragsprüfer</div>
|
<div class="brand"><span class="dot"></span>GWÖ-Antragsprüfer</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
@ -255,14 +441,12 @@
|
|||||||
<span>{{ bundesland }}{% if wahlperiode %} · {{ wahlperiode.split('-')[-1] if '-' in wahlperiode else 'WP' ~ wahlperiode }}{% endif %}</span>
|
<span>{{ bundesland }}{% if wahlperiode %} · {{ wahlperiode.split('-')[-1] if '-' in wahlperiode else 'WP' ~ wahlperiode }}{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="body">
|
<main class="body">
|
||||||
|
|
||||||
<div class="topline">
|
<div class="topline">
|
||||||
<div class="ids">
|
<div class="ids">
|
||||||
<strong>Drs. {{ assessment.drucksache }}</strong>
|
<strong>Drs. {{ assessment.drucksache }}</strong>
|
||||||
{% if antrag_typ %} · {{ antrag_typ }}{% endif %}
|
{% if antrag_typ %} · {{ antrag_typ }}{% endif %}
|
||||||
{% if assessment.datum %} · eingebracht {{ assessment.datum }}{% endif %}
|
{% if assessment.datum %} · {{ assessment.datum }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if assessment.fraktionen %}
|
{% if assessment.fraktionen %}
|
||||||
<div class="party-pill">{{ assessment.fraktionen[0] }}</div>
|
<div class="party-pill">{{ assessment.fraktionen[0] }}</div>
|
||||||
@ -315,58 +499,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="foot">
|
<footer class="foot">
|
||||||
<div><span class="url">gwoe.toppyr.de</span> · /antrag/{{ assessment.drucksache }}</div>
|
<div><span class="url">gwoe.toppyr.de</span> · /antrag/{{ assessment.drucksache }}</div>
|
||||||
<div>CC BY 4.0</div>
|
<div>CC BY 4.0</div>
|
||||||
<div class="qr" aria-hidden="true"></div>
|
<div class="qr" aria-hidden="true"></div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>{# .card-viewport #}
|
</div>{# .card-viewport #}
|
||||||
|
|
||||||
{# Statusleiste mit Download-Button (nur im Browser, nicht im PDF). #}
|
{# ===== Statusleiste — Format-Switcher + Download-Buttons ===== #}
|
||||||
{% set _drs_safe = assessment.drucksache | replace('/', '-') %}
|
{% set _drs_safe = assessment.drucksache | replace('/', '-') %}
|
||||||
|
{% set _query_base = '?drucksache=' + (assessment.drucksache | urlencode) + '&bundesland=' + (bundesland | urlencode) %}
|
||||||
<div class="screen-toolbar">
|
<div class="screen-toolbar">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<strong>Scorecard</strong> · {{ bundesland }} · {{ assessment.drucksache }}
|
<strong>Scorecard</strong> · {{ bundesland }} · {{ assessment.drucksache }}
|
||||||
· 1080×1350 (Instagram 4:5)
|
· {{ width }}×{{ height }}
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="group">
|
||||||
|
{% set _formats = [
|
||||||
|
('portrait', '4:5 Feed'),
|
||||||
|
('square', '1:1 Square'),
|
||||||
|
('story', '9:16 Story'),
|
||||||
|
('wide', '16:9 Wide'),
|
||||||
|
] %}
|
||||||
|
{% for fkey, flabel in _formats %}
|
||||||
|
<a class="format-pill {% if format == fkey %}active{% endif %}"
|
||||||
|
href="/v2/scorecard{{ _query_base }}&format={{ fkey }}">{{ flabel }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
<a class="screen-download"
|
<a class="screen-download"
|
||||||
href="/api/assessment/scorecard.png?drucksache={{ assessment.drucksache | urlencode }}&bundesland={{ bundesland | urlencode }}&format=portrait&scale=2"
|
href="/api/assessment/scorecard.png{{ _query_base }}&format={{ format }}&scale=1"
|
||||||
download="gwoe-scorecard-{{ _drs_safe }}.png">
|
download="gwoe-scorecard-{{ _drs_safe }}-{{ format }}.png">
|
||||||
⬇ PNG herunterladen
|
⬇ PNG
|
||||||
</a>
|
</a>
|
||||||
<a class="screen-download secondary"
|
<a class="screen-download secondary"
|
||||||
href="/api/assessment/scorecard.pdf?drucksache={{ assessment.drucksache | urlencode }}&bundesland={{ bundesland | urlencode }}&format=portrait"
|
href="/api/assessment/scorecard.pdf{{ _query_base }}&format={{ format }}"
|
||||||
download="gwoe-scorecard-{{ _drs_safe }}.pdf">
|
download="gwoe-scorecard-{{ _drs_safe }}-{{ format }}.pdf">
|
||||||
PDF
|
PDF
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* Browser-Preview-Skalierung: Card soll 90 % der Viewport-Hoehe einnehmen.
|
/* Browser-Preview-Skalierung — 90% Viewport-Hoehe, mit Width-Fallback. */
|
||||||
Falls die daraus resultierende Breite ueber den Viewport hinausginge
|
|
||||||
(schmale Mobile-Viewports), greift die Breiten-Begrenzung. WeasyPrint
|
|
||||||
fuehrt kein JS aus → PDF bleibt bei skalierungsfreier Originalgroesse. */
|
|
||||||
(function () {
|
(function () {
|
||||||
|
var W = {{ width }}, H = {{ height }};
|
||||||
function adjustScale() {
|
function adjustScale() {
|
||||||
var card = document.querySelector('.card');
|
var card = document.querySelector('.card');
|
||||||
var viewport = document.querySelector('.card-viewport');
|
var viewport = document.querySelector('.card-viewport');
|
||||||
if (!card || !viewport) return;
|
if (!card || !viewport) return;
|
||||||
var pad = 40;
|
var pad = 40;
|
||||||
var availW = window.innerWidth - pad;
|
var availW = window.innerWidth - pad;
|
||||||
var availH = window.innerHeight - pad;
|
var scaleByHeight = (window.innerHeight * 0.9) / H;
|
||||||
var scaleByHeight = (window.innerHeight * 0.9) / 1350;
|
var scaleByWidth = availW / W;
|
||||||
var scaleByWidth = availW / 1080;
|
var scale = Math.min(scaleByHeight, scaleByWidth, 1);
|
||||||
var scale = Math.min(scaleByHeight, scaleByWidth);
|
|
||||||
if (scale > 1) scale = 1; // nie ueber 100% hochskalieren
|
|
||||||
card.style.transform = 'scale(' + scale + ')';
|
card.style.transform = 'scale(' + scale + ')';
|
||||||
viewport.style.width = (1080 * scale) + 'px';
|
viewport.style.width = (W * scale) + 'px';
|
||||||
viewport.style.height = (1350 * scale) + 'px';
|
viewport.style.height = (H * scale) + 'px';
|
||||||
}
|
}
|
||||||
adjustScale();
|
adjustScale();
|
||||||
window.addEventListener('resize', adjustScale);
|
window.addEventListener('resize', adjustScale);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user