diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44b7356..5cef1bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased
+- Hardened EU AI Act guidance against the Commission Article 50 guidelines published on 20 July 2026.
+- Presets no longer infer human/editorial review; editorial responsibility is explicitly configurable and must include a responsible name when declared.
+- Added first-exposure, machine-readable provider-marking and emoji-only disclosure guardrails plus an EU AI Act operator checklist.
+- Added an optional square `emoji` SVG theme with dedicated symbols for no-AI, research, summarisation, full generation and article-level declarations.
+- Added generator, OpenAPI, documentation and accessibility support for the emoji theme.
- Added configurable Impressum, privacy and accessibility pages with visible placeholder warnings and strict production validation.
- Added secure defaults for client-IP logging, reverse-proxy trust, HSTS, CSP nonces and Prometheus access.
- Added legal/security deployment guidance and operator checklists.
diff --git a/README.md b/README.md
index 4be2c2b..35b0a63 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,8 @@ Weitere Variablen stehen vollständig in [`.env.example`](.env.example). Dazu ge
Die mitgelieferten Rechtstexte sind eine technisch abgestimmte Vorlage, keine individuelle Rechtsberatung. Die konkrete Einordnung hängt unter anderem von Betreiber, Hosting, Vertragsmodell, Zielgruppe, Zusatzdiensten und redaktionellen Inhalten ab. Siehe [`docs/LEGAL-AND-SECURITY.md`](docs/LEGAL-AND-SECURITY.md) und den [`Reviewbericht vom 20. Juli 2026`](docs/REVIEW-2026-07-20.md).
+Für die Abgrenzung zum EU AI Act und insbesondere zu Artikel 4 und Artikel 50 siehe außerdem [`docs/EU-AI-ACT-COMPLIANCE.md`](docs/EU-AI-ACT-COMPLIANCE.md). Das JSON-LD dieses Projekts ist ergänzende Dokumentation und kein automatischer Ersatz für eine Provider-Markierung nach Artikel 50 Absatz 2.
+
## API
```text
@@ -196,6 +198,24 @@ Beispiel für eine Artikelerklärung:
/declaration?mode=article&textExtent=none&textReview=none&imageExtent=full&imageReview=editorial&researchExtent=assisted&researchReview=expert&assurance=technicallyRecorded&lang=de
```
+### SVG-Darstellungen
+
+Der Query-Parameter `theme` unterstützt drei Darstellungen:
+
+| Wert | Ausgabe |
+|---|---|
+| `color` | klassisches zweifarbiges Text-Badge |
+| `mono` | monochromes Text-Badge |
+| `emoji` | quadratisches, rein grafisches SVG-Symbol mit zugänglichem Titel |
+
+Die Emoji-Variante verwendet je nach Preset ein Mensch-, Recherche-, Zusammenfassungs- oder Blitzsymbol. Beispiel:
+
+```text
+/badge/research.svg?theme=emoji&lang=de&link=auto
+```
+
+Bei `theme=emoji` werden `style=flat` und `style=flat-square` ignoriert, da die Ausgabe immer quadratisch ist. Mit der Pro-Funktion `custom_badge` steuert `leftColor` die Symbolfarbe und `rightColor` die Hintergrundfarbe.
+
### Nachweisgrundlage
diff --git a/docs/EU-AI-ACT-COMPLIANCE.md b/docs/EU-AI-ACT-COMPLIANCE.md
new file mode 100644
index 0000000..9556591
--- /dev/null
+++ b/docs/EU-AI-ACT-COMPLIANCE.md
@@ -0,0 +1,144 @@
+# EU AI Act: Compliance-Grenzen und Betreiber-Checkliste
+
+**Rechtsstand: 21. Juli 2026**
+
+Diese Datei beschreibt, welche Teile des EU AI Act (Verordnung (EU) 2024/1689) dieses Projekt technisch unterstützen kann und welche Pflichten außerhalb seines Funktionsumfangs liegen. Sie ersetzt keine Prüfung des konkreten Einsatzes.
+
+## 1. Was dieses Projekt ist
+
+Die mitgelieferte Anwendung erzeugt deterministisch SVG-Hinweise, Erklärungsseiten und JSON-LD aus den vom Nutzer eingegebenen Angaben. Sie enthält selbst kein KI-Modell und führt keine Modellinferenz aus.
+
+Der AI Act erfasst nur Systeme, die unter die Definition eines „AI system“ in Artikel 3 Absatz 1 fallen. Ob eine konkrete, veränderte oder erweiterte Installation diese Definition erfüllt, ist anhand der tatsächlichen technischen Funktion zu prüfen.
+
+Offizielle Quelle:
+
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-3
+
+## 2. Dieses Projekt ist keine vollständige AI-Act-Compliance-Lösung
+
+Der Generator unterstützt vor allem die **sichtbare und dokumentierte Offenlegung von KI-Nutzung**. Er kann insbesondere bei freiwilliger Transparenz und bei bestimmten Betreiberpflichten aus Artikel 50 Absatz 4 helfen.
+
+Er erfüllt oder prüft **nicht automatisch**:
+
+- die Anbieterpflicht zur technischen, maschinenlesbaren Markierung von generierten oder manipulierten Ausgaben nach Artikel 50 Absatz 2;
+- die Transparenzpflicht bei direkter Mensch-KI-Interaktion nach Artikel 50 Absatz 1;
+- Informationspflichten bei Emotionserkennung oder biometrischer Kategorisierung nach Artikel 50 Absatz 3;
+- die Pflicht zur AI Literacy nach Artikel 4;
+- Verbote nach Artikel 5;
+- die Einstufung und Pflichten für Hochrisiko-KI nach Kapitel III;
+- Pflichten für Anbieter von General-Purpose-AI-Modellen nach Kapitel V;
+- sonstige Pflichten aus Datenschutz-, Urheber-, Verbraucher-, Medien- oder Produktsicherheitsrecht.
+
+## 3. Artikel 50: sichtbare Offenlegung durch Betreiber
+
+Artikel 50 gilt ab **2. August 2026**. Betreiber bestimmter generativer KI-Systeme müssen insbesondere Deepfakes und bestimmte KI-generierte oder manipulierte Texte zu Angelegenheiten von öffentlichem Interesse offenlegen.
+
+Für solche sichtbaren Offenlegungen gilt nach Artikel 50 Absatz 5 und den Leitlinien der Kommission insbesondere:
+
+1. Der Hinweis muss **klar und unterscheidbar** sein.
+2. Er muss **spätestens bei der ersten Exposition** gegenüber der betroffenen natürlichen Person erscheinen.
+3. Er muss die geltenden Anforderungen an **Barrierefreiheit** berücksichtigen.
+4. Bei Deepfakes genügt eine nur maschinenlesbare Markierung des Anbieters nicht als sichtbare Betreiber-Offenlegung.
+
+Daher gilt für die Einbettung dieses Projekts:
+
+- Badge oder Klartext unmittelbar am betroffenen Inhalt platzieren;
+- nicht ausschließlich im Footer, Impressum oder auf einer erst später erreichbaren Unterseite;
+- aussagekräftigen Alternativtext verwenden;
+- bei der quadratischen Emoji-Variante zusätzlich einen unmittelbar verständlichen Text oder eine gleichwertig eindeutige sichtbare Kennzeichnung verwenden.
+
+Offizielle Quellen:
+
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-50
+- https://digital-strategy.ec.europa.eu/en/faqs/transparency-obligations-under-article-50-ai-act
+- https://digital-strategy.ec.europa.eu/en/policies/guidelines-transparency-ai-generated-content
+
+## 4. Menschliche Prüfung und redaktionelle Kontrolle
+
+Für bestimmte veröffentlichte KI-generierte oder manipulierte Texte zu Angelegenheiten von öffentlichem Interesse kann die Kennzeichnungspflicht nach Artikel 50 Absatz 4 entfallen, wenn die gesetzlichen Voraussetzungen tatsächlich erfüllt sind.
+
+Die Kommission stellt klar:
+
+- „human review“ erfordert eine bewusste inhaltliche Prüfung durch Personen mit einschlägigem Wissen und professionellem Urteil;
+- „editorial control“ setzt tatsächliche inhaltliche Entscheidungsbefugnis voraus, also insbesondere die Möglichkeit, Inhalte aus sachlichen Gründen zu genehmigen, zu ändern oder abzulehnen;
+- rein oberflächliche, formale oder prozedurale Prüfungen wie Rechtschreib- oder Grammatikprüfung genügen nicht;
+- zusätzlich muss eine natürliche oder juristische Person die redaktionelle Verantwortung für die Veröffentlichung tragen.
+
+Aus diesem Grund setzt der Generator seit dieser Härtung **keine menschliche oder redaktionelle Prüfung mehr automatisch voraus**. Der Nutzer muss den tatsächlich durchgeführten Prozess ausdrücklich auswählen.
+
+Die Felder `editorialResponsibility.name` und `editorialResponsibility.url` dienen nur der Dokumentation. Ihre Angabe beweist nicht automatisch, dass die gesetzlichen Voraussetzungen vorliegen.
+
+## 5. Artikel 50 Absatz 2: JSON-LD ist kein Ersatz für Provider-Marking
+
+Anbieter generativer KI-Systeme müssen in den erfassten Fällen dafür sorgen, dass generierte oder manipulierte Inhalte maschinenlesbar markiert und als künstlich generiert oder manipuliert erkennbar sind. Nach Artikel 50 Absatz 2 müssen die technischen Lösungen wirksam, interoperabel, robust und zuverlässig sein, soweit dies technisch machbar ist.
+
+Das von diesem Projekt erzeugte **JSON-LD ist ergänzende Dokumentation**. Es ist nicht als Ersatz für die technische Markierung gedacht, die der Anbieter des generativen KI-Systems in oder an der Ausgabe implementieren muss.
+
+Wer selbst Anbieter eines generativen KI-Systems im Sinne des AI Act ist, muss deshalb zusätzlich eine geeignete Provider-Marking-Lösung umsetzen und dokumentieren. Der von der Kommission positiv bewertete Code of Practice kann hierfür als freiwilliger Compliance-Rahmen genutzt werden.
+
+Offizielle Quellen:
+
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-50
+- https://digital-strategy.ec.europa.eu/en/policies/code-practice-ai-generated-content
+- https://digital-strategy.ec.europa.eu/en/library/commission-opinion-assessment-code-practice-transparency-ai-generated-content
+
+## 6. Artikel 4: AI Literacy
+
+Artikel 4 gilt bereits seit **2. Februar 2025**. Anbieter und Betreiber von KI-Systemen müssen nach besten Kräften Maßnahmen treffen, um ein ausreichendes Maß an AI Literacy bei Mitarbeitenden und sonstigen Personen sicherzustellen, die in ihrem Auftrag mit Betrieb oder Nutzung von KI-Systemen befasst sind.
+
+Ein Badge oder eine Veröffentlichungserklärung erfüllt diese Organisationspflicht nicht. Betreiber sollten mindestens dokumentieren:
+
+- welche KI-Systeme eingesetzt werden;
+- welche Rollen sie bedienen;
+- welche Kenntnisse und Schulungen erforderlich sind;
+- welche internen Regeln für Prüfung, Freigabe und Eskalation gelten;
+- wann Schulungen oder Richtlinien zuletzt aktualisiert wurden.
+
+Offizielle Quelle:
+
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-4
+
+## 7. Risiko- und Rollenprüfung vor Produktivnutzung
+
+Vor dem Einsatz sollte die Organisation dokumentieren:
+
+1. Ist die eingesetzte Software überhaupt ein KI-System im Sinne von Artikel 3 Absatz 1?
+2. Welche Rolle liegt vor: Anbieter, Betreiber, Importeur, Distributor oder Produkthersteller?
+3. Fällt das konkrete KI-System unter ein Verbot des Artikels 5?
+4. Ist es ein Hochrisiko-KI-System nach Artikel 6 bzw. Anhang III?
+5. Handelt es sich um ein General-Purpose-AI-Modell oder ein darauf basierendes System mit zusätzlichen Pflichten?
+6. Greift eine Transparenzpflicht aus Artikel 50?
+7. Welche anderen Rechtsgebiete sind zusätzlich betroffen, insbesondere DSGVO, Urheberrecht, Verbraucher- und Medienrecht?
+
+Offizielle Einstiegsquellen:
+
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-2
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-5
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-6
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/annex-3
+
+## 8. Zeitlicher Stand
+
+Nach Artikel 113 gilt der AI Act grundsätzlich ab 2. August 2026; einzelne Teile gelten bereits früher. Insbesondere gelten Kapitel I und II, einschließlich Artikel 4 und der Verbote des Artikels 5, seit 2. Februar 2025.
+
+Für Artikel 50 beginnen die Transparenzpflichten am 2. August 2026. Die Kommission weist für bestimmte bereits vor diesem Datum in Verkehr gebrachte generative KI-Systeme auf eine begrenzte Übergangsregel für die Anbieter-Markierung nach Artikel 50 Absatz 2 bis 2. Dezember 2026 hin.
+
+Offizielle Quellen:
+
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-113
+- https://ai-act-service-desk.ec.europa.eu/en/ai-act/timeline/timeline-implementation-eu-ai-act
+- https://digital-strategy.ec.europa.eu/en/faqs/transparency-obligations-under-article-50-ai-act
+
+## 9. Praktische Mindestregel für dieses Projekt
+
+Wenn ein Inhalt **möglicherweise** unter Artikel 50 Absatz 4 fällt, ist die rechtlich defensivere Konfiguration:
+
+- KI-Nutzung wahrheitsgemäß als `partial`, `mostly` oder `full` angeben;
+- die konkrete Tätigkeit angeben;
+- `humanReview` nur dann auf `editorial` oder `expert` setzen, wenn eine tatsächlich substanzielle inhaltliche Prüfung stattgefunden hat;
+- die redaktionell verantwortliche Person oder Organisation dokumentieren, wenn dies zutrifft;
+- einen sichtbaren textlichen Hinweis oder ein verständliches Standard-Badge spätestens bei der ersten Exposition platzieren;
+- das Emoji nicht als alleinige gesetzliche Kennzeichnung verwenden;
+- das JSON-LD nur als zusätzliche Dokumentation behandeln.
+
+Bei Zweifeln über die Ausnahme wegen menschlicher Prüfung ist eine **zusätzliche Offenlegung regelmäßig die risikoärmere technische Entscheidung**, solange dadurch keine anderen rechtlichen Pflichten verletzt werden.
diff --git a/docs/LEGAL-AND-SECURITY.md b/docs/LEGAL-AND-SECURITY.md
index a12a5aa..1d40052 100644
--- a/docs/LEGAL-AND-SECURITY.md
+++ b/docs/LEGAL-AND-SECURITY.md
@@ -127,10 +127,22 @@ METRICS_TOKEN_FILE=/run/secrets/metrics-token
Der Bearer-Token schützt den Endpunkt auf Anwendungsebene. Zusätzlich sollte der Endpunkt nicht über den öffentlichen Ingress erreichbar sein.
-## 8. KI-Transparenz
+## 8. KI-Transparenz und EU AI Act
-Der Standard unterstützt freiwillige Dokumentation und bestimmte Transparenz-Workflows. Er entscheidet nicht automatisch, ob Artikel 50 des AI Act auf einen konkreten Inhalt anwendbar ist. Die offiziellen Transparenzleitlinien der Europäischen Kommission wurden am 20. Juli 2026 veröffentlicht; Artikel 50 gilt ab dem 2. August 2026.
+Der Standard unterstützt freiwillige Dokumentation und bestimmte Transparenz-Workflows, insbesondere sichtbare Offenlegungen. Er entscheidet nicht automatisch, ob Artikel 50 des AI Act auf einen konkreten Inhalt anwendbar ist. Die offiziellen Transparenzleitlinien der Europäischen Kommission wurden am 20. Juli 2026 veröffentlicht; Artikel 50 gilt ab dem 2. August 2026.
+
+Wichtige Grenzen:
+
+- Presets unterstellen keine menschliche oder redaktionelle Prüfung mehr. Ein solcher Prozess muss ausdrücklich angegeben werden.
+- Eine bloß formale Prüfung wie Rechtschreibung oder Grammatik ist nach den Kommissionsleitlinien keine substantielle menschliche Prüfung oder redaktionelle Kontrolle im Sinne der Ausnahme für bestimmte Texte nach Artikel 50 Absatz 4.
+- Ein sichtbarer Hinweis muss bei einer einschlägigen Pflicht klar und unterscheidbar spätestens bei der ersten Exposition erscheinen.
+- Die Emoji-Variante ist nur ein kompakter Zusatz und sollte bei einer gesetzlichen Kennzeichnung nicht ohne verständlichen Text eingesetzt werden.
+- Das erzeugte JSON-LD ist ergänzende Dokumentation und kein automatischer Ersatz für die Provider-Markierung nach Artikel 50 Absatz 2.
+- Der AI Act enthält weitere Pflichten außerhalb dieses Projekts, unter anderem AI Literacy nach Artikel 4 sowie gegebenenfalls Verbote, Hochrisiko- oder GPAI-Pflichten.
+
+Siehe die ausführliche Checkliste in [`EU-AI-ACT-COMPLIANCE.md`](EU-AI-ACT-COMPLIANCE.md).
- AI Act: https://eur-lex.europa.eu/eli/reg/2024/1689/oj
- Leitlinien: https://digital-strategy.ec.europa.eu/en/library/guidelines-transparency-obligations-providers-and-deployers-ai-systems
- FAQ: https://digital-strategy.ec.europa.eu/en/faqs/transparency-obligations-under-article-50-ai-act
+- Code of Practice: https://digital-strategy.ec.europa.eu/en/policies/code-practice-ai-generated-content
diff --git a/internal/app/server.go b/internal/app/server.go
index 565960d..a02c81b 100644
--- a/internal/app/server.go
+++ b/internal/app/server.go
@@ -344,7 +344,7 @@ func (s *Server) renderBadge(w http.ResponseWriter, r *http.Request, q url.Value
}
link = s.cfg.BaseURL + "/declaration?" + copy.Encode()
}
- data, etag := badge.Render(badge.Options{Label: label, Message: message, Extent: extent, Style: q.Get("style"), Theme: q.Get("theme"), Link: link, LeftColor: leftColor, RightColor: rightColor})
+ data, etag := badge.Render(badge.Options{Label: label, Message: message, Extent: extent, Preset: presetID, Article: isArticle, Style: q.Get("style"), Theme: q.Get("theme"), Link: link, LeftColor: leftColor, RightColor: rightColor})
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
diff --git a/internal/app/server_test.go b/internal/app/server_test.go
index a084200..4556d6c 100644
--- a/internal/app/server_test.go
+++ b/internal/app/server_test.go
@@ -249,6 +249,47 @@ func TestSignedAssuranceExplainsScope(t *testing.T) {
}
}
+func TestGeneratorShowsAIACTGuardrails(t *testing.T) {
+ r := httptest.NewRequest(http.MethodGet, "/?lang=de", nil)
+ w := httptest.NewRecorder()
+ testHandler(t).ServeHTTP(w, r)
+ if w.Code != http.StatusOK {
+ t.Fatalf("status %d: %s", w.Code, w.Body.String())
+ }
+ body := w.Body.String()
+ for _, expected := range []string{
+ `id="responsible"`,
+ `id="responsible-url"`,
+ "formale Prüfung",
+ "Platzierung nach Art. 50",
+ "erfüllt aber nicht automatisch die Anbieterpflicht",
+ "Emoji-Variante",
+ } {
+ if !strings.Contains(body, expected) {
+ t.Fatalf("generator missing AI Act guardrail %q", expected)
+ }
+ }
+}
+
+func TestDeclarationExplainsReviewAndMachineMarkingLimits(t *testing.T) {
+ r := httptest.NewRequest(http.MethodGet, "/declaration?preset=full&review=editorial&lang=de&responsible=Beispielredaktion&responsibleUrl=https%3A%2F%2Fexample.org%2Fimpressum", nil)
+ w := httptest.NewRecorder()
+ testHandler(t).ServeHTTP(w, r)
+ if w.Code != http.StatusOK {
+ t.Fatalf("status %d: %s", w.Code, w.Body.String())
+ }
+ body := w.Body.String()
+ for _, expected := range []string{
+ "Beispielredaktion",
+ "begründet für sich allein keine Ausnahme",
+ "wirksame, zuverlässige, robuste und interoperable maschinenlesbare Markierung",
+ } {
+ if !strings.Contains(body, expected) {
+ t.Fatalf("declaration missing AI Act guardrail %q", expected)
+ }
+ }
+}
+
func TestBackgroundPageGerman(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/background?lang=de", nil)
w := httptest.NewRecorder()
@@ -457,3 +498,37 @@ func TestTrustProxyRequiresExplicitCIDRs(t *testing.T) {
t.Fatalf("expected trusted proxy validation error, got %v", err)
}
}
+
+func TestEmojiThemeBadgeEndpoint(t *testing.T) {
+ r := httptest.NewRequest(http.MethodGet, "/badge/summary.svg?lang=de&theme=emoji", nil)
+ w := httptest.NewRecorder()
+ testHandler(t).ServeHTTP(w, r)
+ if w.Code != http.StatusOK {
+ t.Fatalf("status %d: %s", w.Code, w.Body.String())
+ }
+ body := w.Body.String()
+ for _, expected := range []string{
+ `width="64" height="64"`,
+ `data-theme="emoji"`,
+ `data-icon="summary"`,
+ `fill="#b7791f"`,
+ `Inhaltliche Zusammenfassung`,
+ } {
+ if !strings.Contains(body, expected) {
+ t.Fatalf("emoji badge missing %q: %s", expected, body)
+ }
+ }
+}
+
+func TestGeneratorOffersEmojiTheme(t *testing.T) {
+ r := httptest.NewRequest(http.MethodGet, "/?lang=de", nil)
+ w := httptest.NewRecorder()
+ testHandler(t).ServeHTTP(w, r)
+ if w.Code != http.StatusOK {
+ t.Fatalf("status %d: %s", w.Code, w.Body.String())
+ }
+ const expected = ``
+ if !strings.Contains(w.Body.String(), expected) {
+ t.Fatalf("generator missing %q", expected)
+ }
+}
diff --git a/internal/background/content.go b/internal/background/content.go
index d93a891..df880de 100644
--- a/internal/background/content.go
+++ b/internal/background/content.go
@@ -176,13 +176,14 @@ func german() Page {
Eyebrow: "Rechtlicher und redaktioneller Hintergrund",
Title: "Warum KI-Nutzung gekennzeichnet wird",
Lead: "Der EU AI Act führt ab August 2026 konkrete Transparenzpflichten für bestimmte KI-Systeme und Inhalte ein. Dieses Projekt unterstützt darüber hinaus eine freiwillige, verständliche und maschinenlesbare Dokumentation der KI-Nutzung in Artikeln, Webseiten und Medien.",
- StatusKicker: "Aktueller Stand · 20. Juli 2026", StatusTitle: "Artikel 50 gilt ab 2. August 2026", StatusText: "Die Transparenzpflichten betreffen nicht pauschal jede Nutzung von KI. Entscheidend sind unter anderem die Rolle als Anbieter oder Betreiber, die Art des Systems, der Inhalt, der Veröffentlichungszweck und die vorhandene menschliche Prüfung.",
+ StatusKicker: "Rechtsstand · 21. Juli 2026", StatusTitle: "Artikel 50 gilt ab 2. August 2026", StatusText: "Die Transparenzpflichten betreffen nicht pauschal jede Nutzung von KI. Entscheidend sind unter anderem die Rolle als Anbieter oder Betreiber, die Art des Systems, der Inhalt, der Veröffentlichungszweck und die vorhandene menschliche Prüfung.",
ContentsTitle: "Auf dieser Seite", Contents: []string{"Einordnung des EU AI Act", "Pflichten aus Artikel 50", "Bedeutung für Artikel und Webseiten", "Anforderungen an die Kennzeichnung", "Rolle dieses Standards", "Offizielle Quellen"},
OverviewTitle: "Einordnung: keine allgemeine Kennzeichnungspflicht für jede KI-Hilfe",
OverviewParagraphs: []string{
"Der AI Act ist die Verordnung (EU) 2024/1689. Er verfolgt einen risikobasierten Ansatz und enthält neben Regeln für verbotene oder hochriskante Systeme auch besondere Transparenzpflichten. Für die Kennzeichnung von KI-generierten oder manipulierten Inhalten ist vor allem Artikel 50 relevant.",
"Artikel 50 unterscheidet zwischen Anbietern von KI-Systemen und Personen oder Organisationen, die solche Systeme einsetzen. Ein Anbieter muss bestimmte Systeme technisch so gestalten, dass Interaktionen und synthetische Ausgaben erkennbar werden. Betreiber oder veröffentlichende Stellen müssen in bestimmten Fällen Menschen sichtbar informieren, etwa bei Deepfakes oder bestimmten Texten zu Angelegenheiten von öffentlichem Interesse.",
"Nicht jede Recherchehilfe, Rechtschreibkorrektur, Übersetzung oder redaktionelle Unterstützung löst automatisch dieselbe Pflicht aus. Die konkrete Bewertung hängt vom Anwendungsfall ab. Eine freiwillige Offenlegung kann dennoch sinnvoll sein, weil sie Erwartungen klärt, redaktionelle Verantwortung sichtbar macht und über das gesetzliche Mindestmaß hinaus Vertrauen schafft.",
+ "Dieses Projekt bildet nicht den gesamten AI Act ab. Andere Pflichten wie AI Literacy nach Artikel 4, Verbote nach Artikel 5, Hochrisiko-Anforderungen oder Pflichten für General-Purpose-AI-Modelle müssen unabhängig davon geprüft und erfüllt werden.",
},
ScopeTitle: "Was Artikel 50 konkret regelt", ScopeLead: "Die Vorschrift enthält mehrere eigenständige Transparenzpflichten. Für Webseiten und redaktionelle Inhalte sind insbesondere die technische Markierung durch Anbieter sowie die sichtbare Offenlegung durch Betreiber relevant.",
Obligations: []Card{
@@ -207,8 +208,9 @@ func german() Page {
},
ProjectTitle: "Wie dieser Standard einzuordnen ist",
ProjectParagraphs: []string{
- "AI Usage Disclosure ist ein technisches und redaktionelles Werkzeug. Es ermöglicht, die Nutzung von KI nach Inhaltsbestandteilen zu dokumentieren: beispielsweise Text ohne KI, Titelbild KI-generiert, Recherche KI-unterstützt und fachliche Prüfung durch Menschen. Eine einzelne URL bündelt die sichtbare Erklärung, die tabellarischen Details und ein maschinenlesbares JSON-LD-Manifest.",
+ "AI Usage Disclosure ist ein technisches und redaktionelles Werkzeug. Es ermöglicht, die Nutzung von KI nach Inhaltsbestandteilen zu dokumentieren: beispielsweise Text ohne KI, Titelbild KI-generiert, Recherche KI-unterstützt und – nur wenn tatsächlich erfolgt – eine menschliche Prüfung. Eine einzelne URL bündelt die sichtbare Erklärung, die tabellarischen Details und ein maschinenlesbares JSON-LD-Manifest.",
"Der Standard ist bewusst weiter gefasst als die gesetzlichen Mindestfälle. Organisationen können damit auch freiwillige Transparenz veröffentlichen, etwa für Rechercheunterstützung oder generierte Illustrationen, die kein Deepfake darstellen. Gleichzeitig entscheidet das Programm nicht, ob im konkreten Einzelfall eine gesetzliche Pflicht besteht.",
+ "Das JSON-LD ist ergänzende Dokumentation. Es ersetzt nicht automatisch die wirksame, interoperable, robuste und zuverlässige technische Markierung, die Anbieter generativer KI-Systeme in den von Artikel 50 Absatz 2 erfassten Fällen gewährleisten müssen.",
},
ProjectItems: []string{
"Komponentenbezogene Angaben für Text, Bilder, Recherche, Übersetzung, Audio, Video und Code",
@@ -229,13 +231,14 @@ func english() Page {
MetaDescription: "Background on AI labelling, Article 50 of the EU AI Act and voluntary documentation of AI use in digital content.",
NavProduct: "Features", NavGenerator: "Generator", NavBackground: "Background", LanguageLabel: "Language",
Eyebrow: "Legal and editorial background", Title: "Why AI use is disclosed", Lead: "From August 2026, the EU AI Act introduces specific transparency obligations for certain AI systems and content. This project additionally supports voluntary, understandable and machine-readable documentation of AI use in articles, websites and media.",
- StatusKicker: "Current as of 20 July 2026", StatusTitle: "Article 50 applies from 2 August 2026", StatusText: "The transparency obligations do not apply indiscriminately to every use of AI. Relevant factors include the role of provider or deployer, the type of system, the content, the purpose of publication and the human review actually performed.",
+ StatusKicker: "Legal status · 21 July 2026", StatusTitle: "Article 50 applies from 2 August 2026", StatusText: "The transparency obligations do not apply indiscriminately to every use of AI. Relevant factors include the role of provider or deployer, the type of system, the content, the purpose of publication and the human review actually performed.",
ContentsTitle: "On this page", Contents: []string{"Context of the EU AI Act", "Obligations under Article 50", "Meaning for articles and websites", "Requirements for disclosure", "Role of this standard", "Official sources"},
OverviewTitle: "Context: there is no blanket labelling duty for every AI-assisted task",
OverviewParagraphs: []string{
"The AI Act is Regulation (EU) 2024/1689. It follows a risk-based approach and, alongside rules for prohibited and high-risk systems, contains specific transparency obligations. Article 50 is particularly relevant to the identification of AI-generated or manipulated content.",
"Article 50 distinguishes between providers of AI systems and persons or organisations deploying them. Providers must design certain systems so that interactions and synthetic outputs can be identified. Deployers and publishers must visibly inform people in particular situations, including deepfakes and certain text concerning matters of public interest.",
"Not every research aid, spelling correction, translation or editorial assistance automatically triggers the same duty. The assessment depends on the circumstances. Voluntary disclosure may nevertheless be valuable because it sets expectations, makes editorial responsibility visible and can build trust beyond the legal minimum.",
+ "This project does not cover the entire AI Act. Other obligations, including AI literacy under Article 4, prohibited practices under Article 5, high-risk requirements and obligations for general-purpose AI models, must be assessed and met independently where applicable.",
},
ScopeTitle: "What Article 50 covers", ScopeLead: "The provision contains several distinct transparency obligations. For websites and editorial content, provider-side technical marking and deployer-side visible disclosure are especially relevant.",
Obligations: []Card{
diff --git a/internal/badge/render.go b/internal/badge/render.go
index 69f67c9..3a91ba1 100644
--- a/internal/badge/render.go
+++ b/internal/badge/render.go
@@ -13,6 +13,8 @@ type Options struct {
Label string
Message string
Extent string
+ Preset string
+ Article bool
Style string
Theme string
Link string
@@ -24,6 +26,10 @@ var extentColors = map[string]string{
"none": "#2f855a", "assisted": "#2b6cb0", "partial": "#b7791f", "mostly": "#c05621", "full": "#c53030",
}
+var emojiPresetColors = map[string]string{
+ "no-ai": "#2f855a", "research": "#2b6cb0", "summary": "#b7791f", "full": "#c53030",
+}
+
func Render(o Options) ([]byte, string) {
label := truncate(strings.TrimSpace(o.Label), 40)
if label == "" {
@@ -33,6 +39,10 @@ func Render(o Options) ([]byte, string) {
if message == "" {
message = o.Extent
}
+ if o.Theme == "emoji" {
+ return renderEmoji(o, label, message)
+ }
+
style := o.Style
if style != "flat-square" {
style = "flat"
@@ -63,17 +73,100 @@ func Render(o Options) ([]byte, string) {
title := html.EscapeString(label + ": " + message)
labelEsc := html.EscapeString(label)
messageEsc := html.EscapeString(message)
-
- openLink, closeLink := "", ""
- if strings.HasPrefix(o.Link, "https://") || strings.HasPrefix(o.Link, "http://") {
- openLink = ``
- closeLink = ``
- }
+ openLink, closeLink := svgLink(o.Link)
svg := fmt.Sprintf(``,
title, total, total, title, openLink, total, radius, leftWidth, leftColor, leftWidth, rightWidth, rightColor, total,
leftWidth/2, labelEsc, leftWidth/2, labelEsc, leftWidth+rightWidth/2, messageEsc, leftWidth+rightWidth/2, messageEsc, closeLink)
- data := []byte(svg)
+ return withETag([]byte(svg))
+}
+
+func renderEmoji(o Options, label, message string) ([]byte, string) {
+ iconName := emojiIconName(o)
+ background := emojiPresetColors[o.Preset]
+ if o.Article {
+ background = "#7c3aed"
+ }
+ if background == "" {
+ background = extentColors[o.Extent]
+ }
+ if background == "" {
+ background = "#4a5568"
+ }
+ foreground := "#ffffff"
+ if isHexColor(o.LeftColor) {
+ foreground = o.LeftColor
+ }
+ if isHexColor(o.RightColor) {
+ background = o.RightColor
+ }
+
+ title := html.EscapeString(label + ": " + message)
+ openLink, closeLink := svgLink(o.Link)
+ icon := emojiIcon(iconName, foreground)
+ svg := fmt.Sprintf(``,
+ title, iconName, title, openLink, background, icon, closeLink)
+ return withETag([]byte(svg))
+}
+
+func emojiIconName(o Options) string {
+ if o.Article {
+ return "article"
+ }
+ switch o.Preset {
+ case "no-ai":
+ return "human"
+ case "research":
+ return "research"
+ case "summary":
+ return "summary"
+ case "full":
+ return "bolt"
+ }
+ switch o.Extent {
+ case "none":
+ return "human"
+ case "partial":
+ return "partial"
+ case "mostly":
+ return "mostly"
+ case "full":
+ return "bolt"
+ default:
+ return "assisted"
+ }
+}
+
+func emojiIcon(name, color string) string {
+ stroke := ` fill="none" stroke="` + color + `" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"`
+ switch name {
+ case "human":
+ return ``
+ case "research":
+ return ``
+ case "summary":
+ return ``
+ case "bolt":
+ return ``
+ case "article":
+ return ``
+ case "partial":
+ return ``
+ case "mostly":
+ return ``
+ default:
+ return ``
+ }
+}
+
+func svgLink(link string) (string, string) {
+ if strings.HasPrefix(link, "https://") || strings.HasPrefix(link, "http://") {
+ return ``, ``
+ }
+ return "", ""
+}
+
+func withETag(data []byte) ([]byte, string) {
hash := sha256.Sum256(data)
return data, `"` + hex.EncodeToString(hash[:12]) + `"`
}
diff --git a/internal/badge/render_test.go b/internal/badge/render_test.go
index b9aa701..1394c7d 100644
--- a/internal/badge/render_test.go
+++ b/internal/badge/render_test.go
@@ -2,6 +2,7 @@ package badge
import (
"bytes"
+ "strings"
"testing"
)
@@ -17,3 +18,77 @@ func TestRenderEscapesText(t *testing.T) {
t.Fatal("missing etag")
}
}
+
+func TestRenderEmojiIsSquareAndPresetSpecific(t *testing.T) {
+ data, etag := Render(Options{
+ Label: "KI-Nutzung",
+ Message: "Vollständige Inhaltserstellung",
+ Extent: "full",
+ Preset: "full",
+ Theme: "emoji",
+ })
+ body := string(data)
+ for _, expected := range []string{
+ `width="64" height="64"`,
+ `viewBox="0 0 64 64"`,
+ `data-theme="emoji"`,
+ `data-icon="bolt"`,
+ `fill="#c53030"`,
+ `
KI-Nutzung: Vollständige Inhaltserstellung`,
+ } {
+ if !bytes.Contains(data, []byte(expected)) {
+ t.Fatalf("emoji SVG missing %q: %s", expected, body)
+ }
+ }
+ if etag == "" {
+ t.Fatal("missing etag")
+ }
+}
+
+func TestRenderEmojiUsesCustomColoursAndEscapesAccessibleText(t *testing.T) {
+ data, _ := Render(Options{
+ Label: ``,
+ Message: `Research & review`,
+ Extent: "assisted",
+ Preset: "research",
+ Theme: "emoji",
+ LeftColor: "#112233",
+ RightColor: "#abcdef",
+ })
+ body := string(data)
+ for _, expected := range []string{
+ `data-icon="research"`,
+ `fill="#abcdef"`,
+ `stroke="#112233"`,
+ `<AI>: Research & review`,
+ } {
+ if !strings.Contains(body, expected) {
+ t.Fatalf("emoji SVG missing %q: %s", expected, body)
+ }
+ }
+ if strings.Contains(body, ``) {
+ t.Fatal("accessible text was not escaped")
+ }
+}
+
+func TestEmojiIconMapping(t *testing.T) {
+ tests := []struct {
+ name string
+ options Options
+ icon string
+ }{
+ {name: "no AI", options: Options{Extent: "none", Preset: "no-ai", Theme: "emoji"}, icon: "human"},
+ {name: "research", options: Options{Extent: "assisted", Preset: "research", Theme: "emoji"}, icon: "research"},
+ {name: "summary", options: Options{Extent: "assisted", Preset: "summary", Theme: "emoji"}, icon: "summary"},
+ {name: "full", options: Options{Extent: "full", Preset: "full", Theme: "emoji"}, icon: "bolt"},
+ {name: "article", options: Options{Extent: "mostly", Article: true, Theme: "emoji"}, icon: "article"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ data, _ := Render(tt.options)
+ if !bytes.Contains(data, []byte(`data-icon="`+tt.icon+`"`)) {
+ t.Fatalf("expected icon %q: %s", tt.icon, data)
+ }
+ })
+ }
+}
diff --git a/internal/declaration/model.go b/internal/declaration/model.go
index ef7cbc8..aa3cc46 100644
--- a/internal/declaration/model.go
+++ b/internal/declaration/model.go
@@ -71,10 +71,14 @@ type ParseOptions struct {
}
var Presets = map[string]Preset{
- "no-ai": {ID: "no-ai", Extent: "none", Review: "editorial"},
- "research": {ID: "research", Extent: "assisted", Activities: []string{"research"}, Review: "editorial"},
- "summary": {ID: "summary", Extent: "assisted", Activities: []string{"summarisation"}, Review: "editorial"},
- "full": {ID: "full", Extent: "full", Activities: []string{"generation"}, Review: "editorial"},
+ // Human review is deliberately not presumed by presets. Under Article 50(4)
+ // AI Act, substantive human review/editorial control can affect whether
+ // certain public-interest text must be labelled. The factual review process
+ // must therefore be selected explicitly by the publisher.
+ "no-ai": {ID: "no-ai", Extent: "none", Review: "none"},
+ "research": {ID: "research", Extent: "assisted", Activities: []string{"research"}, Review: "none"},
+ "summary": {ID: "summary", Extent: "assisted", Activities: []string{"summarisation"}, Review: "none"},
+ "full": {ID: "full", Extent: "full", Activities: []string{"generation"}, Review: "none"},
}
var validExtents = map[string]bool{"none": true, "assisted": true, "partial": true, "mostly": true, "full": true}
@@ -134,7 +138,8 @@ func NewFromQueryWithOptions(values url.Values, contextURL string, options Parse
extent = "assisted"
}
if review == "" {
- review = "editorial"
+ // Do not infer a legally relevant human-review process from AI usage alone.
+ review = "none"
}
assurance := clean(values.Get("assurance"), 32)
if assurance == "" {
@@ -150,11 +155,8 @@ func NewFromQueryWithOptions(values url.Values, contextURL string, options Parse
}
componentReview := clean(values.Get(name+"Review"), 24)
if componentReview == "" {
- if componentExtent == "none" {
- componentReview = "none"
- } else {
- componentReview = "editorial"
- }
+ // Human review must be stated explicitly; never infer editorial control.
+ componentReview = "none"
}
componentActivities := splitCSV(values.Get(name + "Activities"))
if len(componentActivities) == 0 {
@@ -252,9 +254,14 @@ func Validate(d Declaration) error {
problems = append(problems, "declaredAt must be RFC3339")
}
}
- if d.EditorialResponsibility != nil && d.EditorialResponsibility.URL != "" {
- if u, err := url.ParseRequestURI(d.EditorialResponsibility.URL); err != nil || u.Scheme == "" || u.Host == "" {
- problems = append(problems, "editorialResponsibility.url must be an absolute URL")
+ if d.EditorialResponsibility != nil {
+ if strings.TrimSpace(d.EditorialResponsibility.Name) == "" {
+ problems = append(problems, "editorialResponsibility.name is required when editorialResponsibility is present")
+ }
+ if d.EditorialResponsibility.URL != "" {
+ if u, err := url.ParseRequestURI(d.EditorialResponsibility.URL); err != nil || u.Scheme == "" || u.Host == "" {
+ problems = append(problems, "editorialResponsibility.url must be an absolute URL")
+ }
}
}
for name, c := range d.Components {
diff --git a/internal/declaration/model_test.go b/internal/declaration/model_test.go
index 3976a8d..4cf8473 100644
--- a/internal/declaration/model_test.go
+++ b/internal/declaration/model_test.go
@@ -3,6 +3,7 @@ package declaration
import (
"errors"
"net/url"
+ "strings"
"testing"
)
@@ -19,6 +20,9 @@ func TestPresetQuery(t *testing.T) {
if d.Language != "fr" {
t.Fatalf("unexpected language: %s", d.Language)
}
+ if c.HumanReview != "none" {
+ t.Fatalf("preset inferred human review=%q", c.HumanReview)
+ }
}
func TestNoAIRejectsActivities(t *testing.T) {
@@ -72,8 +76,8 @@ func TestArticleDeclarationFromQuery(t *testing.T) {
if d.Components["text"].HumanReview != "none" {
t.Fatalf("no-AI component review=%q, want none", d.Components["text"].HumanReview)
}
- if d.Components["research"].HumanReview != "editorial" {
- t.Fatalf("AI-assisted component review=%q, want editorial default", d.Components["research"].HumanReview)
+ if d.Components["research"].HumanReview != "none" {
+ t.Fatalf("AI-assisted component review=%q, want no inferred review", d.Components["research"].HumanReview)
}
if got := d.Components["research"].Activities; len(got) != 1 || got[0] != "research" {
t.Fatalf("research activities=%v", got)
@@ -112,3 +116,13 @@ func TestAssuranceFromQuery(t *testing.T) {
}
}
}
+
+func TestEditorialResponsibilityRequiresName(t *testing.T) {
+ _, err := NewFromQuery(url.Values{
+ "preset": {"research"},
+ "responsibleUrl": {"https://example.org/impressum"},
+ }, "https://example.org/context")
+ if err == nil || !strings.Contains(err.Error(), "editorialResponsibility.name is required") {
+ t.Fatalf("expected editorial responsibility name validation error, got %v", err)
+ }
+}
diff --git a/internal/i18n/catalog.go b/internal/i18n/catalog.go
index b24604b..2fa61f0 100644
--- a/internal/i18n/catalog.go
+++ b/internal/i18n/catalog.go
@@ -36,7 +36,8 @@ var catalogs = map[string]Locale{
"generator_eyebrow": "Generator", "generator_title": "Einbettung erzeugen", "generator_intro": "Wähle eine Vorlage oder erfasse die KI-Nutzung strukturiert nach Inhaltsbestandteilen. Vorschau, Einbettungscode und maschinenlesbare Erklärung werden unmittelbar erzeugt.",
"field_preset": "Preset", "option_custom": "Benutzerdefinierte Struktur", "field_component": "Komponente", "field_extent": "KI-Anteil", "field_review": "Menschliche Prüfung",
"field_activities": "Tätigkeiten", "activities_help": "Kommagetrennte Standardwerte, zum Beispiel research, summarisation oder translation.",
- "field_subject": "URL des gekennzeichneten Inhalts", "field_language": "Ausgabesprache", "field_theme": "Darstellung", "theme_mono": "Monochrom", "theme_color": "Farbig",
+ "review_help": "Nur den tatsächlich durchgeführten Prüfprozess angeben. Eine formale Prüfung (z. B. Rechtschreibung) ist keine substanzielle menschliche Prüfung oder redaktionelle Kontrolle im Sinne von Art. 50 Abs. 4 AI Act.",
+ "field_subject": "URL des gekennzeichneten Inhalts", "field_responsible": "Redaktionell verantwortliche Person/Organisation (optional)", "responsible_placeholder": "Name oder Organisation", "field_responsible_url": "Nachweis/Impressum zur redaktionellen Verantwortung (optional)", "field_language": "Ausgabesprache", "field_theme": "Darstellung", "theme_mono": "Monochrom", "theme_color": "Farbig",
"preview": "Vorschau", "copy_html": "HTML kopieren", "copied": "Kopiert",
"pro_eyebrow": "Pro-Anpassung", "pro_title": "Eigene Texte und Badge-Designs", "pro_enabled": "Diese Instanz besitzt eine gültige Pro-Lizenz. Individuelle Texte und Farben können verwendet werden.",
"pro_locked": "Individuelle Titel, Beschreibungstexte, Badge-Beschriftungen und Farben sind in der Pro-Ausgabe verfügbar.",
@@ -48,18 +49,24 @@ var catalogs = map[string]Locale{
"footer_no_legal": "Keine Rechtsberatung.", "back": "Zurück", "declaration_eyebrow": "KI-Nutzungserklärung", "fact_component": "Komponente", "fact_extent": "KI-Anteil",
"fact_activities": "Tätigkeiten", "fact_review": "Menschliche Prüfung", "fact_note": "Hinweis", "fact_assurance": "Nachweisniveau", "fact_subject": "Gekennzeichneter Inhalt",
"fact_responsibility": "Redaktionelle Verantwortung", "fact_declared_at": "Erstellt", "none_value": "keine", "transparency_label": "Transparenzhinweis:",
- "transparency_text": "Diese Erklärung beschreibt die angegebene Nutzung von KI. Sie ist keine Lizenz, Zertifizierung oder Rechtsberatung.", "manifest": "Maschinenlesbares Manifest",
+ "transparency_text": "Diese Erklärung gibt die von der veröffentlichenden Person oder Organisation bereitgestellten Angaben zur KI-Nutzung wieder. Sie ersetzt weder eine rechtliche Bewertung noch eine Zertifizierung.",
+ "review_legal_label": "Menschliche Prüfung:", "review_legal_text": "Ein angegebenes Prüfungsniveau ist eine Selbstauskunft zum tatsächlichen Arbeitsprozess. Es begründet für sich allein keine Ausnahme von einer Kennzeichnungspflicht nach Art. 50 Abs. 4 AI Act; hierfür müssen die gesetzlichen Voraussetzungen, insbesondere substanzielle menschliche Prüfung oder redaktionelle Kontrolle und redaktionelle Verantwortung, tatsächlich erfüllt sein.",
+ "machine_marking_label": "Art. 50 Abs. 2:", "machine_marking_text": "Das verlinkte JSON-LD ist eine ergänzende Dokumentation. Es ist nicht als Ersatz für die vom Anbieter eines generativen KI-Systems geforderte wirksame, zuverlässige, robuste und interoperable maschinenlesbare Markierung der generierten oder manipulierten Ausgabe gedacht.",
+ "ai_act_embed_label": "Platzierung nach Art. 50:", "ai_act_embed_text": "Bei einer gesetzlich erforderlichen Offenlegung muss der sichtbare Hinweis klar und unterscheidbar spätestens bei der ersten Exposition zum betroffenen Inhalt erscheinen. Ein Badge nur im Footer oder erst hinter einem späteren Link kann dafür zu spät sein.",
+ "ai_act_machine_label": "Maschinenlesbarkeit:", "ai_act_machine_text": "Dieses JSON-LD dokumentiert die Nutzung ergänzend, erfüllt aber nicht automatisch die Anbieterpflicht zur technischen Markierung nach Art. 50 Abs. 2 AI Act.",
+ "emoji_compliance_label": "Emoji-Variante:", "emoji_compliance_text": "Das quadratische Emoji ist ein kompakter Zusatz und kein offizielles EU-Kennzeichnungssymbol. Bei einer gesetzlichen Offenlegung sollte es nicht ohne einen unmittelbar verständlichen Text oder eine gleichwertig klare Kennzeichnung verwendet werden.",
+ "manifest": "Maschinenlesbares Manifest",
"ai_label": "KI-Nutzung", "capabilities": "Funktionen",
},
map[string]PresetText{
- "no-ai": {"Kein Einsatz von KI", "Für die Erstellung des gekennzeichneten Inhalts wurde keine generative KI eingesetzt. Die inhaltliche Verantwortung und Prüfung lagen bei Menschen."},
- "research": {"Rechercheunterstützung", "KI wurde unterstützend zur Recherche und zum Auffinden relevanter Quellen eingesetzt. Auswahl, Einordnung und redaktionelle Ausarbeitung erfolgten durch Menschen."},
- "summary": {"Inhaltliche Zusammenfassung", "KI wurde zur Zusammenfassung von Quellen eingesetzt. Auswahl, fachliche Einordnung und Endfassung wurden durch Menschen verantwortet und geprüft."},
- "full": {"Vollständige Inhaltserstellung", "Der gekennzeichnete Inhalt wurde überwiegend oder vollständig mit generativer KI erstellt. Die veröffentlichte Fassung wurde anschließend menschlich geprüft und redaktionell verantwortet."},
+ "no-ai": {"Kein Einsatz von KI", "Für die Erstellung des gekennzeichneten Inhalts wurde nach den gemachten Angaben keine generative KI eingesetzt."},
+ "research": {"Rechercheunterstützung", "KI wurde unterstützend zur Recherche oder zum Auffinden relevanter Quellen eingesetzt. Eine menschliche Prüfung wird nur ausgewiesen, wenn sie ausdrücklich ausgewählt wurde."},
+ "summary": {"Inhaltliche Zusammenfassung", "KI wurde zur Zusammenfassung von Quellen eingesetzt. Eine menschliche Prüfung oder redaktionelle Kontrolle wird nicht automatisch unterstellt."},
+ "full": {"Vollständige Inhaltserstellung", "Der gekennzeichnete Inhalt wurde überwiegend oder vollständig mit generativer KI erstellt. Ein tatsächlicher menschlicher Prüfprozess ist gesondert anzugeben."},
},
map[string]string{"none": "Kein Einsatz von KI", "assisted": "KI-unterstützt", "partial": "Teilweise KI-generiert", "mostly": "Überwiegend KI-generiert", "full": "Vollständig KI-generiert"},
map[string]string{"text": "Text", "coverImage": "Titelbild", "image": "Bild", "audio": "Audio", "video": "Video", "code": "Code", "other": "Sonstiges"},
- map[string]string{"none": "Keine", "basic": "Grundlegend", "editorial": "Redaktionell", "expert": "Fachlich"},
+ map[string]string{"none": "Keine dokumentiert", "basic": "Grundlegend/formal", "editorial": "Inhaltlich-redaktionell", "expert": "Fachlich-inhaltlich"},
map[string]string{"research": "Recherche", "summarisation": "Zusammenfassung", "drafting": "Entwurf", "generation": "Erzeugung", "translation": "Übersetzung", "editing": "Überarbeitung", "imageGeneration": "Bilderzeugung", "codeGeneration": "Codeerzeugung", "transcription": "Transkription", "classification": "Klassifikation"},
map[string]string{"selfDeclared": "Selbsterklärung", "technicallyRecorded": "Technisch protokolliert", "signed": "Signiert", "verified": "Verifiziert"}),
"en": locale("en", "English", map[string]string{
@@ -67,18 +74,18 @@ var catalogs = map[string]Locale{
"standard_eyebrow": "Open disclosure standard", "hero_title": "Disclose how AI contributed to content.", "hero_lead": "A visible SVG badge, a human-readable declaration page and machine-readable JSON-LD – without cookies, tracking or a database requirement.",
"generator_eyebrow": "Generator", "generator_title": "Create an embed", "generator_intro": "Choose a template or record AI use systematically by content component. The preview, embed code and machine-readable declaration are generated immediately.",
"field_preset": "Preset", "option_custom": "Custom structure", "field_component": "Component", "field_extent": "AI contribution", "field_review": "Human review", "field_activities": "Activities",
- "activities_help": "Comma-separated standard values such as research, summarisation or translation.", "field_subject": "URL of the labelled content", "field_language": "Output language", "field_theme": "Appearance", "theme_mono": "Monochrome", "theme_color": "Colour",
+ "activities_help": "Comma-separated standard values such as research, summarisation or translation.", "review_help": "Record only the review process that actually took place. A merely formal check (for example spelling or grammar) is not substantive human review or editorial control for the purposes of Article 50(4) AI Act.", "field_subject": "URL of the labelled content", "field_responsible": "Editorially responsible person/organisation (optional)", "responsible_placeholder": "Name or organisation", "field_responsible_url": "Evidence/imprint for editorial responsibility (optional)", "field_language": "Output language", "field_theme": "Appearance", "theme_mono": "Monochrome", "theme_color": "Colour",
"preview": "Preview", "copy_html": "Copy HTML", "copied": "Copied", "pro_eyebrow": "Pro customisation", "pro_title": "Custom copy and badge designs",
"pro_enabled": "This instance has a valid Pro licence. Custom copy and colours are available.", "pro_locked": "Custom titles, descriptions, badge labels and colours are available in the Pro edition.",
"custom_title": "Custom declaration title", "custom_description": "Custom description", "custom_badge_label": "Custom left badge label", "custom_badge_message": "Custom right badge label", "custom_left_color": "Left colour", "custom_right_color": "Right colour", "pro_required": "Pro licence required",
"api_badge_title": "Badge endpoint", "api_badge_desc": "Standard badges are generated from presets and structured parameters. Pro adds custom copy and colours.", "api_manifest_title": "Manifest", "api_manifest_desc": "The JSON-LD manifest can be linked, validated or included in build pipelines.",
"api_stateless_title": "Stateless", "api_stateless_desc": "No sessions and deterministic responses. Designed for horizontal scaling, reverse proxies and CDNs.", "footer_no_legal": "Not legal advice.",
- "back": "Back", "declaration_eyebrow": "AI usage declaration", "fact_component": "Component", "fact_extent": "AI contribution", "fact_activities": "Activities", "fact_review": "Human review", "fact_note": "Note", "fact_assurance": "Assurance level", "fact_subject": "Labelled content", "fact_responsibility": "Editorial responsibility", "fact_declared_at": "Declared at", "none_value": "none", "transparency_label": "Transparency notice:", "transparency_text": "This declaration describes the stated use of AI. It is not a licence, certification or legal advice.", "manifest": "Machine-readable manifest", "ai_label": "AI use", "capabilities": "Capabilities",
+ "back": "Back", "declaration_eyebrow": "AI usage declaration", "fact_component": "Component", "fact_extent": "AI contribution", "fact_activities": "Activities", "fact_review": "Human review", "fact_note": "Note", "fact_assurance": "Assurance level", "fact_subject": "Labelled content", "fact_responsibility": "Editorial responsibility", "fact_declared_at": "Declared at", "none_value": "none", "transparency_label": "Transparency notice:", "transparency_text": "This declaration presents the information on AI use supplied by the publishing person or organisation. It does not replace a legal assessment or certification.", "review_legal_label": "Human review:", "review_legal_text": "A stated review level is a self-declaration of the process that actually took place. By itself it does not establish an exemption from a labelling duty under Article 50(4) AI Act; the statutory conditions, including substantive human review or editorial control and editorial responsibility, must actually be met.", "machine_marking_label": "Article 50(2):", "machine_marking_text": "The linked JSON-LD is supplementary documentation. It is not intended to replace the effective, reliable, robust and interoperable machine-readable marking of generated or manipulated output required from the provider of an in-scope generative AI system.", "ai_act_embed_label": "Article 50 placement:", "ai_act_embed_text": "Where disclosure is legally required, the visible notice must be clear and distinguishable no later than the first exposure to the affected content. A badge shown only in a footer or behind a later link may be too late.", "ai_act_machine_label": "Machine readability:", "ai_act_machine_text": "This JSON-LD supplements documentation but does not automatically satisfy the provider-side technical marking duty under Article 50(2) AI Act.", "emoji_compliance_label": "Emoji variant:", "emoji_compliance_text": "The square emoji is a compact supplement and is not an official EU labelling icon. Where a legal disclosure is required, do not rely on it without immediately understandable text or an equivalently clear disclosure.", "manifest": "Machine-readable manifest", "ai_label": "AI use", "capabilities": "Capabilities",
},
- map[string]PresetText{"no-ai": {"No AI use", "No generative AI was used to create the labelled content. Responsibility for the content and its review remained with people."}, "research": {"Research assistance", "AI was used to support research and identify relevant sources. Selection, assessment and editorial preparation were carried out by people."}, "summary": {"Content summarisation", "AI was used to summarise source material. Selection, contextual assessment and the final version were the responsibility of human editors."}, "full": {"Full content generation", "The labelled content was created mostly or entirely with generative AI. The published version was subsequently reviewed and editorially approved by people."}},
+ map[string]PresetText{"no-ai": {"No AI use", "According to the information supplied, no generative AI was used to create the labelled content."}, "research": {"Research assistance", "AI was used to support research or identify relevant sources. Human review is shown only when explicitly selected."}, "summary": {"Content summarisation", "AI was used to summarise source material. Human review or editorial control is not presumed automatically."}, "full": {"Full content generation", "The labelled content was created mostly or entirely with generative AI. Any actual human review process must be stated separately."}},
map[string]string{"none": "No AI use", "assisted": "AI-assisted", "partial": "Partially AI-generated", "mostly": "Mostly AI-generated", "full": "Fully AI-generated"},
map[string]string{"text": "Text", "coverImage": "Cover image", "image": "Image", "audio": "Audio", "video": "Video", "code": "Code", "other": "Other"},
- map[string]string{"none": "None", "basic": "Basic", "editorial": "Editorial", "expert": "Expert"},
+ map[string]string{"none": "None recorded", "basic": "Basic/formal", "editorial": "Substantive editorial", "expert": "Substantive expert"},
map[string]string{"research": "Research", "summarisation": "Summarisation", "drafting": "Drafting", "generation": "Generation", "translation": "Translation", "editing": "Editing", "imageGeneration": "Image generation", "codeGeneration": "Code generation", "transcription": "Transcription", "classification": "Classification"},
map[string]string{"selfDeclared": "Self-declared", "technicallyRecorded": "Technically recorded", "signed": "Signed", "verified": "Verified"}),
"fr": europeanLocale("fr", "Français", "Déclarer comment l’IA a contribué à un contenu.", "Utilisation de l’IA", "Aucune utilisation de l’IA", "Assisté par l’IA", "Partiellement généré par l’IA", "Majoritairement généré par l’IA", "Entièrement généré par l’IA",
@@ -135,6 +142,25 @@ func init() {
applyEuropeanTaxonomyTranslations()
applyArticleTranslations()
applyAssuranceTranslations()
+ applyThemeTranslations()
+}
+
+func applyThemeTranslations() {
+ values := map[string]string{
+ "de": "Emoji (quadratisch)",
+ "en": "Emoji (square)",
+ "fr": "Emoji (carré)",
+ "es": "Emoji (cuadrado)",
+ "it": "Emoji (quadrato)",
+ "nl": "Emoji (vierkant)",
+ "pt": "Emoji (quadrado)",
+ "pl": "Emoji (kwadratowe)",
+ }
+ for code, value := range values {
+ l := catalogs[code]
+ l.Text["theme_emoji"] = value
+ catalogs[code] = l
+ }
}
func applyArticleTranslations() {
diff --git a/openapi.yaml b/openapi.yaml
index f98aac4..10bb54b 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -172,7 +172,7 @@ components:
theme:
name: theme
in: query
- schema: {type: string, enum: [mono, color], default: color}
+ schema: {type: string, enum: [mono, color, emoji], default: color}
style:
name: style
in: query
diff --git a/schema/declaration.schema.json b/schema/declaration.schema.json
index 74eaedb..a989ab1 100644
--- a/schema/declaration.schema.json
+++ b/schema/declaration.schema.json
@@ -16,6 +16,7 @@
"editorialResponsibility": {
"type": "object",
"additionalProperties": false,
+ "required": ["name"],
"properties": {
"name": {"type": "string"},
"url": {"type": "string", "format": "uri"}
diff --git a/web/static/app.js b/web/static/app.js
index 566caa0..bdb10ba 100644
--- a/web/static/app.js
+++ b/web/static/app.js
@@ -6,10 +6,12 @@
if (!form) return;
const $ = id => document.getElementById(id);
const presets = {
- 'no-ai': {extent:'none', activities:'', review:'editorial'},
- research: {extent:'assisted', activities:'research', review:'editorial'},
- summary: {extent:'assisted', activities:'summarisation', review:'editorial'},
- full: {extent:'full', activities:'generation', review:'editorial'}
+ // Never assume human/editorial review. It can be legally relevant under
+ // Article 50(4) AI Act and must reflect the publisher's actual workflow.
+ 'no-ai': {extent:'none', activities:'', review:'none'},
+ research: {extent:'assisted', activities:'research', review:'none'},
+ summary: {extent:'assisted', activities:'summarisation', review:'none'},
+ full: {extent:'full', activities:'generation', review:'none'}
};
function value(id) {
@@ -66,6 +68,8 @@
params.set('lang', lang);
params.set('assurance', assurance);
if ($('subject').value.trim()) params.set('subject', $('subject').value.trim());
+ if (value('responsible')) params.set('responsible', value('responsible'));
+ if (value('responsible-url')) params.set('responsibleUrl', value('responsible-url'));
if (cfg.capabilities.custom_text) {
if (value('custom-title')) params.set('customTitle', value('custom-title'));
@@ -84,7 +88,9 @@
badgeParams.set('link', declarationURL);
const badgeURL = `${base}/v1/badge.svg?${badgeParams}`;
const manifestURL = `${base}/v1/declaration.json?${params}`;
- const alt = articleMode ? ((cfg.locales[lang] || cfg.locales.en).text.badge_article || 'Article transparency') : 'AI usage disclosure';
+ const alt = articleMode
+ ? (locale.text.badge_article || 'Article transparency')
+ : ((locale.presets[preset] && locale.presets[preset].title) || locale.extents[$('extent').value] || 'AI usage disclosure');
$('preview-badge').src = badgeURL;
$('preview-badge').alt = alt;
@@ -101,9 +107,9 @@
if (!reviewSelect) return;
if (extentSelect.value === 'none') {
reviewSelect.value = 'none';
- } else if (reviewSelect.value === 'none') {
- reviewSelect.value = 'editorial';
}
+ // For AI-assisted/generated components keep "none" until the publisher
+ // explicitly selects the review process that actually took place.
update(false);
});
});
diff --git a/web/static/style.css b/web/static/style.css
index 4818be0..2981126 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -1,5 +1,5 @@
:root{--bg:#f4f2ed;--surface:#fff;--ink:#171717;--muted:#646464;--line:#d7d3ca;--accent:#262626;--radius:18px;--shadow:0 16px 40px rgba(0,0,0,.08)}
-*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.55}a{color:inherit}.site-header,footer{max-width:1180px;margin:auto;padding:24px 28px;display:flex;align-items:center;justify-content:space-between;gap:24px}.site-header nav{display:flex;gap:20px}.site-header nav a,.brand{text-decoration:none}.brand{font-weight:800;letter-spacing:-.02em}.site-header nav a{color:var(--muted)}main{max-width:1180px;margin:auto;padding:24px 28px 80px}.hero{padding:72px 0 88px;max-width:920px}.eyebrow{text-transform:uppercase;letter-spacing:.14em;font-size:.74rem;font-weight:800;color:var(--muted)}h1,h2{letter-spacing:-.045em;line-height:1.03;margin:.25em 0}h1{font-size:clamp(3rem,8vw,6.7rem)}h2{font-size:clamp(2rem,4vw,3.6rem)}.lead{font-size:clamp(1.15rem,2vw,1.45rem);max-width:760px;color:#404040}.badge-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:34px}.badge-row img,.preview-card img{height:20px}.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:clamp(24px,5vw,58px);box-shadow:var(--shadow)}.generator-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin:34px 0}.generator-grid label,.code-tabs label{display:flex;flex-direction:column;gap:7px;font-weight:700}.generator-grid .wide{grid-column:1/-1}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;border:1px solid var(--line);border-radius:10px;background:#fbfaf7;color:var(--ink);padding:12px 14px}small{font-weight:400;color:var(--muted)}.preview-card{border-top:1px solid var(--line);padding-top:28px}.code-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin:22px 0}.code-tabs textarea{height:110px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.82rem}button{border:0;border-radius:999px;background:var(--accent);color:#fff;padding:12px 20px;font-weight:800;cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid #5b9cff;outline-offset:3px}.docs-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px;margin-top:24px}.docs-grid article{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:28px;overflow:hidden}.docs-grid h2{font-size:2rem}.docs-grid code{display:block;overflow:auto;padding:12px;background:#eeeae2;border-radius:8px;font-size:.82rem}.declaration-page{max-width:1180px}.back{text-decoration:none;color:var(--muted)}.declaration-card{margin-top:36px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:clamp(26px,4vw,52px);box-shadow:var(--shadow)}.declaration-card h1{font-size:clamp(2.8rem,7vw,5.5rem)}.facts{margin:42px 0}.facts div{display:grid;grid-template-columns:minmax(150px,.7fr) 1.5fr;gap:24px;padding:15px 0;border-top:1px solid var(--line)}.facts dt{font-weight:800}.facts dd{margin:0;overflow-wrap:anywhere}.notice{background:#eeeae2;border-radius:12px;padding:18px}footer{border-top:1px solid var(--line);color:var(--muted)}@media(max-width:760px){.site-header nav{display:none}.generator-grid,.code-tabs,.docs-grid{grid-template-columns:1fr}.generator-grid .wide{grid-column:auto}.facts div{grid-template-columns:1fr;gap:4px}.hero{padding-top:44px}h1{font-size:3.2rem}}
+*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.55}a{color:inherit}.site-header,footer{max-width:1180px;margin:auto;padding:24px 28px;display:flex;align-items:center;justify-content:space-between;gap:24px}.site-header nav{display:flex;gap:20px}.site-header nav a,.brand{text-decoration:none}.brand{font-weight:800;letter-spacing:-.02em}.site-header nav a{color:var(--muted)}main{max-width:1180px;margin:auto;padding:24px 28px 80px}.hero{padding:72px 0 88px;max-width:920px}.eyebrow{text-transform:uppercase;letter-spacing:.14em;font-size:.74rem;font-weight:800;color:var(--muted)}h1,h2{letter-spacing:-.045em;line-height:1.03;margin:.25em 0}h1{font-size:clamp(3rem,8vw,6.7rem)}h2{font-size:clamp(2rem,4vw,3.6rem)}.lead{font-size:clamp(1.15rem,2vw,1.45rem);max-width:760px;color:#404040}.badge-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:34px}.badge-row img,.preview-card img{height:20px}.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:clamp(24px,5vw,58px);box-shadow:var(--shadow)}.generator-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin:34px 0}.generator-grid label,.code-tabs label{display:flex;flex-direction:column;gap:7px;font-weight:700}.generator-grid .wide{grid-column:1/-1}input,select,textarea,button{font:inherit}input,select,textarea{width:100%;border:1px solid var(--line);border-radius:10px;background:#fbfaf7;color:var(--ink);padding:12px 14px}small{font-weight:400;color:var(--muted)}.preview-card{border-top:1px solid var(--line);padding-top:28px}.code-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin:22px 0}.code-tabs textarea{height:110px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.82rem}button{border:0;border-radius:999px;background:var(--accent);color:#fff;padding:12px 20px;font-weight:800;cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid #5b9cff;outline-offset:3px}.docs-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px;margin-top:24px}.docs-grid article{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:28px;overflow:hidden}.docs-grid h2{font-size:2rem}.docs-grid code{display:block;overflow:auto;padding:12px;background:#eeeae2;border-radius:8px;font-size:.82rem}.declaration-page{max-width:920px}.back{text-decoration:none;color:var(--muted)}.declaration-card{margin-top:36px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:clamp(26px,6vw,70px);box-shadow:var(--shadow)}.declaration-card h1{font-size:clamp(2.8rem,7vw,5.5rem)}.facts{margin:42px 0}.facts div{display:grid;grid-template-columns:minmax(150px,.7fr) 1.5fr;gap:24px;padding:15px 0;border-top:1px solid var(--line)}.facts dt{font-weight:800}.facts dd{margin:0;overflow-wrap:anywhere}.notice{background:#eeeae2;border-radius:12px;padding:18px}footer{border-top:1px solid var(--line);color:var(--muted)}@media(max-width:760px){.site-header nav{display:none}.generator-grid,.code-tabs,.docs-grid{grid-template-columns:1fr}.generator-grid .wide{grid-column:auto}.facts div{grid-template-columns:1fr;gap:4px}.hero{padding-top:44px}h1{font-size:3.2rem}}
.language-switch select{width:auto;min-width:130px;padding:8px 34px 8px 10px;background:#fff}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.pro-panel{margin:28px 0;border:1px solid var(--line);border-radius:14px;padding:24px;background:#f8f6f1}.pro-panel.enabled{background:#f2f7f3}.pro-panel.locked{background:#f3f1ed}.pro-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}.pro-heading h2{font-size:clamp(1.7rem,3vw,2.5rem)}.edition-pill{display:inline-flex;border:1px solid var(--line);border-radius:999px;padding:6px 12px;font-size:.78rem;font-weight:800;text-transform:uppercase;background:#fff}.generator-grid.compact{margin-bottom:0}.pro-panel input:disabled{cursor:not-allowed;opacity:.68}.declaration-card img{max-width:100%;height:auto}
@media(max-width:760px){.language-switch{margin-left:auto}.site-header{gap:12px}.pro-heading{align-items:center}}
@@ -46,3 +46,16 @@
/* Legal pages and footer navigation */
.footer-legal{display:flex;flex-wrap:wrap;gap:12px 20px;align-items:center}.footer-legal a{color:inherit;text-underline-offset:4px}.legal-page{background:var(--bg)}.legal-main{width:min(960px,calc(100% - 40px));margin:0 auto;padding:0 0 96px}.legal-hero{padding:clamp(54px,9vw,108px) 0 42px;border-bottom:1px solid var(--line)}.legal-hero h1{font-size:clamp(3rem,8vw,6.5rem);line-height:.93;letter-spacing:-.065em;margin:.15em 0}.legal-hero .lead{max-width:800px}.legal-updated{color:var(--muted);font-size:.92rem;margin-top:26px}.legal-language{display:flex;gap:8px}.legal-language a{display:inline-flex;align-items:center;justify-content:center;min-width:38px;min-height:38px;border:1px solid var(--line);border-radius:999px;text-decoration:none;font-weight:800}.configuration-warning{margin:34px 0;padding:22px 24px;border:2px solid #9b2c2c;border-radius:14px;background:#fff5f5;color:#651b1b}.configuration-warning strong{display:block;font-size:1.05rem}.configuration-warning p{margin:.5rem 0 0;line-height:1.6}.legal-content{display:grid;gap:0}.legal-section{padding:52px 0;border-bottom:1px solid var(--line)}.legal-section h2{font-size:clamp(1.65rem,4vw,2.8rem);letter-spacing:-.045em;margin:0 0 22px}.legal-section p,.legal-section li{max-width:82ch;line-height:1.75}.legal-section ul{padding-left:1.4rem}.legal-fields{margin:26px 0 0;border:1px solid var(--line);border-radius:14px;overflow:hidden;background:#fff}.legal-fields div{display:grid;grid-template-columns:minmax(190px,.7fr) minmax(0,1.4fr);border-top:1px solid var(--line)}.legal-fields div:first-child{border-top:0}.legal-fields dt,.legal-fields dd{padding:15px 17px;margin:0;white-space:pre-line;overflow-wrap:anywhere}.legal-fields dt{font-weight:800;background:#f3f1ec;border-right:1px solid var(--line)}.legal-fields a{overflow-wrap:anywhere}
@media(max-width:700px){.legal-main{width:min(100% - 28px,960px)}.legal-fields div{grid-template-columns:1fr}.legal-fields dt{border-right:0;border-bottom:1px solid var(--line)}.site-header .legal-language{margin-left:auto}footer{align-items:flex-start;gap:18px}.footer-legal{justify-content:flex-start}}
+
+/* Square emoji badge preview */
+.preview-card img[src*="theme=emoji"]{width:64px;height:64px}
+.declaration-card img[src*="theme=emoji"]{width:72px;height:72px}
+
+/* Compliance guidance */
+.form-warning {
+ grid-column: 1 / -1;
+ margin: 0;
+ font-size: .9rem;
+ line-height: 1.5;
+}
+.compliance-notice { margin-top: 14px; }
diff --git a/web/templates/declaration.html b/web/templates/declaration.html
index 5cd1e2f..843c399 100644
--- a/web/templates/declaration.html
+++ b/web/templates/declaration.html
@@ -52,6 +52,8 @@
{{range .Facts}}