diff --git a/CLAUDE.md b/CLAUDE.md index e1e7f22..da8461a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,7 @@ Dieses Dokument beschreibt Architektur, Konventionen und wichtige Implementierun - **Einstiegspunkt:** `index.html` - **Styles:** `css/styles.css` (Layout & Komponenten) + `css/tokens.css` (Design-Tokens) - **Logik:** `js/app.js` (eine einzige Datei) -- **Aktuelle Version:** `v62` (Script-Tag: ` + ``` ### Externe Aufrufe @@ -66,7 +66,7 @@ Die App hat keinen Build-Schritt – getestet wird **die ausgelieferte App**, al ```bash npm install # nur Dev: @playwright/test (exakt gepinnt) npx playwright install chromium # einmalig -npm test # 355 Tests, ~140 s +npm test # 361 Tests, ~145 s npm run test:ui # interaktiver Modus ``` @@ -253,6 +253,31 @@ d.distributions = [{ title, format, accessURL, license }, …] // mindestens e **CSV:** die flache Liste zeigt bewusst nur die **erste** Verteilung (Spalten `format`/`license`) plus die Spalte `verteilungen` (Anzahl). Der Rückimport führt entsprechend nur die erste zusammen und lässt weitere unangetastet – dieselbe Merge-Logik wie bei den Clearing-Antworten. Vollständig sind alle Verteilungen in JSON-LD, Turtle und der Projektdatei. +### Metadaten-Güte nach MQA (`mqaScore`, ersetzt die alte Vollständigkeit) + +Bis v62 zählte `completeness(d)` schlicht die gefüllten `REQUIRED_FIELDS`. Der Leitgedanke der **Metadata Quality Assurance** von data.europa.eu ist aber gerade, dass die Pflichtfelder allein noch keine gute Beschreibung ergeben – ein Datensatz mit allen Pflichtangaben, aber ohne Schlagwörter, ohne Zeitraum und nur als PDF ist schwer zu finden und kaum nachnutzbar. + +`MQA_DIMENSIONEN` trägt fünf FAIR-Dimensionen mit **405 Punkten**: + +| Dimension | Punkte | Prüfungen | +|---|---|---| +| Auffindbarkeit | 100 | Schlagwörter (30), Kategorie (30), räumliche (20) und zeitliche Abdeckung (20) | +| Zugänglichkeit | 100 | Zugriffs-URL je Verteilung (50), Info-Seite (20), Zugriffsrechte (30) | +| Interoperabilität | 110 | Format je Verteilung (30), maschinenlesbar (40), nicht herstellergebunden (20), Zyklus (20) | +| Nachnutzbarkeit | 75 | Lizenz je Verteilung (25), Lizenz offen (20), Herausgeber (15), Kontakt mit E-Mail (15) | +| Kontext | 20 | Beschreibung (8), Titel (6), Datum (6) | + +> ⚠️ **Was übernommen ist und was nicht.** Die **fünf Dimensionen und ihre Gewichte** stammen aus der MQA-Methodik (Quelle: Recherchebericht des Nutzers). Die **Zuordnung der Einzelprüfungen und ihre Punkte innerhalb einer Dimension sind eigene Zutat** – die Aufteilung im Original lag nicht vor. Ein Test hält beides auseinander: er pinnt die fünf Dimensionsgewichte wörtlich und prüft, dass jede Dimension genau ihr Maximum ausschöpft. Wer eine Prüfung ergänzt, muss innerhalb der Dimension umverteilen. + +> ⚠️ **Nicht dasselbe wie der Wert des Portals.** Die MQA ruft URLs tatsächlich ab und prüft, ob sie antworten. DatenLotse macht **keine Netzaufrufe** – geprüft wird nur, ob eine Angabe da und wohlgeformt ist. Der Kasten im Qualitäts-Tab sagt das ausdrücklich. + +- **`completeness(d)` behält seinen Namen** und liefert `mqaScore(d).prozent`. Damit gibt es weiterhin genau **eine** graduelle Zahl: Badge, Ø im Inventar, Sortierung, Dashboard, Status-Einseiter und PDF-Bericht hängen alle daran. Die Ampelschwellen (≥ 80 / ≥ 50) bleiben. +- **Die Bestnote ist erreichbar** – ein rundum gepflegter Datensatz erreicht 100 %. Ein Test hält das fest; ohne ihn wäre die 80-%-Schwelle eine Schikane. +- **Ein frischer Import fällt von ~83 % auf ~46 %.** Das ist beabsichtigt und der eigentliche Zweck: die alte Zahl war zu freundlich. +- `MQA_OFFENE_FORMATE` ist eine **Positivliste**. „Nicht herstellergebunden" lässt sich nicht aus dem Fehlen von etwas ableiten; unbekannte Formate zählen deshalb **nicht** mit, statt großzügig durchgewunken zu werden. +- `renderMqaSummary()` zeigt im Qualitäts-Tab (`#quality-mqa`) die Dimensionen **über den ganzen Bestand gemittelt** plus die drei schwächsten Einzelprüfungen. Der Einzelwert steht schon auf der Karte; hier interessiert, **wo** das Inventar schwach ist. +- `empty(v)` liegt seit v63 **modulweit** – vorher stand dieselbe Definition zweimal lokal in `validateDataset()` und `inventoryIssues()`. + ### Vollständigkeit (Ampel) `completeness(d)` misst den Anteil gefüllter `REQUIRED_FIELDS` (`title, description, publisher, contactPoint, accrualPeriodicity, license, accessRights`) als 0–100 %. Schwellen für die Badge-Farbe: ≥ 80 % `--ampel-gruen`, ≥ 50 % `--ampel-gelb`, sonst `--ampel-rot`. Eingaben werden per `input`-Listener live in `inventory[idx]` zurückgeschrieben und Badge + Durchschnitt sofort aktualisiert. @@ -632,6 +657,7 @@ Nach Änderungen an `app.js` `?v=N` im Script-Tag **und** die `v{N}` im Footer e | v16 | Daten-Kompass (Herzstück) – eigene View + Hero-Haupt-CTA (Topbar-„Loslegen" zeigt ebenfalls darauf): ausführliche Open-Data-Reifegrad-Checkliste nach ODRA / EU Open Data Maturity / 5-Sterne / DCAT-AP.de / DSGVO·FAIR (7 Dimensionen, Quellenangaben), Status je Item mit Score & Ampel, Vorbelegung aus dem App-Stand, adaptive Sprünge in die passenden Bausteine, Persistenz (`datenlotse_kompass`) und PDF-Export; leeres „Loslegen"-Platzhalter-Modal entfernt | | v17 | Weiterer Ausbau (1/4) – Inventar Suche, Filter & Sortierung: `renderInventory()` in `renderInventory()` + `renderInventoryBody()` aufgeteilt; `.inv-controls` (Volltextsuche + Schutzbedarf-/Clearing-Ampel-Filter + Sortierung Titel/Vollständigkeit) über `invFilter`-State und `filteredInventory()`; der echte `idx` wird durch den Filter mitgeführt, sodass Editieren über gefilterten Teilmengen weiterhin den richtigen Datensatz trifft; Live-Meta „X von Y" + Empty-State | | v18 | Weiterer Ausbau (2/4) – Pseudonymisierung erweitert: drei neue Muster (Sozialversicherungsnummer, Steuer-ID *kontextgetriggert*, Kfz-Kennzeichen), Aktenzeichen um Geschäftszeichen/„Gz." und buchstabenhaltige Kerne erweitert, zusätzliche Geburtsdatum-Trigger („Geburtsdatum"/„Geburtstag"); Mapping-Export als CSV (`buildPseudoMappingCSV` + Button im Mapping-Kopf); Demo-Text und Grenzen-Liste aktualisiert; verifiziert auf Determinismus, Platzhalter-Konsistenz und Null-Falschtreffer auf neutralem Verwaltungstext | +| v63 | **Die Vollständigkeits-Zahl war zu freundlich.** `completeness()` zählte gefüllte Pflichtfelder – ein Datensatz mit allen Pflichtangaben stand auf 100 %, auch ohne Schlagwörter, ohne Zeitraum und nur als PDF. Genau dagegen richtet sich der Leitgedanke der Metadata Quality Assurance von data.europa.eu. Die Zahl folgt jetzt deren Schema: fünf FAIR-Dimensionen, 405 Punkte, 18 Einzelprüfungen. **Ersatz statt drittes Maß** – der Funktionsname bleibt, damit Badge, Durchschnitt, Sortierung, Dashboard und Bericht weiter an genau einer graduellen Zahl hängen. Ein frischer Import fällt damit von ~83 % auf ~46 %; das ist der Zweck. Sauber getrennt ist, was übernommen wurde: die **Dimensionsgewichte** stammen aus der Methodik, die **Zuordnung der Einzelprüfungen** ist eigene Zutat – ein Test pinnt die Gewichte wörtlich und prüft, dass jede Dimension ihr Maximum ausschöpft. Ausdrücklich benannt ist auch, was der Wert **nicht** ist: die MQA ruft URLs ab, DatenLotse macht keine Netzaufrufe und prüft nur Vorhandensein und Wohlgeformtheit. Der stärkste Test geht die 18 Prüfungen einzeln durch und misst, ob jede ihre vollen Punkte ausmacht – eine wirkungslose Prüfung fällt sofort auf. Beim Bauen zeigte sich nebenbei, dass `empty()` zweimal wortgleich als lokale Hilfe existierte; jetzt einmal modulweit. 8 neue Tests, drei bestehende auf die neue Semantik umgeschrieben statt gelockert. Ein erster Mutationsversuch war zu schwach gewählt und lief grün durch – erst die schärfere Fassung (Positivliste akzeptiert alles) machte ihn rot. | | v62 | **Rechtslage nachgezogen – mit offengelegter Herkunft.** `LEGAL_BASIS` kannte weder den **Data Act** noch den **Data Governance Act**, obwohl beide inzwischen anwendbar sind, und der Eintrag zu § 12a EGovG stand noch auf dem Stand vor dem OZG-Änderungsgesetz (Ausweitung auf die „Behörden des Bundes", Open-Data-Koordinatoren, Berichtspflicht). Der interessanteste Zugewinn ist **DGG Kapitel II**: das Statistische Bundesamt ist dort zentrale Informationsstelle für die Weiterverwendung geschützter Verwaltungsdaten und unterstützt ausdrücklich bei Anonymisierung und Pseudonymisierung – das ist der amtliche Anknüpfungspunkt zu Modul 3a und 3b. Beim Data Act war die Formulierung heikel: Kapitel V verpflichtet nur bei **außergewöhnlicher Notwendigkeit**, und ein Eintrag, der das verschweigt, weckt die Erwartung eines allgemeinen Datenzugangs; ein Test hält den Vorbehalt fest. **Zur Herkunft:** die Angaben stammen aus einem Recherchebericht des Nutzers, nicht aus eigener Prüfung an der amtlichen Quelle – die Netzwerkrichtlinie sperrt EUR-Lex und gesetze-im-internet.de. Verlinkt wurde deshalb nur, was sich aus dem dokumentierten CELEX-Schema sicher bilden lässt; DADG und DGG stehen im Text, aber ohne Link, damit keine geratene Fundstelle als amtliche Quelle erscheint. Dazu: die Prüfwerkzeuge nannten die **DCAT-AP.de-Spezifikation 2.0**, aktuell ist 3.0 – das Konventionenhandbuch bleibt dagegen ausdrücklich bei 2.0. 6 neue Tests, drei Mutationen geprüft. | | v61 | **Derselbe Fehler, zwei Tabs weiter.** Nachdem v57 im Risiko-Tab drei Fehlerklassen behoben hatte, lag die Frage nahe, ob die Schwester-Tabs sie auch tragen. Eine gezielte Probe sagte: ja, alle drei. Am schwersten wiegt die Spaltenbereinigung, weil dort ein **Download** daran hängt: wer nach dem Bereinigen eine zweite Spalte auf „ganze Spalte ersetzen" stellt, sah weiterhin das alte Ergebnis – und der Knopf „Bereinigte CSV" lieferte eine Datei, in der der Wert noch im Klartext stand, obwohl die Konfiguration daneben etwas anderes behauptete. Nachgemessen: Konfiguration `{0: ganz, 1: ganz}`, Datei `Name,Ort\n[PERSON_1],Leipzig`. Im Freitext-Tab dasselbe Muster – die bereinigte Fassung blieb nach einer Textänderung stehen, samt Download. Dazu der Fokusverlust beim Umschalten einer Spaltenbehandlung, weil `renderPseudoCsv()` die ganze Box neu baute. Alle drei behoben; die Regel steht jetzt als Konvention in der Doku, weil sie zum dritten Mal aufgetreten ist. 5 neue Tests, drei Mutationen einzeln geprüft. | | v60 | **Die Ampel war nicht lesbar – gemessen, nicht vermutet.** Beim Kontrast-Nachmessen in v57 war aufgefallen, dass die bestehenden Badges dieselbe Schwäche haben wie der damals neue Kasten; dieser Schnitt zieht es nach. Ein Durchgang über alle Ampelnutzungen förderte **zwei** Muster zutage, beide betroffen: Ampeltext auf der 12-%-Tönung derselben Farbe (`--ampel-gelb` **2,66 : 1**, `--ampel-gruen` 2,98) und weiße Schrift auf dem Vollton (3,00 bzw. 3,40). Rot bestand mit 4,54 / 5,44 – die beiden anderen nicht. Statt Einzelfarben zu korrigieren gibt es jetzt je Stufe **zwei Token**: der Vollton für Flächen, Balken und Punkte, eine dunklere Variante für Text und bedeutungstragende Icons. Die Regel ist mechanisch (`color:` → Text-Token) und per Test abgesichert, sodass eine neue Textfarbe auf dem Vollton rot wird. **Dazu der dritte Fund derselben Art beim `hidden`-Attribut:** nach `.quality-panel` (v28) und `.pseudo-mini-btn` (v29) war es in v58 `.sample-card` – jedes Mal gewinnt eine Klasse mit eigenem `display` gegen die UA-Regel mit Specificity 0-0-1. Statt eines vierten Einzelfalls jetzt **eine** globale Regel plus ein Test, der für jede der rund 180 Klassen mit eigenem `display` eine Probe anlegt und prüft, ob `hidden` greift. Der erste Lauf meldete 176 Verstöße – die Regel schließt sie alle. Nebenbei: mein erster Kontrast-Scanner meldete für viele Elemente exakt 1,00 : 1, weil die DOM-Suche bei Verläufen keinen Hintergrund findet und auf Weiß zurückfällt – gemessen wurde am Ende an den Token-Werten. | diff --git a/README.md b/README.md index 22e196e..5ea537c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ | 📥 | **DCAT-AP.de-Katalog einlesen** | Wer bereits veröffentlicht, braucht kein Erstinventar, sondern eine Prüfung des Bestands: einen vorhandenen JSON-LD-Katalog einlesen, gegen die Qualitätsprüfung halten und mit Clearing und Governance verbinden. Zusammengeführt über die `id` – bereits gegebene Clearing-Antworten bleiben | | ♻️ | **Rückimport der Inventar-CSV** | Die exportierte Liste außerhalb bearbeiten (Tabellenkalkulation, Zuarbeit aus den Fachbereichen) und wieder einlesen. Der Import **führt über die `id` zusammen statt zu ersetzen** – bereits gegebene Clearing-Antworten bleiben erhalten | | 🗂️ | **Verteilungen je Datensatz** | Ein Datensatz kann in mehreren Formaten vorliegen – CSV *und* JSON *und* GeoJSON, je mit eigener Zugriffs-URL und Lizenz. Genau so modelliert es DCAT-AP.de; die Vollständigkeit verlangt entsprechend eine Lizenz **je** Verteilung | +| 📐 | **Metadaten-Güte (MQA)** | Statt gefüllte Pflichtfelder zu zählen, bewertet DatenLotse die Beschreibung graduell nach dem Schema der **Metadata Quality Assurance** von data.europa.eu: fünf FAIR-Dimensionen, 405 Punkte, 18 Prüfungen. Der Qualitäts-Tab zeigt, **wo** das Inventar schwach ist – „Auffindbarkeit 20 %" ist ein Arbeitsauftrag, ein Durchschnitt ist nur eine Zahl. ⚠️ Nicht identisch mit dem Wert des Portals: dort werden Adressen tatsächlich abgerufen, DatenLotse macht keine Netzaufrufe und prüft nur Vorhandensein und Wohlgeformtheit | | 🔍 | **Suche, Filter & Sortierung** | Inventar live durchsuchen (Titel/Publisher/Quellsystem), nach Schutzbedarf, Clearing-Ampel oder **Publikationsreife** filtern und nach Titel, Vollständigkeit oder **Publikationsreife** sortieren – Editieren bleibt auch über gefilterten Teilmengen korrekt | | 📊 | **Status-Dashboard** | Startseite zeigt – sobald Daten vorliegen – Live-Kennzahlen über alle Bausteine (Kompass-Reifegrad, Inventar-Anzahl & Ø-Vollständigkeit, Clearing-Ampelverteilung, Governance-Reifegrad) mit Schnellsprung direkt zum Weiterarbeiten | | 📤 | **DCAT-Export (JSON-LD, RDF/Turtle, CSV)** | DCAT-AP.de-konformes JSON-LD (`dcat:Catalog`/`dcat:Dataset` mit `@context`) zum Harvesting durch GovData/CKAN, dieselbe Ausgabe als **RDF/Turtle** (`.ttl`, für Portale die RDF direkt harvesten) sowie eine flache CSV-Liste | @@ -115,7 +116,7 @@ Die App selbst bleibt abhängigkeitsfrei; für die Tests wird einmalig Playwrigh ```bash npm install # nur Dev: @playwright/test npx playwright install chromium -npm test # 355 Tests, ~140 s +npm test # 361 Tests, ~145 s npm run test:ui # interaktiver Modus ``` @@ -305,7 +306,7 @@ datenlotse/ │ ├── sample-landkreis.csv # Beispiel: fiktive Kreisverwaltung (12 Datensätze) │ ├── sample-landesbehoerde.csv # Beispiel: fiktive Landesebene (12 Datensätze) │ └── template.csv # Leere Vorlage zum eigenen Befüllen -├── tests/ # Playwright-End-to-End-Tests (355 Tests) +├── tests/ # Playwright-End-to-End-Tests (361 Tests) │ ├── helpers.js # openApp/loadSample/Download-Helfer │ ├── smoke.spec.js # Views, Routing, Dashboard, HTML-Validität │ ├── import.spec.js # CSV-Parser, Ableitung, Formel-Injection diff --git a/assets/fonts/fa/icons.min.css b/assets/fonts/fa/icons.min.css index f8d0e30..8b57222 100644 --- a/assets/fonts/fa/icons.min.css +++ b/assets/fonts/fa/icons.min.css @@ -8,5 +8,5 @@ */ @font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:swap;src:url(../webfonts/fa-regular-400.subset.woff2) format("woff2")} @font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:swap;src:url(../webfonts/fa-solid-900.subset.woff2) format("woff2")} -.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-regular,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-brands:before,.fa-regular:before,.fa-solid:before,.fa:before,.fab:before,.far:before,.fas:before{content:var(--fa)}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{animation-name:fa-beat;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{animation-name:fa-bounce;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{animation-name:fa-fade;animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{animation-name:fa-beat-fade;animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{animation-name:fa-flip;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{animation-name:fa-shake;animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{animation-name:fa-spin;animation-duration:var(--fa-animation-duration,2s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{animation-name:fa-spin;animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{animation-delay:-1ms;animation-duration:1ms;animation-iteration-count:1;transition-delay:0s;transition-duration:0s}}@keyframes fa-beat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-bounce{0%{transform:scale(1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{transform:scale(1) translateY(0)}to{transform:scale(1) translateY(0)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-flip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-shake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,to{transform:rotate(0deg)}}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{transform:rotate(90deg)}.fa-rotate-180{transform:rotate(180deg)}.fa-rotate-270{transform:rotate(270deg)}.fa-flip-horizontal{transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}.fa-rotate-by{transform:rotate(var(--fa-rotate-angle,0))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}.fa-trash-can{--fa:"\f2ed"}.fa-file-lines{--fa:"\f15c"}.fa-file-csv{--fa:"\f6dd"}.fa-gavel{--fa:"\f0e3"}.fa-table{--fa:"\f0ce"}.fa-compass{--fa:"\f14e"}.fa-bars{--fa:"\f0c9"}.fa-lightbulb{--fa:"\f0eb"}.fa-circle-exclamation{--fa:"\f06a"}.fa-square-check{--fa:"\f14a"}.fa-lock{--fa:"\f023"}.fa-share-nodes{--fa:"\f1e0"}.fa-bookmark{--fa:"\f02e"}.fa-wand-magic-sparkles{--fa:"\e2ca"}.fa-globe{--fa:"\f0ac"}.fa-spell-check{--fa:"\f891"}.fa-city{--fa:"\f64f"}.fa-file-arrow-up{--fa:"\f574"}.fa-map-location-dot{--fa:"\f5a0"}.fa-table-columns{--fa:"\f0db"}.fa-handshake{--fa:"\f2b5"}.fa-circle-check{--fa:"\f058"}.fa-file-import{--fa:"\f56f"}.fa-scroll{--fa:"\f70e"}.fa-shield-halved{--fa:"\f3ed"}.fa-square{--fa:"\f0c8"}.fa-question{--fa:"\3f"}.fa-hand-holding-heart{--fa:"\f4be"}.fa-arrow-up-right-from-square{--fa:"\f08e"}.fa-chart-pie{--fa:"\f200"}.fa-chart-line{--fa:"\f201"}.fa-arrow-right{--fa:"\f061"}.fa-recycle{--fa:"\f1b8"}.fa-clipboard-check{--fa:"\f46c"}.fa-circle-question{--fa:"\f059"}.fa-route{--fa:"\f4d7"}.fa-pen{--fa:"\f304"}.fa-cubes{--fa:"\f1b3"}.fa-arrow-left{--fa:"\f060"}.fa-align-left{--fa:"\f036"}.fa-circle-nodes{--fa:"\e4e2"}.fa-circle-info{--fa:"\f05a"}.fa-landmark{--fa:"\f66f"}.fa-check-double{--fa:"\f560"}.fa-minus{--fa:"\f068"}.fa-flask{--fa:"\f0c3"}.fa-download{--fa:"\f019"}.fa-file-circle-check{--fa:"\e5a0"}.fa-id-card{--fa:"\f2c2"}.fa-house{--fa:"\f015"}.fa-file-arrow-down{--fa:"\f56d"}.fa-book-open-reader{--fa:"\f5da"}.fa-gauge-high{--fa:"\f625"}.fa-boxes-stacked{--fa:"\f468"}.fa-link{--fa:"\f0c1"}.fa-play{--fa:"\f04b"}.fa-chevron-down{--fa:"\f078"}.fa-code-compare{--fa:"\e13a"}.fa-list-check{--fa:"\f0ae"}.fa-user-shield{--fa:"\f505"}.fa-diagram-project{--fa:"\f542"}.fa-plus{--fa:"\2b"}.fa-xmark{--fa:"\f00d"}.fa-ruler-combined{--fa:"\f546"}.fa-map{--fa:"\f279"}.fa-rocket{--fa:"\f135"}.fa-arrow-trend-up{--fa:"\e098"}.fa-traffic-light{--fa:"\f637"}.fa-rotate{--fa:"\f2f1"}.fa-gears{--fa:"\f085"}.fa-award{--fa:"\f559"}.fa-file-export{--fa:"\f56e"}.fa-arrow-trend-down{--fa:"\e097"}.fa-scale-balanced{--fa:"\f24e"}.fa-table-list{--fa:"\f00b"}.fa-book{--fa:"\f02d"}.fa-diagram-successor{--fa:"\e47a"}.fa-triangle-exclamation{--fa:"\f071"}.fa-database{--fa:"\f1c0"}.fa-circle-xmark{--fa:"\f057"}.fa-arrows-spin{--fa:"\e4bb"}.fa-print{--fa:"\f02f"}.fa-users-gear{--fa:"\f509"}.fa-building-columns{--fa:"\f19c"}.fa-graduation-cap{--fa:"\f19d"} +.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-regular,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-brands:before,.fa-regular:before,.fa-solid:before,.fa:before,.fab:before,.far:before,.fas:before{content:var(--fa)}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{animation-name:fa-beat;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{animation-name:fa-bounce;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{animation-name:fa-fade;animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{animation-name:fa-beat-fade;animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{animation-name:fa-flip;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{animation-name:fa-shake;animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{animation-name:fa-spin;animation-duration:var(--fa-animation-duration,2s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{animation-name:fa-spin;animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{animation-delay:-1ms;animation-duration:1ms;animation-iteration-count:1;transition-delay:0s;transition-duration:0s}}@keyframes fa-beat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-bounce{0%{transform:scale(1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{transform:scale(1) translateY(0)}to{transform:scale(1) translateY(0)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-flip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-shake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,to{transform:rotate(0deg)}}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{transform:rotate(90deg)}.fa-rotate-180{transform:rotate(180deg)}.fa-rotate-270{transform:rotate(270deg)}.fa-flip-horizontal{transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}.fa-rotate-by{transform:rotate(var(--fa-rotate-angle,0))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}.fa-trash-can{--fa:"\f2ed"}.fa-file-lines{--fa:"\f15c"}.fa-file-csv{--fa:"\f6dd"}.fa-gavel{--fa:"\f0e3"}.fa-table{--fa:"\f0ce"}.fa-compass{--fa:"\f14e"}.fa-bars{--fa:"\f0c9"}.fa-lightbulb{--fa:"\f0eb"}.fa-circle-exclamation{--fa:"\f06a"}.fa-square-check{--fa:"\f14a"}.fa-lock{--fa:"\f023"}.fa-share-nodes{--fa:"\f1e0"}.fa-bookmark{--fa:"\f02e"}.fa-wand-magic-sparkles{--fa:"\e2ca"}.fa-globe{--fa:"\f0ac"}.fa-spell-check{--fa:"\f891"}.fa-city{--fa:"\f64f"}.fa-file-arrow-up{--fa:"\f574"}.fa-map-location-dot{--fa:"\f5a0"}.fa-table-columns{--fa:"\f0db"}.fa-handshake{--fa:"\f2b5"}.fa-circle-check{--fa:"\f058"}.fa-file-import{--fa:"\f56f"}.fa-scroll{--fa:"\f70e"}.fa-shield-halved{--fa:"\f3ed"}.fa-square{--fa:"\f0c8"}.fa-question{--fa:"\3f"}.fa-hand-holding-heart{--fa:"\f4be"}.fa-arrow-up-right-from-square{--fa:"\f08e"}.fa-chart-pie{--fa:"\f200"}.fa-chart-line{--fa:"\f201"}.fa-arrow-right{--fa:"\f061"}.fa-recycle{--fa:"\f1b8"}.fa-clipboard-check{--fa:"\f46c"}.fa-circle-question{--fa:"\f059"}.fa-route{--fa:"\f4d7"}.fa-pen{--fa:"\f304"}.fa-cubes{--fa:"\f1b3"}.fa-arrow-left{--fa:"\f060"}.fa-align-left{--fa:"\f036"}.fa-circle-nodes{--fa:"\e4e2"}.fa-circle-info{--fa:"\f05a"}.fa-landmark{--fa:"\f66f"}.fa-check-double{--fa:"\f560"}.fa-minus{--fa:"\f068"}.fa-flask{--fa:"\f0c3"}.fa-download{--fa:"\f019"}.fa-file-circle-check{--fa:"\e5a0"}.fa-id-card{--fa:"\f2c2"}.fa-house{--fa:"\f015"}.fa-file-arrow-down{--fa:"\f56d"}.fa-arrows-turn-to-dots{--fa:"\e4c1"}.fa-book-open-reader{--fa:"\f5da"}.fa-gauge-high{--fa:"\f625"}.fa-boxes-stacked{--fa:"\f468"}.fa-link{--fa:"\f0c1"}.fa-play{--fa:"\f04b"}.fa-magnifying-glass{--fa:"\f002"}.fa-chevron-down{--fa:"\f078"}.fa-code-compare{--fa:"\e13a"}.fa-list-check{--fa:"\f0ae"}.fa-user-shield{--fa:"\f505"}.fa-diagram-project{--fa:"\f542"}.fa-plus{--fa:"\2b"}.fa-xmark{--fa:"\f00d"}.fa-ruler-combined{--fa:"\f546"}.fa-map{--fa:"\f279"}.fa-rocket{--fa:"\f135"}.fa-arrow-trend-up{--fa:"\e098"}.fa-traffic-light{--fa:"\f637"}.fa-rotate{--fa:"\f2f1"}.fa-gears{--fa:"\f085"}.fa-award{--fa:"\f559"}.fa-file-export{--fa:"\f56e"}.fa-arrow-trend-down{--fa:"\e097"}.fa-scale-balanced{--fa:"\f24e"}.fa-table-list{--fa:"\f00b"}.fa-book{--fa:"\f02d"}.fa-diagram-successor{--fa:"\e47a"}.fa-triangle-exclamation{--fa:"\f071"}.fa-database{--fa:"\f1c0"}.fa-circle-xmark{--fa:"\f057"}.fa-arrows-spin{--fa:"\e4bb"}.fa-print{--fa:"\f02f"}.fa-users-gear{--fa:"\f509"}.fa-building-columns{--fa:"\f19c"}.fa-graduation-cap{--fa:"\f19d"} .fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}.fa-brands,.fab{font-weight:400}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}.fa-solid,.fas{font-weight:900} diff --git a/assets/fonts/webfonts/fa-regular-400.subset.woff2 b/assets/fonts/webfonts/fa-regular-400.subset.woff2 index 4004a60..6558aea 100644 Binary files a/assets/fonts/webfonts/fa-regular-400.subset.woff2 and b/assets/fonts/webfonts/fa-regular-400.subset.woff2 differ diff --git a/assets/fonts/webfonts/fa-solid-900.subset.woff2 b/assets/fonts/webfonts/fa-solid-900.subset.woff2 index 3ed36fe..0fb9992 100644 Binary files a/assets/fonts/webfonts/fa-solid-900.subset.woff2 and b/assets/fonts/webfonts/fa-solid-900.subset.woff2 differ diff --git a/css/styles.css b/css/styles.css index 52fc071..c127d4f 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1120,6 +1120,31 @@ a { color: var(--c-accent); } /* ── Bestandsprüfung (übergreifend) ───────────────────────────── */ .quality-inventory { min-width: 0; } + +/* ── Metadaten-Güte nach dem MQA-Schema ── */ +.quality-mqa { min-width: 0; } +.quality-mqa:not(:empty) { + background: var(--glass-bg); border: 1px solid var(--c-border-s); + border-radius: var(--radius); padding: 16px 18px; margin-bottom: 16px; +} +.qual-mqa-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 14px; margin-bottom: 14px; } +.qual-mqa-head strong { font-size: 15px; color: var(--c-accent); } +.qual-mqa-score { margin-left: auto; font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; } +.qual-mqa-score span { font-size: 12px; font-weight: 500; color: var(--c-muted); } +.qual-mqa-dims { display: flex; flex-direction: column; gap: 8px; } +.qual-mqa-dim { display: grid; grid-template-columns: minmax(0, 160px) 1fr 44px; align-items: center; gap: 10px; } +.qual-mqa-dim-lbl { font-size: 12px; color: var(--c-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.qual-mqa-dim-lbl i { color: var(--c-accent); margin-right: 5px; } +.qual-mqa-bar { height: 8px; border-radius: 4px; background: rgba(122,117,145,0.16); overflow: hidden; } +.qual-mqa-bar > span { display: block; height: 100%; border-radius: 4px; background: var(--c-accent); transition: width 0.25s var(--ease-fluid); } +.qual-mqa-pct { font-size: 12px; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; } +.qual-mqa-hint { font-size: 12.5px; margin-top: 14px; } +.qual-mqa-anteil { color: var(--c-muted); } +.qual-mqa-note { font-size: 11.5px; color: var(--c-muted); margin-top: 10px; line-height: 1.5; } +@media (max-width: 600px) { + .qual-mqa-dim { grid-template-columns: 1fr 44px; } + .qual-mqa-bar { grid-column: 1 / -1; order: 3; } +} .qual-cross { margin: 0 0 18px; padding: 14px 16px; diff --git a/datenschutz/index.html b/datenschutz/index.html index 83b4b5c..3b2d6ee 100644 --- a/datenschutz/index.html +++ b/datenschutz/index.html @@ -42,7 +42,7 @@ } - + diff --git a/index.html b/index.html index 537261b..c0c7837 100644 --- a/index.html +++ b/index.html @@ -125,10 +125,10 @@ - + - - + +
@@ -392,8 +392,9 @@