Design Anpassungen Dashboard + Rep. PC Importer

This commit is contained in:
Michael 2026-08-24 15:17:22 +02:00
parent 0effadb736
commit 6cfe0c1fd5
37 changed files with 658 additions and 316 deletions

View File

@ -20,6 +20,10 @@
## UI typography ## UI typography
- Use the locally bundled Inter variable font as the single application typeface. Do not introduce view-specific display, condensed, serif, or monospace families; hierarchy comes from size and weight.
- Use only the visual weights Regular 400, Semibold 600, and Bold 700. Bold is reserved for branding and exceptional emphasis.
- Use the semantic typography roles: H1 56px/600, H2 32px/600, H3 18px/600, H4 16px/600, body and table content 16px/400, labels 14px/600, metadata and table headers 13px, and buttons 14px/600.
- Preserve normal title case for H1-H4, field labels, and buttons. Uppercase is limited to compact table headers, badges/categories, and the ASSESSOR wordmark.
- Render continuous prose and other content-bearing text at no less than `16px`, including finding content, report text, Grid cell content, and Raw Data. - Render continuous prose and other content-bearing text at no less than `16px`, including finding content, report text, Grid cell content, and Raw Data.
- Compact interface text such as button captions, field labels, metadata, badges, and comparable control annotations may use a smaller type size. - Compact interface text such as button captions, field labels, metadata, badges, and comparable control annotations may use a smaller type size.
- When adding or changing styles, preserve this distinction instead of reducing content text to a compact UI-label token. - When adding or changing styles, preserve this distinction instead of reducing content text to a compact UI-label token.
@ -31,6 +35,31 @@
- Apply the inset exactly once on the view's root content container. Split panes and edge-to-edge structures belong inside that inset and must not add a second outer inset. - Apply the inset exactly once on the view's root content container. Split panes and edge-to-edge structures belong inside that inset and must not add a second outer inset.
- Keep the 20px outer inset at responsive breakpoints unless the user explicitly changes this standard. - Keep the 20px outer inset at responsive breakpoints unless the user explicitly changes this standard.
## UI segments and tiles
- Treat segments and tiles as a binding two-level visual grammar independent of their content. A segment is the rounded grouped background; a tile is a rounded, individually bordered element inside it.
- Use only the shared `--segment-*` and `--tile-*` tokens for segment surfaces, borders, radii, insets, gaps, tile surfaces, shadows, widths, hover states, and selected contours. Do not create view-local substitute geometry.
- Segments use the light neutral group surface, a fine border, `--segment-radius`, one uniform inset, and one uniform gap. Segments remain flat and do not receive a floating shadow.
- Tiles use the common white or theme-equivalent surface, fine gray border, `--tile-radius`, and the single subtle `--tile-card-shadow`. Hover may change border and surface but must not lift or translate the tile.
- Status color belongs only in a small accent line, marker, data visualization, or selected contour. Never fill the complete tile with a status color.
- Selected tiles use the shared selected border/outline. Action tiles and add tiles use the same geometry as content tiles.
- Keep unused segment space intentionally empty: lay tiles out from the start at their shared compact or content width and never stretch a partial final row merely to fill the container.
- Tile content must never cross the tile boundary or cover a neighboring tile. Nothing may be clipped, clamped, truncated, or replaced by an ellipsis. Text and actions reflow inside the tile; media scales within the available inner area. If content does not fit, enlarge or reflow the tile.
- Avoid line breaks in compact tile labels where practical. If wrapping is necessary, wrap only at word boundaries: never split ordinary words or numeric values across lines. Use a wider or responsive tile whenever a complete word or value cannot fit.
## UI donut charts
- Every donut chart uses the same full 360-degree geometry: a continuous neutral-gray track and a colored clockwise value arc starting at 12 o'clock.
- Donut arcs use flat ends and the shared `--donut-ring-stroke`, `--donut-track`, value, and typography tokens. Do not introduce 270-degree gauges, open gaps, rounded arc caps, or local substitute geometry.
- Place the primary numeric value itself on the exact visual center of the ring. Append a percent sign or other short unit to its right on the same baseline in the smaller shared UI size without letting that unit shift the number away from the center. Supporting counts may remain as subdued secondary text inside the ring, but every text element must stay fully within the clear inner area and must never touch or overlap the ring at any supported value width.
- Multi-segment distributions retain their categorical segments, but use the same track, stroke proportion, start position, centered value treatment, and shared tokens as single-value donuts.
## UI branding position
- Use the Data Hub header as the binding position reference for the ASSESSOR logo, wordmark, and version.
- Keep the brand container geometry identical across every application view; do not add area- or view-specific brand padding, margin, alignment, height, or transforms.
- Switching views must never move the logo, wordmark, or version.
## Security and verification ## Security and verification
- Preserve local-only behavior until a server transition is explicitly implemented and documented. - Preserve local-only behavior until a server transition is explicitly implemented and documented.

View File

@ -1349,6 +1349,12 @@ Aktualisierung nach dem Schritt-11-Orchestrierungsvertikalschnitt vom 2026-08-04
| Datum | Entscheidung | Begründung | | Datum | Entscheidung | Begründung |
|---|---|---| |---|---|---|
| 2026-08-24 | Segmente und Kacheln folgen anwendungsweit einer gemeinsamen visuellen Grammatik: flacher abgerundeter Segmentcontainer mit einheitlichem Inset und Gap; darin gleichartige abgerundete Kacheln mit feiner Kontur, dezentem gemeinsamem Schatten und fester Inhaltsbreite. Statusfarbe bleibt Akzent oder Auswahlkontur. Unvollstaendige Zeilen werden nicht gestreckt. Jede Kachel begrenzt ihren Inhalt; Titel, Werte, Labels, Zusatztexte, Medien und Aktionen duerfen weder abgeschnitten, geklemmt, ellipsiert oder verborgen werden noch die Kachelgrenze ueberschreiten oder Nachbarkacheln ueberdecken. Notwendige Umbrueche erfolgen ausschliesslich an Wortgrenzen, Zahlen werden nicht getrennt. | Ein gemeinsamer Vertrag verhindert springende Geometrie und ansichtsspezifische Sonderkarten. Die harte Inhaltsgrenze schuetzt Nachbarkacheln, waehrend vollstaendige Inhalte die fachliche Lesbarkeit erhalten. Wenn Inhalt nicht vollstaendig passt, wird er intern neu angeordnet oder die Kachel verbreitert beziehungsweise responsiv neu angeordnet. |
| 2026-08-24 | Die neueste Nutzerkorrektur ersetzt fuer Finding-Titel die unmittelbar folgende fruehere 18-px-Reader-Entscheidung: Trefferlistentitel bleiben auf der 16-px-Inhaltsstufe, werden aber mit Medium 500 statt Semibold gesetzt; der Titel des geoeffneten Findings verwendet eine eigene 20-px-Detailtitelrolle mit Semibold. Die Finding-Abschnittsueberschrift bleibt 18 px, regulaere Matrixkacheln bleiben 16 px. | Die Trefferliste soll ruhig und scanbar bleiben, waehrend das geoeffnete Finding als primaerer Detailkontext klar hervortreten muss. Die getrennten semantischen Rollen loesen den Kontrast ohne die 16-px-Untergrenze fuer inhaltstragenden Text zu unterschreiten. |
| 2026-08-24 | Finding-Titel verwenden innerhalb von Work eine kompakte semantische Hierarchie: Reader-Titel und Finding-Abschnitt nutzen H3 mit 18 px; Titel in Trefferlisten und regulaeren Matrixkacheln nutzen H4 mit 16 px. Die globale H1-/H2-Hierarchie und die eigenstaendige Pathfinder-Micro-Card bleiben unveraendert. | Lange Scannerbezeichnungen benoetigen in Listen und Kacheln sonst zu viele Zeilen und verdraengen Status sowie Metadaten. Die vorhandenen Typografietokens erhalten die Lesbarkeitsuntergrenze von 16 px fuer inhaltstragenden Text und vermeiden lokale Sondergroessen. |
| 2026-08-24 | Alle Donutdiagramme verwenden einen gemeinsamen visuellen Vertrag: vollstaendige neutrale 360-Grad-Ringspur, farbiger Wertbogen im Uhrzeigersinn ab 12 Uhr, flache Bogenenden, gemeinsame Strichstaerke und eine mittige Primaerkennzahl mit kleiner Einheit auf derselben Grundlinie. Mehrteilige Verteilungen behalten ihre fachlichen Farbsegmente innerhalb derselben Geometrie. | Eine gemeinsame Form verhindert wechselnde Gauge-, Ring- und Prozentdarstellungen zwischen Datahub, Dashboard und Sidebar. Die neutrale Restspur macht den verbleibenden Anteil sichtbar; die zentrale Kennzahl bleibt unabhaengig von Kartenbreite und Legende unmittelbar lesbar. |
| 2026-08-24 | ASSESSOR verwendet anwendungsweit die lokal gebuendelte variable Schriftfamilie Inter. Die verbindliche Hierarchie ist H1 56 px/600, H2 32 px/600, H3 18 px/600, H4 und Fliesstext 16 px mit 600 beziehungsweise 400, UI-Labels und Buttons 14 px, Metadaten und Tabellenkoepfe 13 px sowie kompakte Finding-Badges 12 px. Uppercase bleibt Tabellenkoepfen und kurzen technischen Kennzeichnungen vorbehalten; regulaere Ueberschriften und Feldbezeichnungen verwenden Title Case. | Eine lokal gebuendelte Einzelschrift verhindert plattformabhaengige Fallbacks und visuelles Springen. Semantische Rollen statt lokaler Einzelwerte schaffen nachvollziehbare Abstufungen, waehrend 16 px fuer inhaltstragenden Text die bestehende Lesbarkeitsgrenze bewahrt und dichte Finding-Zustandszeilen visuell hinter den Titel treten. |
| 2026-08-24 | HTML-Berichte oberhalb von 1 MiB werden in Browser- und Backendlaufzeit direkt mit dem begrenzten lexikalischen Extraktor verarbeitet; kleinere Dokumente duerfen weiterhin den DOM-Adapter verwenden. Die isolierte Worker-Zeitgrenze von 30 Sekunden und alle bestehenden Ressourcenlimits bleiben unveraendert. | Scanner-HTML kann schon deutlich unter der bisherigen 4-MiB-Schwelle in eine DOM-Struktur expandieren, die das Worker-Zeitbudget ueberschreitet. Der vorhandene lineare Extraktor bewahrt Tabellen-, Locator-, Sicherheits- und Source-Extractor-Vertraege, ohne einen vollstaendigen DOM-Baum aufzubauen oder die Schutzgrenze aufzuweichen. |
| 2026-08-23 | Im Pathfinder gerenderte Knoteninformationen sind das eigenstaendige Designelement `Pathfinder Micro Card` und keine allgemeine Matrix-/Finding-TileCard. Der Vertrag besitzt die Varianten `finding` und `relation`; Category bleibt eine groessere Relation-Auspragung. Bestaetigte Geometrie, Typografie und Radien sind eigene Tokens beziehungsweise Konstanten, und die gesamte Komposition skaliert unveraendert mit Faktor 0,4 im Graphkoordinatensystem. | Graphkarten beschriften Knoten und muessen Beziehungen sichtbar lassen. Eine Uebernahme allgemeiner Kartenmasse oder nachtraegliche kamerainverse Typografie wuerde die bestaetigte Darstellung verdecken beziehungsweise ihre inneren Proportionen veraendern. Ein eigener Vertrag erlaubt gezielte Regressionen, ohne regulaere TileCards ausserhalb des Pathfinders zu beeinflussen. | | 2026-08-23 | Im Pathfinder gerenderte Knoteninformationen sind das eigenstaendige Designelement `Pathfinder Micro Card` und keine allgemeine Matrix-/Finding-TileCard. Der Vertrag besitzt die Varianten `finding` und `relation`; Category bleibt eine groessere Relation-Auspragung. Bestaetigte Geometrie, Typografie und Radien sind eigene Tokens beziehungsweise Konstanten, und die gesamte Komposition skaliert unveraendert mit Faktor 0,4 im Graphkoordinatensystem. | Graphkarten beschriften Knoten und muessen Beziehungen sichtbar lassen. Eine Uebernahme allgemeiner Kartenmasse oder nachtraegliche kamerainverse Typografie wuerde die bestaetigte Darstellung verdecken beziehungsweise ihre inneren Proportionen veraendern. Ein eigener Vertrag erlaubt gezielte Regressionen, ohne regulaere TileCards ausserhalb des Pathfinders zu beeinflussen. |
| 2026-08-23 | Scanner-Kontext wird zweistufig gespeichert: quellengetreue Reportabschnitte mit Prosa und Tabellen sowie typisierte EnvironmentObservations mit stabilen Schluesseln und explizitem Erfassungszustand. Findings bleiben ein getrennter Vertrag. | Nur die Rohstruktur verhindert Informationsverlust bei neuen PingCastle-Versionen; nur typisierte Fakten erlauben zugleich belastbare Dashboard-, Result-DB-, LLM- und Toolabfragen. Ein Zustandsfeld trennt echte Abwesenheit von leerer Ausgabe, fehlender Berechtigung, Lizenzgrenze und Parserluecke. | | 2026-08-23 | Scanner-Kontext wird zweistufig gespeichert: quellengetreue Reportabschnitte mit Prosa und Tabellen sowie typisierte EnvironmentObservations mit stabilen Schluesseln und explizitem Erfassungszustand. Findings bleiben ein getrennter Vertrag. | Nur die Rohstruktur verhindert Informationsverlust bei neuen PingCastle-Versionen; nur typisierte Fakten erlauben zugleich belastbare Dashboard-, Result-DB-, LLM- und Toolabfragen. Ein Zustandsfeld trennt echte Abwesenheit von leerer Ausgabe, fehlender Berechtigung, Lizenzgrenze und Parserluecke. |
| 2026-08-23 | Typisierte Scanner-EnvironmentObservations werden als eigener Unified-Result-Quelltyp `environment-observation` projiziert und niemals als Finding umgedeutet. Die Projektion ist ein abgeleitetes Readmodell ueber die bestehende Environment-Persistenz; sie bewahrt Original-/Normalwert und vollstaendige Provenienz, bietet eigene Environment-Facetten und nimmt diese Zeilen aus Finding-spezifischen Facetten heraus. | Result DB, Exporte, LLMs und weitere Tools benoetigen einen gemeinsamen timeslotgebundenen Zugriff, ohne aus DFL, OS, PKI, Trusts oder Coverage kuenstliche Schweregrade, IOEs, Abweichungen oder Reviews abzuleiten. Ein Readmodell vermeidet Datenkopien und eine weitere Migration, waehrend der getrennte SourceKind den Vertrag eindeutig haelt. | | 2026-08-23 | Typisierte Scanner-EnvironmentObservations werden als eigener Unified-Result-Quelltyp `environment-observation` projiziert und niemals als Finding umgedeutet. Die Projektion ist ein abgeleitetes Readmodell ueber die bestehende Environment-Persistenz; sie bewahrt Original-/Normalwert und vollstaendige Provenienz, bietet eigene Environment-Facetten und nimmt diese Zeilen aus Finding-spezifischen Facetten heraus. | Result DB, Exporte, LLMs und weitere Tools benoetigen einen gemeinsamen timeslotgebundenen Zugriff, ohne aus DFL, OS, PKI, Trusts oder Coverage kuenstliche Schweregrade, IOEs, Abweichungen oder Reviews abzuleiten. Ein Readmodell vermeidet Datenkopien und eine weitere Migration, waehrend der getrennte SourceKind den Vertrag eindeutig haelt. |
@ -1603,6 +1609,13 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode
| Risiko | Gegenmaßnahme | | Risiko | Gegenmaßnahme |
|---|---| |---|---|
| Lokale Kartenraster koennen Kacheln wieder auf Restbreite strecken; lange Titel, Werte, Labels, Zusatztexte, Medien oder Aktionen koennen abgeschnitten, ellipsiert oder verborgen werden, aus einer Kachel herausragen, Nachbarkacheln ueberdecken oder durch aggressive Umbruchregeln mitten im Wort getrennt werden. | Gemeinsame Segment-/Kacheltokens, feste Startausrichtung und eine zuletzt geladene Kachel-Vertragsschicht verwenden. Woerter nur an Wortgrenzen umbrechen und Zahlen zusammenhalten. Bei Platzmangel Inhalt intern neu anordnen oder die Kachel verbreitern beziehungsweise responsiv neu anordnen; Clipping, Clamp, Ellipse und Truncation sind keine zulaessigen Rueckfaelle. Statische Regressionen sichern Vertrag, relevante Ansichten und Kaskadenreihenfolge. |
| Trefferlistentitel und geoeffneter Finding-Titel koennen trotz unterschiedlicher Informationshierarchie optisch gleich laut oder in der falschen Reihenfolge erscheinen. | Trefferliste auf 16 px/500 und geoeffneten Detailtitel auf 20 px/600 ueber eigene zentrale Tokens binden; Finding-Abschnitt und Matrixkacheln getrennt regressionspruefen. |
| Finding-Badges und Outcome-Texte koennen bei derselben Groesse wie regulaere Labels eine lange Titelzeile optisch ueberladen und MITRE-Zuordnungen unnoetig umbrechen. | Fuer Finding-Zustandszeilen ausschliesslich den zentralen 12-px-Badgetoken verwenden; Farbe, Glyph, Rahmen und feste Dimensionsreihenfolge bleiben als zusaetzliche Bedeutungstraeger erhalten. |
| Lange Finding-Titel koennen bei Verwendung der globalen H2- oder allgemeinen Heading-Rolle Reader, Trefferlisten und Matrixkacheln visuell dominieren und Statusinformationen aus dem sichtbaren Bereich druecken. | Finding-spezifische Titelrollen regressionspruefen: Reader 18 px, Listen- und Matrixkacheln 16 px; Fliesstext bleibt mindestens 16 px. Globale Seitentitel und Pathfinder-Micro-Cards nicht ueber lokale Work-Regeln mitveraendern. |
| Unabhaengige Donutimplementierungen koennen erneut in offene 270-Grad-Gauges, abweichende Ringspuren, runde Bogenenden oder unterschiedlich platzierte Prozentwerte auseinanderlaufen; mehrteilige Verteilungen koennten den gemeinsamen Standard umgehen. | Vollkreis, Startposition, flache Enden, neutrale Spur, Strichstaerke und zentrale Wertebehandlung ueber gemeinsame Donuttokens und Regressionen fuer Datahub-Karten, Sidebar, Dashboardstatus und mehrteilige Risiko-Verteilung absichern. Neue Donuts muessen denselben Vertrag verwenden. |
| Lokale Systemschriften, konkurrierende Font-Stacks oder frei gesetzte Schriftgroessen koennen zwischen Betriebssystemen und Ansichten unterschiedliche Laufweiten, Zeilenumbrueche und Hierarchien erzeugen. | Inter als lokale WOFF2-Abhaengigkeit ohne CDN laden, den Font-Stack und alle semantischen Schriftrollen zentral tokenisieren und Import, Hierarchie, Mindestgroesse, erlaubte Grossschreibung sowie das Fehlen abweichender Display-Schriften regressionspruefen. |
| Mittelgrosse, strukturell komplexe Scanner-HTML-Berichte koennen vor der groessenbasierten DOM-Umschaltung die isolierte 30-Sekunden-Parsergrenze erreichen; ein pauschal hoeheres Timeout wuerde problematische Eingaben nur laenger binden. | Berichte oberhalb von 1 MiB frueh in den bestehenden begrenzten lexikalischen Extraktor routen, Workerzeit-, Speicher-, Tabellen- und Zeilengrenzen beibehalten und den 1,6-MiB-PingCastle-Pfad mit synthetischer Tabelle sowie `html-bounded-extraction`-Diagnose regressionspruefen. |
| Eine Anzeige der technischen Paketversion koennte faelschlich als aktueller fachlicher Verarbeitungsstand verstanden werden; eine lokal im React-Code hartcodierte Zahl koennte ausserdem vom Processingvertrag abweichen. | Die zentrale Konstante `CURRENT_ASSESSOR_PROCESSING_VERSION` aus dem Processingvertrag im Markenblock rendern, weder `v` noch `Version` voranstellen und Quelle, Wert, Position sowie kompakte Metadaten-Typografie regressionspruefen. | | Eine Anzeige der technischen Paketversion koennte faelschlich als aktueller fachlicher Verarbeitungsstand verstanden werden; eine lokal im React-Code hartcodierte Zahl koennte ausserdem vom Processingvertrag abweichen. | Die zentrale Konstante `CURRENT_ASSESSOR_PROCESSING_VERSION` aus dem Processingvertrag im Markenblock rendern, weder `v` noch `Version` voranstellen und Quelle, Wert, Position sowie kompakte Metadaten-Typografie regressionspruefen. |
| Ein Scanner-Reimport koennte den alten Lauf vor erfolgreicher Validierung loeschen, den falschen Timeslot beziehungsweise ein anderes Werkzeug ersetzen oder alte und neue Ergebnisse gleichzeitig in Work und Result DB anzeigen. | Vorschau und Abschluss kryptografisch an dasselbe Projekt-, Assessment- und ScanRun-Ziel binden, das erkannte Werkzeug gegen den aktiven Vorgaenger pruefen und Supersession samt neuem kanonischem Lauf in einer Repositorymutation committen. Den alten Lauf nicht loeschen; aktive Read-Modelle filtern `supersededByScanRunId`, waehrend der Projektexport beide Laeufe und ihre Beziehung erhaelt. | | Ein Scanner-Reimport koennte den alten Lauf vor erfolgreicher Validierung loeschen, den falschen Timeslot beziehungsweise ein anderes Werkzeug ersetzen oder alte und neue Ergebnisse gleichzeitig in Work und Result DB anzeigen. | Vorschau und Abschluss kryptografisch an dasselbe Projekt-, Assessment- und ScanRun-Ziel binden, das erkannte Werkzeug gegen den aktiven Vorgaenger pruefen und Supersession samt neuem kanonischem Lauf in einer Repositorymutation committen. Den alten Lauf nicht loeschen; aktive Read-Modelle filtern `supersededByScanRunId`, waehrend der Projektexport beide Laeufe und ihre Beziehung erhaelt. |
| Ein Bericht desselben Werkzeugs mit gleichem Dateinamen oder Datum koennte faelschlich als Reimport akzeptiert werden; bei Mehrdatei-Reports koennte eine Teilmenge oder eine zusaetzliche Datei den falschen Bestand ersetzen. Ein Client koennte ausserdem eine reine Vorschaupruefung umgehen. | Anzahl und vollstaendiges SHA-256-Multiset der Originaldateien in Preview und Complete vergleichen und dieselbe Invariante in Local- und PostgreSQL-Repository sowie beim Uebernehmen bereits importierter Ersatzlaeufe erzwingen. Fehlende Originalhashes, Offline-Fallback und jede Abweichung fail closed ablehnen; der bestehende Scan bleibt aktiv. | | Ein Bericht desselben Werkzeugs mit gleichem Dateinamen oder Datum koennte faelschlich als Reimport akzeptiert werden; bei Mehrdatei-Reports koennte eine Teilmenge oder eine zusaetzliche Datei den falschen Bestand ersetzen. Ein Client koennte ausserdem eine reine Vorschaupruefung umgehen. | Anzahl und vollstaendiges SHA-256-Multiset der Originaldateien in Preview und Complete vergleichen und dieselbe Invariante in Local- und PostgreSQL-Repository sowie beim Uebernehmen bereits importierter Ersatzlaeufe erzwingen. Fehlende Originalhashes, Offline-Fallback und jede Abweichung fail closed ablehnen; der bestehende Scan bleibt aktiv. |
@ -1780,6 +1793,15 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode
## 12. Änderungsprotokoll ## 12. Änderungsprotokoll
| Datum | Änderung | | Datum | Änderung |
| 2026-08-24 | Phase-7-Segment-/Kachelgrammatik nach Nutzerreferenz anwendungsweit umgesetzt und als Projektvorgabe verankert: gemeinsame Segment- und Kacheltokens fuer Flaeche, Kontur, Radius, Inset, Gap, Schatten, Auswahl und Breiten; flache Gruppencontainer; gleichartige Inhalts-, Aktions- und Hinzufuegekacheln; Statusfarben nur als Akzent; unvollstaendige Zeilen bleiben unstretched. Datahub, Dashboard, Sidebar, Work-TileGrid, DB Structure, Policy Analysis und statische Pathfinder-Karten verwenden den Vertrag. Die verbindliche Nutzerregel verbietet abgeschnittene, geklemmte, ellipsierte oder verborgene Titel, Werte, Labels, Zusatztexte und Aktionen. Dashboardstatuskarten wurden auf 290 beziehungsweise 210 px abgestimmt; Anzahl und Prozentwert stehen getrennt, sodass `Compliance` und Werte wie `453` vollstaendig bleiben. Das Donut-Raster nutzt auf Desktop sechs Spalten und die Matrix verteilt ihre feste Spaltenzahl ueber die volle Segmentbreite. Bei Platzmangel wird Inhalt neu angeordnet oder die Kachel verbreitert beziehungsweise responsiv umgebrochen. Sechzehn fokussierte Layout-/Donuttests und der Gesamtlauf mit 736 bestandenen sowie 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung mit 0 Rohfarben sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Finding-Titelhierarchie nach visueller Nutzerkorrektur nachgeschaerft: Trefferlistentitel bleiben 16 px, verwenden aber Medium 500 statt Semibold; der geoeffnete Finding-Titel verwendet eine neue zentrale 20-px-Detailtitelrolle mit Semibold. Finding-Abschnitt 18 px, Matrixkacheln 16 px und die 16-px-Untergrenze fuer Inhalt bleiben unveraendert. Titel brechen nur an Wortgrenzen. Drei fokussierte Typografieregressionen sowie der Gesamtlauf mit 736 bestandenen und 8 uebersprungenen Tests, Local-only-/Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Finding-Labels nach Nutzerkorrektur verdichtet: Outcome (`Failed`/`Passed`), Weight, Severity, Category, Score, CIS- und MITRE-Zuordnungen verwenden in Reader und regulaeren Matrixkacheln nun einen zentralen 12-px-Badgetoken statt der bisherigen 13-px-Labelstufe. Farb-, Glyph-, Rahmen-, Reihenfolge- und Statussemantik bleiben unveraendert; inhaltstragender Text wird nicht verkleinert. Fuenf fokussierte Typografieregressionen, 736 Standardtests, Local-only-Pruefung, Designpruefer und vollstaendiger Client-/Serverbuild erfolgreich; 8 optionale PostgreSQL-Tests uebersprungen. Keine Daten-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Finding-Typografie nach Nutzerkorrektur verdichtet: Der Reader-Titel und die Finding-Abschnittsueberschrift verwenden jetzt die bestehende H3-Rolle mit 18 px statt H2 mit 32 px; Titel in Trefferlisten und regulaeren Matrixkacheln verwenden die H4-Rolle mit 16 px statt der bisherigen 18-px-Heading-Rolle. Lange Scannerbezeichnungen beanspruchen damit weniger Zeilen, waehrend inhaltstragender Text die verbindliche 16-px-Untergrenze behaelt. Die Aenderung betrifft ausschliesslich bestehende Work-Selektoren; globale Seitentitel, Pathfinder-Micro-Cards, Daten-, Import- und Persistenzlogik bleiben unveraendert. Zwei neue Typografieregressionen, 735 Standardtests, Local-only-Pruefung, Designpruefer und vollstaendiger Client-/Serverbuild erfolgreich; 8 optionale PostgreSQL-Tests uebersprungen. Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Donut-Innenraum nach visueller Nutzerpruefung korrigiert: Zahl, Prozentzeichen und vorhandener Unterwert bleiben unveraendert im Ring, duerfen seine Kontur aber nicht beruehren. Die Zahl ist unabhaengig von der Einheit geometrisch zentriert; dreistellige Prozentwerte erhalten eine automatische Kompaktstufe. Fuer die kleineren Sidebar-Ringe sind Wert, Einheit und Einheitenabstand ueber eigene gemeinsame Tokens reduziert. 13 fokussierte Donut-, Sidebar- und Datahub-Regressionen, Local-only-Sicherheitspruefung, Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Daten-, Filter-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Donutstandard nach Nutzerreferenz vereinheitlicht und verbindlich dokumentiert: Datahub-Statuskarten und Sidebar-Severity-Karten verwenden statt offener 270-Grad-Gauges nun vollstaendige neutrale Ringspuren mit farbigem Uhrzeigersinnbogen ab 12 Uhr, flachen Enden und mittigem Prozentwert samt kleiner Einheit; der absolute Wert bleibt als dezente Sekundaerinformation erhalten. Dashboard-Statusringe verwenden dieselbe Spur und Strichstaerke. Die mehrteilige Risk-Severity-Verteilung behaelt ihre fachlichen Segmente, uebernimmt aber Geometrie, Spur, Strichstaerke, Startposition und zentrierte Prozentdarstellung. 730 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-Sicherheitspruefung, Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, Filter-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Typografiesystem vereinheitlicht und als verbindliche Projektvorgabe verankert: Inter Variable wird ausschliesslich lokal gebuendelt und ersetzt abweichende System- und Display-Schriften. Zentrale semantische Rollen definieren H1 bis H4, Fliesstext, UI-Labels, Metadaten, Tabellenkoepfe und Buttons; Uppercase ist auf Tabellenkoepfe und kurze technische Kennzeichnungen begrenzt. Datahub, Bereichsheader, Mandantendetail, Tabellen, Controls, Raw Data und Kennzahlen verwenden die gemeinsame Hierarchie. 730 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-Sicherheitspruefung, Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Designkonflikte bereinigt: Mandantenkarten und die gemeinsam verwendeten Data-/Pathfinder-Tiles tragen weder regulaere noch Hover-Schatten und verwenden keinen Hover-Lift; Interaktion bleibt ueber Haarlinie und Hintergrund sichtbar. Redundante Pathfinder-Null-Ueberschreibungen wurden entfernt, Radius-/Control-Verweise auf vorhandene Tokens vereinheitlicht, die dynamische Dashboard-Segmentbreite erhielt einen CSS-Fallback und berechnete Result-Spaltenbreiten bleiben als erlaubte Geometrie gekapselt. Die Schattenobergrenze fuer `legacy-views.css` wurde von sechs auf drei abgesenkt. 728 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-Sicherheitspruefung, vollstaendiger Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
| 2026-08-24 | Phase-13A-HTML-Import gegen den reproduzierten 30-Sekunden-Abbruch mittelgrosser Scannerberichte nachgehaertet: Die gemeinsame Browser-/Node-Umschaltung verwendet den bereits begrenzten lexikalischen HTML-Extraktor nun ab mehr als 1 MiB statt erst oberhalb von 4 MiB. Worker-Timeout und Ressourcenlimits bleiben unveraendert. Ein synthetischer 1,6-MiB-PingCastle-Bericht bestaetigt Format, Profil, Tabelleninhalt und `html-bounded-extraction`; 30 fokussierte Parser-/Worker-/PingCastle-Tests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich. Keine Realdaten gelesen, keine API-, Persistenz-, Schema- oder Profilversionsaenderung; Phase 13A bleibt abgeschlossen. |
| 2026-08-24 | Phase-7-Dashboard-IOE-Kennzahl als Filtereinstieg umgesetzt: `IOEs found` ist bei vorhandenem Trefferbestand anklickbar, setzt bestehende Dashboardfilter zurueck, uebergibt exakt die als explizite `IOE Found`/`IOEs found` gezaehlten Datensatz-IDs an die vorhandene Work-Teilmenge und oeffnet Work. Bei null Treffern bleibt das Steuerelement deaktiviert, damit kein ungefilterter Bestand erscheint. Zehn fokussierte Dashboard- und Filtertests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich; keine Import-, API-, Persistenz- oder Schemaaenderung. | | 2026-08-24 | Phase-7-Dashboard-IOE-Kennzahl als Filtereinstieg umgesetzt: `IOEs found` ist bei vorhandenem Trefferbestand anklickbar, setzt bestehende Dashboardfilter zurueck, uebergibt exakt die als explizite `IOE Found`/`IOEs found` gezaehlten Datensatz-IDs an die vorhandene Work-Teilmenge und oeffnet Work. Bei null Treffern bleibt das Steuerelement deaktiviert, damit kein ungefilterter Bestand erscheint. Zehn fokussierte Dashboard- und Filtertests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich; keine Import-, API-, Persistenz- oder Schemaaenderung. |
| 2026-08-24 | Phase-7-Dashboard-Ringspur nach Nutzerreferenz sichtbar gemacht: Der bislang mit dem Panel verschmelzende ungefuellte Anteil der oberen Statusringe verwendet nun den vorhandenen neutralgrauen Donut-Rahmentoken; Farbbogen, Geometrie, Werte und Interaktionen bleiben unveraendert. Sieben fokussierte Dashboardtests und vollstaendiger Client-/Server-Produktionsbuild erfolgreich; keine Import-, API-, Persistenz- oder Schemaaenderung. | | 2026-08-24 | Phase-7-Dashboard-Ringspur nach Nutzerreferenz sichtbar gemacht: Der bislang mit dem Panel verschmelzende ungefuellte Anteil der oberen Statusringe verwendet nun den vorhandenen neutralgrauen Donut-Rahmentoken; Farbbogen, Geometrie, Werte und Interaktionen bleiben unveraendert. Sieben fokussierte Dashboardtests und vollstaendiger Client-/Server-Produktionsbuild erfolgreich; keine Import-, API-, Persistenz- oder Schemaaenderung. |
| 2026-08-24 | Phase-7-Dashboard-Statusleiste nach Nutzerreferenz korrigiert: Die sechs gleichartigen Donutkarten sind durch eine fuenfteilige Leiste ersetzt. Compliance Score erhaelt den grossen Ring mit Formelhinweis; Deviation, Passed und Not Run verwenden kompakte Ringe mit realer Anzahl und Prozentwert. Not Applicable und Error stehen gemeinsam in einer Zusammenfassung neben `IOEs found`. Der IOE-Wert zaehlt ausschliesslich explizite `IOE Found`-/`IOEs found`-Quellenfelder und wird nicht aus Deviations abgeleitet; Statuskarten bleiben direkte Filtereinstiege in Work. 14 fokussierte Dashboard-, Donut- und Inset-Tests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich. Der bekannte Designcheck bleibt ausschliesslich an den unveraenderten Schattenregeln in `datahub-redesign.css` Zeilen 39/42 rot; keine Import-, API-, Persistenz- oder Schemaaenderung. | | 2026-08-24 | Phase-7-Dashboard-Statusleiste nach Nutzerreferenz korrigiert: Die sechs gleichartigen Donutkarten sind durch eine fuenfteilige Leiste ersetzt. Compliance Score erhaelt den grossen Ring mit Formelhinweis; Deviation, Passed und Not Run verwenden kompakte Ringe mit realer Anzahl und Prozentwert. Not Applicable und Error stehen gemeinsam in einer Zusammenfassung neben `IOEs found`. Der IOE-Wert zaehlt ausschliesslich explizite `IOE Found`-/`IOEs found`-Quellenfelder und wird nicht aus Deviations abgeleitet; Statuskarten bleiben direkte Filtereinstiege in Work. 14 fokussierte Dashboard-, Donut- und Inset-Tests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich. Der bekannte Designcheck bleibt ausschliesslich an den unveraenderten Schattenregeln in `datahub-redesign.css` Zeilen 39/42 rot; keine Import-, API-, Persistenz- oder Schemaaenderung. |

10
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "assessor", "name": "assessor",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"pg": "^8.22.0", "pg": "^8.22.0",
"react": "19.2.4", "react": "19.2.4",
@ -961,6 +962,15 @@
} }
} }
}, },
"node_modules/@fontsource-variable/inter": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
"integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",

View File

@ -21,6 +21,7 @@
"test:watch": "vitest --config vitest.config.ts" "test:watch": "vitest --config vitest.config.ts"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"pg": "^8.22.0", "pg": "^8.22.0",
"react": "19.2.4", "react": "19.2.4",

View File

@ -39,7 +39,7 @@ function AreaIcon({ area }: { area: AppArea }) {
const AREA_TOPBAR_TITLE: Record<AppArea, string> = { const AREA_TOPBAR_TITLE: Record<AppArea, string> = {
'db-structure': 'DB Structure', 'db-structure': 'DB Structure',
'data-hub': 'Data Hub', 'data-hub': 'Data Hub',
dashboard: 'Dashboard', dashboard: 'Client Info',
graph: 'Pathfinder', graph: 'Pathfinder',
work: 'Work', work: 'Work',
results: 'Result DB' results: 'Result DB'
@ -52,7 +52,7 @@ const AREA_TOPBAR_TITLE: Record<AppArea, string> = {
const NAV_AREAS = [ const NAV_AREAS = [
['db-structure', 'DB Structure'], ['db-structure', 'DB Structure'],
['data-hub', 'Datahub'], ['data-hub', 'Datahub'],
['dashboard', 'Dashboard'], ['dashboard', 'Client Info'],
['work', 'Work'], ['work', 'Work'],
['results', 'Result DB'], ['results', 'Result DB'],
['graph', 'Pathfinder'] ['graph', 'Pathfinder']

View File

@ -37,21 +37,23 @@ export function DonutMetricCard({
onClick onClick
}: DonutMetricCardProps) { }: DonutMetricCardProps) {
const boundedPercentage = Math.min(100, Math.max(0, Math.round(percentage))); const boundedPercentage = Math.min(100, Math.max(0, Math.round(percentage)));
const arcLength = boundedPercentage * 0.75; const dashArray = String(boundedPercentage) + ' ' + String(100 - boundedPercentage);
const dashArray = String(arcLength) + ' ' + String(100 - arcLength);
const progressStyle = { '--donut-progress-dash': dashArray } as CSSProperties; const progressStyle = { '--donut-progress-dash': dashArray } as CSSProperties;
const classes = ['donut-metric-card', 'donut-metric-card--' + tone, className].filter(Boolean).join(' '); const classes = ['donut-metric-card', 'donut-metric-card--' + tone, className].filter(Boolean).join(' ');
const ariaLabel = label + ': ' + value.toLocaleString('en-US') + ', ' + boundedPercentage + '%'; const ariaLabel = label + ': ' + value.toLocaleString('en-US') + ', ' + boundedPercentage + '%';
const content = <> const content = <>
<h3 className="donut-metric-card__title">{label}</h3> <h3 className="donut-metric-card__title">{label}</h3>
<div className="donut-metric-card__plot"> <div className="donut-metric-card__plot">
<svg viewBox="0 0 200 200" aria-hidden="true"> <svg viewBox="0 0 120 120" aria-hidden="true">
<circle className="donut-metric-card__track" cx="100" cy="100" r="78" pathLength="100" strokeDasharray="75 25" /> <circle className="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100" />
<circle key={label + '-' + value + '-' + boundedPercentage} className="donut-metric-card__progress" cx="100" cy="100" r="78" pathLength="100" strokeDasharray={dashArray} style={progressStyle}/> <circle key={label + '-' + value + '-' + boundedPercentage} className="donut-metric-card__progress" cx="60" cy="60" r="48" pathLength="100" strokeDasharray={dashArray} style={progressStyle}/>
</svg> </svg>
<div className="donut-metric-card__values"> <div className="donut-metric-card__values">
<strong>{value.toLocaleString('en-US')}{valueSuffix}</strong> <div className={`donut-metric-card__primary${boundedPercentage >= 100 ? ' is-compact' : ''}`}>
{showPercentage && <span>{boundedPercentage}%</span>} <strong>{showPercentage ? boundedPercentage : value.toLocaleString('en-US')}{showPercentage ? '' : valueSuffix}</strong>
{showPercentage && <span>%</span>}
</div>
{showPercentage && <small>{value.toLocaleString('en-US')}{valueSuffix}</small>}
</div> </div>
</div> </div>
</>; </>;

View File

@ -21,6 +21,10 @@ function clampColumnWidth(width: number): number {
return Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, Math.round(width))); return Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, Math.round(width)));
} }
function columnWidthStyle(width: number): CSSProperties {
return { width: `${width}px` };
}
function measuredColumnWidths(table: HTMLTableElement, columnCount: number): number[] { function measuredColumnWidths(table: HTMLTableElement, columnCount: number): number[] {
const headers = Array.from(table.tHead?.rows[0]?.cells ?? []); const headers = Array.from(table.tHead?.rows[0]?.cells ?? []);
return Array.from({ length: columnCount }, (_, index) => clampColumnWidth(headers[index]?.getBoundingClientRect().width || MIN_COLUMN_WIDTH)); return Array.from({ length: columnCount }, (_, index) => clampColumnWidth(headers[index]?.getBoundingClientRect().width || MIN_COLUMN_WIDTH));
@ -101,7 +105,7 @@ export function ResizableResultTable({ headers, children, enabled = false, class
return ( return (
<table ref={tableRef} className={`${className}${enabled ? ' is-column-resizable' : ''}${appliedWidths.length ? ' has-custom-column-widths' : ''}`} style={tableStyle}> <table ref={tableRef} className={`${className}${enabled ? ' is-column-resizable' : ''}${appliedWidths.length ? ' has-custom-column-widths' : ''}`} style={tableStyle}>
{enabled && <caption className="result-column-instructions">Column widths can be changed by dragging a header separator or fitted by double-clicking it.</caption>} {enabled && <caption className="result-column-instructions">Column widths can be changed by dragging a header separator or fitted by double-clicking it.</caption>}
{appliedWidths.length > 0 && <colgroup>{appliedWidths.map((width, index) => <col key={index} style={{ width: `${width}px` }} />)}</colgroup>} {appliedWidths.length > 0 && <colgroup>{appliedWidths.map((width, index) => <col key={index} style={columnWidthStyle(width)} />)}</colgroup>}
<thead><tr>{headers.map((header, index) => <th key={index}>{header}{enabled && index < columnCount - 1 && <span <thead><tr>{headers.map((header, index) => <th key={index}>{header}{enabled && index < columnCount - 1 && <span
className="result-column-resizer" className="result-column-resizer"
role="separator" role="separator"

View File

@ -51,11 +51,11 @@ function DashboardStatusMetric({ label, value, percentage, tone, large = false,
<circle className="dashboard-status-ring-track" cx="60" cy="60" r="48" pathLength="100" /> <circle className="dashboard-status-ring-track" cx="60" cy="60" r="48" pathLength="100" />
<circle className="dashboard-status-ring-progress" cx="60" cy="60" r="48" pathLength="100" strokeDasharray={`${roundedPercentage} ${100 - roundedPercentage}`} /> <circle className="dashboard-status-ring-progress" cx="60" cy="60" r="48" pathLength="100" strokeDasharray={`${roundedPercentage} ${100 - roundedPercentage}`} />
</svg> </svg>
<span><strong>{roundedPercentage}</strong><small>%</small></span> <span><span className={`dashboard-donut-primary${roundedPercentage >= 100 ? ' is-compact' : ''}`}><strong>{roundedPercentage}</strong><small>%</small></span></span>
</div> </div>
<div className="dashboard-status-copy"> <div className="dashboard-status-copy">
<span>{label}</span> <span>{label}</span>
<strong>{large ? `${roundedPercentage} %` : <>{value.toLocaleString('en-US')} <small>· {roundedPercentage}%</small></>}</strong> <strong>{large ? `${roundedPercentage} %` : <>{value.toLocaleString('en-US')}<small>{roundedPercentage}%</small></>}</strong>
{helper && <small>{helper}</small>} {helper && <small>{helper}</small>}
</div> </div>
</>; </>;
@ -328,24 +328,27 @@ function RiskProfile({ sourceFacet, categoryFacet, severityFacet, records, profi
) : <p className="dashboard-empty">No risk categories available.</p>} ) : <p className="dashboard-empty">No risk categories available.</p>}
<div className="dashboard-risk-severity"> <div className="dashboard-risk-severity">
{severities.length > 0 ? <> {severities.length > 0 ? <>
<svg className="dashboard-risk-donut" viewBox="0 0 120 120" role="img" aria-label="Risk severity distribution"> <div className="dashboard-risk-donut" role="img" aria-label="Risk severity distribution: 100%">
<circle className="dashboard-risk-donut-track" cx="60" cy="60" r="48" pathLength="100" /> <svg viewBox="0 0 120 120" aria-hidden="true">
{severities.map(severity => { <circle className="dashboard-risk-donut-track" cx="60" cy="60" r="48" pathLength="100" />
const percentage = share(severity.count, severityTotal); {severities.map(severity => {
const offset = severityOffset; const percentage = share(severity.count, severityTotal);
severityOffset += percentage; const offset = severityOffset;
return <circle severityOffset += percentage;
className={`dashboard-risk-donut-segment severity-${severity.value}`} return <circle
cx="60" className={`dashboard-risk-donut-segment severity-${severity.value}`}
cy="60" cx="60"
r="48" cy="60"
key={severity.value} r="48"
pathLength="100" key={severity.value}
strokeDasharray={`${percentage} ${100 - percentage}`} pathLength="100"
strokeDashoffset={-offset} strokeDasharray={`${percentage} ${100 - percentage}`}
/>; strokeDashoffset={-offset}
})} />;
</svg> })}
</svg>
<span><span className="dashboard-donut-primary is-compact"><strong>100</strong><small>%</small></span></span>
</div>
<ul className="dashboard-risk-legend"> <ul className="dashboard-risk-legend">
{severities.map(severity => ( {severities.map(severity => (
<li key={severity.value}> <li key={severity.value}>

View File

@ -34,10 +34,10 @@ import { toolIdForRecord } from '../../importers/status-normalization';
* zweizeilige Titel bleibt durch den Overscan unbemerkbar. * zweizeilige Titel bleibt durch den Overscan unbemerkbar.
*/ */
const RECORD_ROW_HEIGHT = 78; const RECORD_ROW_HEIGHT = 78;
/** Mindestbreite einer Kachel, siehe `.tile-grid` in legacy-views.css. */ /** Verbindliche Inhaltskachelbreite, siehe `--tile-content-width`. */
const TILE_MIN_WIDTH = 280; const TILE_MIN_WIDTH = 320;
/** `gap` von `.tile-grid` (--panel-inset-Token, 20px). */ /** Einheitlicher Segmentabstand, siehe `--segment-gap`. */
const TILE_GAP = 20; const TILE_GAP = 8;
/** `padding` von `.tile-grid` (--panel-inset-Token, 20px je Seite). */ /** `padding` von `.tile-grid` (--panel-inset-Token, 20px je Seite). */
const TILE_GRID_INSET = 20; const TILE_GRID_INSET = 20;
/** Geschaetzte Kachelzeilenhoehe inklusive Zeilenabstand, aus demselben Grund /** Geschaetzte Kachelzeilenhoehe inklusive Zeilenabstand, aus demselben Grund
@ -316,7 +316,7 @@ export function DatasetViewer() {
// literalen Inline-Style-Objekts, damit die Darstellung sonst in // literalen Inline-Style-Objekts, damit die Darstellung sonst in
// src/styles bleibt (siehe FloatingResultsWindow.tsx fuer denselben // src/styles bleibt (siehe FloatingResultsWindow.tsx fuer denselben
// Kompromiss bei einer anderen laufzeitberechneten Position). // Kompromiss bei einer anderen laufzeitberechneten Position).
const tileGridStyle = { gridTemplateColumns: `repeat(${tileColumns}, 1fr)` }; const tileGridStyle = { gridTemplateColumns: `repeat(${tileColumns}, minmax(var(--tile-content-width), 1fr))` };
const tileTopSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingTop }; const tileTopSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingTop };
const tileBottomSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingBottom }; const tileBottomSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingBottom };
const recordTopSpacerStyle = { height: recordWindow.paddingTop }; const recordTopSpacerStyle = { height: recordWindow.paddingTop };
@ -332,7 +332,7 @@ export function DatasetViewer() {
{!records.length ? ( {!records.length ? (
<EmptyWorkspace showOpenData={false} /> <EmptyWorkspace showOpenData={false} />
) : view === 'columns' ? ( ) : view === 'columns' ? (
<div className="viewer-scroll" ref={setTileScrollNode}><div className="tile-grid" style={tileGridStyle}> <div className="viewer-scroll tile-view-scroll" ref={setTileScrollNode}><div className="tile-grid" style={tileGridStyle}>
{tileWindow.paddingTop > 0 && <div aria-hidden="true" style={tileTopSpacerStyle} />} {tileWindow.paddingTop > 0 && <div aria-hidden="true" style={tileTopSpacerStyle} />}
{records.slice(tileWindow.startIndex * tileColumns, tileWindow.endIndex * tileColumns).map(record => ( {records.slice(tileWindow.startIndex * tileColumns, tileWindow.endIndex * tileColumns).map(record => (
<TileCard key={record.id} record={record} title={recordTitle(record)} bookmarked={Boolean(reviews[record.id]?.include)} resultColor={resultColor(record.id)} toolId={activeSheet?.toolId ?? dataset?.profileId} search={search} onOpen={() => { <TileCard key={record.id} record={record} title={recordTitle(record)} bookmarked={Boolean(reviews[record.id]?.include)} resultColor={resultColor(record.id)} toolId={activeSheet?.toolId ?? dataset?.profileId} search={search} onOpen={() => {

View File

@ -1,6 +1,10 @@
import type { DecodedHtmlDocument, DecodedHtmlTable } from './runtime-adapters'; import type { DecodedHtmlDocument, DecodedHtmlTable } from './runtime-adapters';
const MAXIMUM_DOM_HTML_CHARACTERS = 4 * 1024 * 1024; // Real-world scanner reports can exceed the isolated worker's time budget well
// before four MiB when their markup expands into a large DOM. Route reports
// above one MiB through the bounded lexical parser, which preserves the table
// and source-extractor contract without constructing the complete DOM tree.
const MAXIMUM_DOM_HTML_CHARACTERS = 1 * 1024 * 1024;
const MAXIMUM_EXTRACTED_TABLES = 10_001; const MAXIMUM_EXTRACTED_TABLES = 10_001;
const MAXIMUM_EXTRACTED_ROWS_PER_TABLE = 250_001; const MAXIMUM_EXTRACTED_ROWS_PER_TABLE = 250_001;
const MAXIMUM_EXTRACTED_ROWS_TOTAL = 500_001; const MAXIMUM_EXTRACTED_ROWS_TOTAL = 500_001;

View File

@ -2,6 +2,7 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from './app/App'; import { App } from './app/App';
import { AppProvider } from './app/AppProvider'; import { AppProvider } from './app/AppProvider';
import '@fontsource-variable/inter/wght.css';
import './styles/layers.css'; import './styles/layers.css';
import './styles/ui-tokens.css'; import './styles/ui-tokens.css';
/* styles/ui-components.css wird absichtlich noch nicht global geladen. /* styles/ui-components.css wird absichtlich noch nicht global geladen.
@ -25,6 +26,7 @@ import './styles/import-workflow.css';
import './styles/legacy-views.css'; import './styles/legacy-views.css';
import './styles/obsidian-graph.css'; import './styles/obsidian-graph.css';
import './styles/functional-icons.css'; import './styles/functional-icons.css';
import './styles/tile-contract.css';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>

View File

@ -1,14 +1,19 @@
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body, #root { width: 100%; min-width: 0; min-height: 100%; margin: 0; } html, body, #root { width: 100%; min-width: 0; min-height: 100%; margin: 0; }
body { min-height: 100vh; overflow: hidden; color: var(--text); background: var(--bg); font: var(--ui-body-weight) var(--ui-body-size)/1.45 var(--font); } body { min-height: 100vh; overflow: hidden; color: var(--text); background: var(--bg); font: var(--fw-regular) var(--type-body-size)/1.5 var(--font-ui); }
button, input, select, textarea { color: inherit; font: inherit; } button, input, select, textarea { color: inherit; font: inherit; }
button, select { cursor: pointer; } button, select { cursor: pointer; }
button:disabled, select:disabled { cursor: not-allowed; opacity: .42; } button:disabled, select:disabled { cursor: not-allowed; opacity: .42; }
h1, h2, h3, h4, p { margin-top: 0; } h1, h2, h3, h4, p { margin-top: 0; }
h1 { font-size: var(--type-h1-size); font-weight: var(--fw-semibold); line-height: 1.1; letter-spacing: -.04em; }
h2 { font-size: var(--type-h2-size); font-weight: var(--fw-semibold); line-height: 1.15; letter-spacing: -.03em; }
h3 { font-size: var(--type-h3-size); font-weight: var(--fw-semibold); line-height: 1.25; letter-spacing: 0; }
h4 { font-size: var(--type-h4-size); font-weight: var(--fw-semibold); line-height: 1.3; letter-spacing: 0; }
svg { display: block; width: 17px; height: 17px; } svg { display: block; width: 17px; height: 17px; }
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; }
th, td { padding: 9px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; overflow-wrap: anywhere; } th, td { padding: 9px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
th { position: sticky; top: 0; z-index: 1; color: var(--muted); background: var(--panel-soft); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; } td { font-size: var(--type-body-size); font-weight: var(--fw-regular); }
th { position: sticky; top: 0; z-index: 1; color: var(--muted); background: var(--panel-soft); font-size: var(--type-table-head-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; }
tbody tr:hover td { background: var(--panel-hover); } tbody tr:hover td { background: var(--panel-hover); }
pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; } pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
mark { padding: 1px 2px; color: var(--state-active-text); background: var(--state-active); } mark { padding: 1px 2px; color: var(--state-active-text); background: var(--state-active); }
@ -19,13 +24,13 @@ button, .btn, .icon-btn, .tab-btn, .bookmark-btn, .results-btn {
border: 1px solid var(--button-border); border: 1px solid var(--button-border);
color: var(--button-text); color: var(--button-text);
background: var(--button-bg); background: var(--button-bg);
font-size: var(--ui-control-size); font-size: var(--type-button-size);
font-weight: 500; font-weight: var(--fw-semibold);
line-height: 1.15; line-height: 1.15;
} }
.select, .input { .select, .input {
min-height: var(--control-height); border: var(--border-hair) solid var(--line); border-radius: 0; min-height: var(--control-height); border: var(--border-hair) solid var(--line); border-radius: 0;
color: var(--text); background-color: var(--panel-raised); font-size: var(--ui-control-size); font-weight: 500; color: var(--text); background-color: var(--panel-raised); font-size: var(--type-label-size); font-weight: var(--fw-regular);
} }
/* Verbindliche Buttonform: eine Hoehe, ein Radius, ein waagerechtes Polster. /* Verbindliche Buttonform: eine Hoehe, ein Radius, ein waagerechtes Polster.
Senkrecht traegt der Knopf kein eigenes Polster - die Hoehe kommt aus Senkrecht traegt der Knopf kein eigenes Polster - die Hoehe kommt aus
@ -42,7 +47,7 @@ button, .btn, .icon-btn, .tab-btn, .bookmark-btn, .results-btn {
border: 1px solid var(--button-border); border: 1px solid var(--button-border);
color: var(--button-marked-text); color: var(--button-marked-text);
background: var(--button-marked-bg); background: var(--button-marked-bg);
font-weight: 600; font-weight: var(--fw-semibold);
} }
.btn.primary:hover, .btn--primary:hover, .btn.active:hover, .tab-btn.active:hover, .bookmark-btn[aria-pressed="true"]:hover, .icon-btn[aria-pressed="true"]:hover, .results-btn.is-open:hover { .btn.primary:hover, .btn--primary:hover, .btn.active:hover, .tab-btn.active:hover, .bookmark-btn[aria-pressed="true"]:hover, .icon-btn[aria-pressed="true"]:hover, .results-btn.is-open:hover {
border-color: var(--button-border); border-color: var(--button-border);

View File

@ -13,13 +13,13 @@ mark, .search-highlight {
.empty-brand-logo { position: relative; display: block; width: 82px; height: 82px; } .empty-brand-logo { position: relative; display: block; width: 82px; height: 82px; }
.empty-brand-logo img { position: absolute; inset: 0; width: 82px; height: 82px; object-fit: contain; } .empty-brand-logo img { position: absolute; inset: 0; width: 82px; height: 82px; object-fit: contain; }
.empty-brand-logo-light { opacity: 0; } .empty-brand-logo-light { opacity: 0; }
.empty-brand-mark strong { color: var(--heading); font-size: clamp(34px, 4vw, 58px); font-weight: 750; line-height: 1; letter-spacing: .08em; } .empty-brand-mark strong { color: var(--heading); font-size: clamp(34px, 4vw, 58px); font-weight: var(--fw-bold); line-height: 1; letter-spacing: .08em; }
.empty-open-data { position: relative; display: flex; align-items: center; gap: 13px; width: min(310px, 100%); min-height: 50px; padding: 10px 20px; border: 0; border-radius: 0 var(--legacy-radius-md) var(--legacy-radius-md) 0; color: var(--text); background: var(--panel-raised); box-shadow: none; font-size: var(--ui-heading-size); font-weight: 750; text-align: left; } .empty-open-data { position: relative; display: flex; align-items: center; gap: 13px; width: min(310px, 100%); min-height: 50px; padding: 10px 20px; border: 0; border-radius: 0 var(--legacy-radius-md) var(--legacy-radius-md) 0; color: var(--text); background: var(--panel-raised); box-shadow: none; font-size: var(--ui-heading-size); font-weight: var(--fw-semibold); text-align: left; }
.empty-open-data::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--legacy-accent); } .empty-open-data::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--legacy-accent); }
.empty-open-data svg { flex: 0 0 25px; width: 25px; height: 25px; } .empty-open-data svg { flex: 0 0 25px; width: 25px; height: 25px; }
.empty-open-data:hover { color: var(--heading); background: var(--panel-hover); } .empty-open-data:hover { color: var(--heading); background: var(--panel-hover); }
.empty-open-data:active { background: var(--panel-soft); } .empty-open-data:active { background: var(--panel-soft); }
.empty-supported-formats { color: var(--muted); font-size: var(--ui-body-size); font-weight: 500; line-height: 1.8; letter-spacing: .025em; } .empty-supported-formats { color: var(--muted); font-size: var(--ui-body-size); font-weight: var(--fw-regular); line-height: 1.8; letter-spacing: .025em; }
html[data-theme="light"] .empty-brand-logo-dark { opacity: 0; } html[data-theme="light"] .empty-brand-logo-dark { opacity: 0; }
html[data-theme="light"] .empty-brand-logo-light { opacity: 1; } html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
/* Die drei Light-Sonderregeln fuer .empty-open-data sind entfallen: die /* Die drei Light-Sonderregeln fuer .empty-open-data sind entfallen: die
@ -39,15 +39,15 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
.record-list .finding-card.active, .record-list .finding-card.active:hover, .record-list .finding-card.active:focus-visible { z-index: 1; outline: 0; border-color: transparent; border-left: 0; color: var(--state-selected-flat-text); background: var(--state-selected-flat); box-shadow: none; } .record-list .finding-card.active, .record-list .finding-card.active:hover, .record-list .finding-card.active:focus-visible { z-index: 1; outline: 0; border-color: transparent; border-left: 0; color: var(--state-selected-flat-text); background: var(--state-selected-flat); box-shadow: none; }
.record-list .finding-card.active h3, .record-list .finding-card.active .finding-card-status { color: var(--state-selected-flat-text); } .record-list .finding-card.active h3, .record-list .finding-card.active .finding-card-status { color: var(--state-selected-flat-text); }
.record-list .finding-card-actions, .record-list .finding-badges { display: none; } .record-list .finding-card-actions, .record-list .finding-badges { display: none; }
.record-list .finding-card h3 { margin-top: 1px; font-size: var(--ui-heading-size); font-weight: 700; line-height: 1.35; } .record-list .finding-card h3 { margin-top: 1px; font-size: var(--type-h4-size); font-weight: var(--fw-content-medium); line-height: 1.35; }
.finding-card-status { display: none; } .finding-card-status { display: none; }
.record-list .finding-card-status { display: block; margin: 5px 0 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.2; } .record-list .finding-card-status { display: block; margin: 5px 0 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.2; }
.finding-hex { display: grid; place-items: center; width: 29px; height: 34px; margin-top: 1px; color: var(--sev-on); background: var(--severity, var(--muted)); clip-path: polygon(50% 0,94% 24%,94% 76%,50% 100%,6% 76%,6% 24%); font-size: var(--ui-control-size); font-weight: 900; } .finding-hex { display: grid; place-items: center; width: 29px; height: 34px; margin-top: 1px; color: var(--sev-on); background: var(--severity, var(--muted)); clip-path: polygon(50% 0,94% 24%,94% 76%,50% 100%,6% 76%,6% 24%); font-size: var(--ui-control-size); font-weight: var(--fw-bold); }
.finding-hex > span { transform: translate(-3px, 1px); } .finding-hex > span { transform: translate(-3px, 1px); }
.reader-title-hex { position: relative; display: block; width: 30px; height: 34px; margin-top: 1px; background: var(--severity, var(--muted)); clip-path: polygon(50% 0,94% 24%,94% 76%,50% 100%,6% 76%,6% 24%); } .reader-title-hex { position: relative; display: block; width: 30px; height: 34px; margin-top: 1px; background: var(--severity, var(--muted)); clip-path: polygon(50% 0,94% 24%,94% 76%,50% 100%,6% 76%,6% 24%); }
.reader-title-hex::after { content: ''; position: absolute; left: 2px; top: 14px; width: 8px; height: 4px; border-left: 2px solid var(--sev-on); border-bottom: 2px solid var(--sev-on); transform: rotate(-45deg); pointer-events: none; } .reader-title-hex::after { content: ''; position: absolute; left: 2px; top: 14px; width: 8px; height: 4px; border-left: 2px solid var(--sev-on); border-bottom: 2px solid var(--sev-on); transform: rotate(-45deg); pointer-events: none; }
.finding-card h3 { margin: 0; color: var(--heading); font-size: var(--ui-heading-size); font-weight: 650; line-height: 1.42; overflow-wrap: anywhere; } .finding-card h3 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); line-height: 1.42; overflow-wrap: normal; word-break: normal; hyphens: manual; }
.workspace-label { display: inline-flex; align-items: center; justify-content: center; margin-left: 6px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-weight: 700; font-variant-numeric: tabular-nums; vertical-align: middle; } .workspace-label { display: inline-flex; align-items: center; justify-content: center; margin-left: 6px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; vertical-align: middle; }
.finding-badges, .mitre-line { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } .finding-badges, .mitre-line { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.finding-card-actions { display: grid; align-content: start; gap: 8px; } .finding-card-actions { display: grid; align-content: start; gap: 8px; }
.bookmark-btn { color: var(--button-text); border: 1px solid var(--button-border); border-radius: var(--btn-radius); background: var(--button-bg); } .bookmark-btn { color: var(--button-text); border: 1px solid var(--button-border); border-radius: var(--btn-radius); background: var(--button-bg); }
@ -89,18 +89,18 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
/* Der Fundname nutzt die volle Breite der Lesespalte; die Schaltflaechen /* Der Fundname nutzt die volle Breite der Lesespalte; die Schaltflaechen
ruecken in eine eigene Zeile darunter, statt den Titel zu beschneiden. */ ruecken in eine eigene Zeile darunter, statt den Titel zu beschneiden. */
.reader-heading { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--legacy-space-3) 16px; align-items: start; padding-bottom: 28px; border-bottom: 1px solid var(--line); } .reader-heading { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--legacy-space-3) 16px; align-items: start; padding-bottom: 28px; border-bottom: 1px solid var(--line); }
.reader-heading h2 { margin: 0; color: var(--heading); font-size: var(--ui-reading-heading-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; } .reader-heading h2 { margin: 0; color: var(--heading); font-size: var(--type-detail-title-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; overflow-wrap: normal; word-break: normal; hyphens: manual; }
.reader-heading-actions { display: flex; grid-column: 1 / -1; align-items: center; justify-content: flex-end; gap: 10px; align-self: center; } .reader-heading-actions { display: flex; grid-column: 1 / -1; align-items: center; justify-content: flex-end; gap: 10px; align-self: center; }
/* Der PingCastle-Score-Erlaeuterungsabsatz wurde samt Markup entfernt. */ /* Der PingCastle-Score-Erlaeuterungsabsatz wurde samt Markup entfernt. */
.bookmark-label { color: var(--text); font-size: var(--ui-control-size); font-weight: var(--ui-label-weight); } .bookmark-label { color: var(--text); font-size: var(--ui-control-size); font-weight: var(--ui-label-weight); }
.results-detail-btn { min-width: 136px; } .results-detail-btn { min-width: 136px; }
.dashboard-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 28px auto 24px; } .dashboard-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 28px auto 24px; }
.dashboard-grid label, .review-grid > label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-value-size); font-weight: 750; letter-spacing: .035em; text-transform: uppercase; } .dashboard-grid label, .review-grid > label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); letter-spacing: 0; text-transform: none; }
.dashboard-grid input, .dashboard-grid select, .review-grid textarea { width: 100%; min-height: 39px; padding: 8px 11px; border: var(--border-hair) solid var(--line-strong); border-radius: var(--legacy-radius-sm); color: var(--text); background-color: var(--surface-inset); text-transform: none; } .dashboard-grid input, .dashboard-grid select, .review-grid textarea { width: 100%; min-height: 39px; padding: 8px 11px; border: var(--border-hair) solid var(--line-strong); border-radius: var(--legacy-radius-sm); color: var(--text); background-color: var(--surface-inset); text-transform: none; }
.reader-summary, .review-grid, .all-fields { margin-top: 14px; padding: 20px 0; border: 0; border-bottom: 1px solid var(--line); background: transparent; } .reader-summary, .review-grid, .all-fields { margin-top: 14px; padding: 20px 0; border: 0; border-bottom: 1px solid var(--line); background: transparent; }
/* Traegt seit der Zusammenfuehrung mit "Check title" die Pruefbeschreibung der /* Traegt seit der Zusammenfuehrung mit "Check title" die Pruefbeschreibung der
Regel, also einen Satz. Versalien waeren dafuer unlesbar. */ Regel, also einen Satz. Versalien waeren dafuer unlesbar. */
.reader-summary h3 { margin-bottom: var(--legacy-space-5); color: var(--heading); font-size: var(--ui-reading-heading-size); text-transform: none; } .reader-summary h3 { margin-bottom: var(--legacy-space-5); color: var(--heading); font-size: var(--type-h3-size); text-transform: none; }
.reader-summary h4 { margin: 0 0 7px; color: var(--heading); font-size: var(--ui-reading-size); text-transform: uppercase; } .reader-summary h4 { margin: 0 0 7px; color: var(--heading); font-size: var(--ui-reading-size); text-transform: uppercase; }
.reader-summary > div { padding: 0 0 15px; margin: 0 0 15px; border-bottom: 1px solid var(--line); } .reader-summary > div { padding: 0 0 15px; margin: 0 0 15px; border-bottom: 1px solid var(--line); }
.reader-summary > div:last-child { margin-bottom: 0; } .reader-summary > div:last-child { margin-bottom: 0; }
@ -424,12 +424,12 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
.donut-metric-rack { .donut-metric-rack {
display: grid; display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr)); grid-template-columns: repeat(6, minmax(0, 1fr));
gap: var(--space-2); gap: var(--segment-gap);
padding: var(--space-3); padding: var(--segment-inset);
border: var(--border-hair) solid var(--donut-border); border: var(--border-hair) solid var(--segment-border);
border-radius: var(--donut-rack-radius); border-radius: var(--segment-radius);
background: var(--donut-rack-surface); background: var(--segment-surface);
box-shadow: var(--donut-rack-shadow); box-shadow: none;
} }
.donut-metric-card { .donut-metric-card {
position: relative; position: relative;
@ -439,11 +439,11 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
min-height: var(--donut-card-min-h); min-height: var(--donut-card-min-h);
padding: var(--space-5) var(--space-3) var(--space-2); padding: var(--space-5) var(--space-3) var(--space-2);
overflow: hidden; overflow: hidden;
border: var(--border-hair) solid var(--donut-border); border: var(--border-hair) solid var(--tile-border);
border-radius: var(--donut-card-radius); border-radius: var(--tile-radius);
color: var(--donut-all); color: var(--donut-all);
background: linear-gradient(145deg, var(--donut-card-surface), var(--donut-card-surface-end)); background: var(--tile-surface);
box-shadow: var(--donut-card-shadow); box-shadow: var(--tile-card-shadow);
} }
.donut-metric-card::before { .donut-metric-card::before {
position: absolute; position: absolute;
@ -478,7 +478,7 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: visible; overflow: visible;
transform: rotate(135deg); transform: rotate(-90deg);
} }
.donut-metric-card__plot circle { .donut-metric-card__plot circle {
fill: none; fill: none;
@ -487,7 +487,6 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
.donut-metric-card__track { stroke: var(--donut-track); } .donut-metric-card__track { stroke: var(--donut-track); }
.donut-metric-card__progress { .donut-metric-card__progress {
stroke: currentColor; stroke: currentColor;
stroke-linecap: round;
animation: donut-progress-build var(--dur-data-build) var(--ease-data-build) both; animation: donut-progress-build var(--dur-data-build) var(--ease-data-build) both;
} }
@keyframes donut-progress-build { @keyframes donut-progress-build {
@ -505,20 +504,37 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-top: var(--space-4);
font-family: var(--font-donut-display); font-family: var(--font-donut-display);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
line-height: 1; line-height: 1;
} }
.donut-metric-card__values strong { .donut-metric-card__primary {
position: relative;
display: block;
}
.donut-metric-card__primary strong {
color: var(--donut-value); color: var(--donut-value);
font-size: var(--donut-value-size); font-size: var(--donut-value-size);
font-weight: var(--fw-semibold); font-weight: var(--fw-semibold);
} }
.donut-metric-card__values span { .donut-metric-card__primary span {
margin-top: var(--space-2); position: absolute;
color: currentColor; left: calc(100% + var(--space-1));
bottom: 0;
color: var(--donut-value);
font-size: var(--donut-percent-size); font-size: var(--donut-percent-size);
font-weight: var(--fw-semibold);
}
.donut-metric-card__primary.is-compact strong {
font-size: calc(var(--donut-value-size) * .85);
}
.donut-metric-card__primary.is-compact span {
font-size: var(--type-meta-size);
}
.donut-metric-card__values > small {
margin-top: var(--space-2);
color: var(--donut-label);
font-size: var(--type-meta-size);
font-weight: var(--fw-regular); font-weight: var(--fw-regular);
} }
.donut-metric-card--all { color: var(--donut-all); } .donut-metric-card--all { color: var(--donut-all); }
@ -539,17 +555,17 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
.sidebar-severity-rack { .sidebar-severity-rack {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
gap: var(--space-2); gap: var(--segment-gap);
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
margin: 0 var(--space-3) var(--space-2); margin: 0 var(--space-3) var(--space-2);
padding: var(--space-2); padding: var(--segment-inset);
overflow-y: auto; overflow-y: auto;
border: var(--border-hair) solid var(--donut-border); border: var(--border-hair) solid var(--segment-border);
border-radius: var(--donut-rack-radius); border-radius: var(--segment-radius);
background: var(--donut-rack-surface); background: var(--segment-surface);
box-shadow: var(--donut-rack-shadow); box-shadow: none;
scrollbar-color: var(--border-strong) var(--donut-rack-surface); scrollbar-color: var(--border-strong) var(--segment-surface);
scrollbar-width: thin; scrollbar-width: thin;
} }
.sidebar-severity-card { .sidebar-severity-card {
@ -563,15 +579,12 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
.sidebar-severity-card .donut-metric-card__plot { .sidebar-severity-card .donut-metric-card__plot {
width: calc(var(--donut-ring-size) - var(--space-8)); width: calc(var(--donut-ring-size) - var(--space-8));
} }
.sidebar-severity-card .donut-metric-card__values { .sidebar-severity-card .donut-metric-card__primary strong {
padding-top: var(--space-2); font-size: var(--donut-compact-value-size);
} }
.sidebar-severity-card .donut-metric-card__values strong { .sidebar-severity-card .donut-metric-card__primary span {
font-size: var(--donut-value-size); left: calc(100% + var(--donut-compact-unit-gap));
} font-size: var(--donut-compact-percent-size);
.sidebar-severity-card .donut-metric-card__values span {
margin-top: var(--space-1);
font-size: var(--donut-percent-size);
} }
.sidebar-severity-card:hover, .sidebar-severity-card:hover,
.sidebar-severity-card[aria-pressed="true"] { .sidebar-severity-card[aria-pressed="true"] {

View File

@ -35,12 +35,12 @@
.datahub-reference-knob { font-style: normal; } .datahub-reference-knob { font-style: normal; }
.datahub-comparison-error { display: block; margin: 0 var(--legacy-space-3) var(--legacy-space-2); color: var(--danger-text); font-size: var(--ui-control-size); font-style: normal; } .datahub-comparison-error { display: block; margin: 0 var(--legacy-space-3) var(--legacy-space-2); color: var(--danger-text); font-size: var(--ui-control-size); font-style: normal; }
.datahub-tenants-module { min-height: 0; padding: 0; border: 0; background: transparent; } .datahub-tenants-module { min-height: 0; padding: 0; border: 0; background: transparent; }
.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--space-2); padding: var(--space-3); border: var(--border-hair) solid var(--donut-border); border-radius: var(--donut-rack-radius); background: var(--donut-rack-surface); box-shadow: var(--donut-rack-shadow); } .datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); box-shadow: none; }
.datahub-tenant-card { position: relative; min-height: var(--donut-card-min-h); padding: 0; transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .12s ease; } .datahub-tenant-card { position: relative; min-height: var(--donut-card-min-h); padding: 0; transition: background .12s ease, border-color .12s ease; }
.datahub-tenant-card::before { content: none; } .datahub-tenant-card::before { content: none; }
.datahub-tenant-card:hover, .datahub-tenant-card:hover,
.datahub-tenant-card:focus-within { border-color: var(--line-strong); background: var(--panel-hover); box-shadow: var(--shadow-raised-hover); transform: translateY(-2px); } .datahub-tenant-card:focus-within { border-color: var(--line-strong); background: var(--tile-surface-hover); box-shadow: var(--tile-card-shadow); }
.datahub-tenant-card.is-selected { outline: var(--bw-emphasis) solid var(--button-marked-bg); outline-offset: calc(-1 * var(--bw-emphasis)); } .datahub-tenant-card.is-selected { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); }
.datahub-tenant-processing-dot { position: absolute; z-index: 3; top: var(--datahub-readiness-dot-inset); left: var(--datahub-readiness-dot-inset); width: var(--datahub-readiness-dot-size); height: var(--datahub-readiness-dot-size); border: var(--border-hair) solid var(--surface-app); border-radius: 50%; background: var(--state-online); pointer-events: none; } .datahub-tenant-processing-dot { position: absolute; z-index: 3; top: var(--datahub-readiness-dot-inset); left: var(--datahub-readiness-dot-inset); width: var(--datahub-readiness-dot-size); height: var(--datahub-readiness-dot-size); border: var(--border-hair) solid var(--surface-app); border-radius: 50%; background: var(--state-online); pointer-events: none; }
.datahub-tenant-processing-dot[data-status="update"] { background: var(--warning); } .datahub-tenant-processing-dot[data-status="update"] { background: var(--warning); }
.datahub-tenant-processing-dot[data-status="error"] { background: var(--state-offline); } .datahub-tenant-processing-dot[data-status="error"] { background: var(--state-offline); }
@ -70,14 +70,13 @@
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.datahub-tenant-card { transition: none; } .datahub-tenant-card { transition: none; }
.datahub-tenant-card:hover, .datahub-tenant-card:focus-within { transform: none; }
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.datahub-tenant-card-rack { grid-template-columns: repeat(3, minmax(0, 1fr)); } .datahub-tenant-card-rack { grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); }
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.datahub-tenant-card-rack { grid-template-columns: repeat(2, minmax(0, 1fr)); } .datahub-tenant-card-rack { grid-template-columns: minmax(0, var(--tile-compact-width)); }
} }
.datahub-reference-module { overflow: hidden; } .datahub-reference-module { overflow: hidden; }

View File

@ -8,10 +8,10 @@
.datahub-projects, .datahub-create { margin-top: var(--legacy-space-6); } .datahub-projects, .datahub-create { margin-top: var(--legacy-space-6); }
.datahub-section-heading { display: flex; align-items: end; justify-content: space-between; gap: var(--legacy-space-5); margin-bottom: var(--legacy-space-3); } .datahub-section-heading { display: flex; align-items: end; justify-content: space-between; gap: var(--legacy-space-5); margin-bottom: var(--legacy-space-3); }
.datahub-section-heading h3 { margin: 0; color: var(--heading); font-size: var(--ui-body-size); } .datahub-section-heading h3 { margin: 0; color: var(--heading); font-size: var(--ui-body-size); }
.datahub-section-heading > strong { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-value-size); font-weight: 700; font-variant-numeric: tabular-nums; } .datahub-section-heading > strong { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.datahub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); gap: var(--legacy-space-2); } .datahub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(0, var(--tile-content-width))); justify-content: start; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.datahub-card { display: grid; grid-template-columns: 40px minmax(0,1fr) auto; align-items: center; gap: var(--legacy-space-3); min-height: 96px; padding: var(--legacy-space-4); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); color: var(--text); background: var(--panel); text-align: left; transition: border-color .12s ease, background .12s ease; } .datahub-card { display: grid; grid-template-columns: 40px minmax(0,1fr) auto; align-items: center; gap: var(--legacy-space-3); min-width: 0; min-height: 96px; padding: var(--legacy-space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; transition: border-color .12s ease, background .12s ease; }
.datahub-card:hover { border-color: var(--line-strong); background: var(--panel-hover); } .datahub-card:hover { border-color: var(--line-strong); background: var(--tile-surface-hover); }
.datahub-card-mark { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--legacy-radius-sm); color: var(--text-on-accent); background: var(--legacy-accent); font-size: var(--ui-control-size); font-weight: var(--ui-strong-weight); letter-spacing: .05em; } .datahub-card-mark { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--legacy-radius-sm); color: var(--text-on-accent); background: var(--legacy-accent); font-size: var(--ui-control-size); font-weight: var(--ui-strong-weight); letter-spacing: .05em; }
.datahub-card-body { display: grid; gap: var(--legacy-space-1); min-width: 0; } .datahub-card-body { display: grid; gap: var(--legacy-space-1); min-width: 0; }
.datahub-card-body strong { color: var(--heading); font-size: var(--ui-body-size); overflow: hidden; text-overflow: ellipsis; } .datahub-card-body strong { color: var(--heading); font-size: var(--ui-body-size); overflow: hidden; text-overflow: ellipsis; }
@ -24,7 +24,7 @@
.datahub-create label small { color: var(--muted); font-weight: 400; } .datahub-create label small { color: var(--muted); font-weight: 400; }
.datahub-offline, .datahub-alert, .datahub-empty, .datahub-loading { margin-top: var(--legacy-space-6); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); } .datahub-offline, .datahub-alert, .datahub-empty, .datahub-loading { margin-top: var(--legacy-space-6); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); }
.datahub-offline { display: flex; gap: var(--legacy-space-5); padding: var(--legacy-space-5); border: var(--border-hair) solid var(--line); } .datahub-offline { display: flex; gap: var(--legacy-space-5); padding: var(--legacy-space-5); border: var(--border-hair) solid var(--line); }
.datahub-offline-icon { display: grid; place-items: center; flex: 0 0 32px; width: 32px; height: 32px; border-radius: var(--legacy-radius-sm); color: var(--state-active-text); background: var(--state-active); font-size: var(--ui-heading-size); font-weight: 900; } .datahub-offline-icon { display: grid; place-items: center; flex: 0 0 32px; width: 32px; height: 32px; border-radius: var(--legacy-radius-sm); color: var(--state-active-text); background: var(--state-active); font-size: var(--ui-heading-size); font-weight: var(--fw-bold); }
.datahub-offline h3, .datahub-empty h3 { margin: 0 0 var(--legacy-space-2); color: var(--heading); } .datahub-offline h3, .datahub-empty h3 { margin: 0 0 var(--legacy-space-2); color: var(--heading); }
.datahub-offline p, .datahub-empty p { max-width: 650px; color: var(--muted); line-height: 1.5; } .datahub-offline p, .datahub-empty p { max-width: 650px; color: var(--muted); line-height: 1.5; }
.datahub-alert { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-3); padding: var(--legacy-space-3); border-color: var(--danger); color: var(--danger-text); background: var(--danger-surface); } .datahub-alert { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-3); padding: var(--legacy-space-3); border-color: var(--danger); color: var(--danger-text); background: var(--danger-surface); }
@ -62,18 +62,18 @@
.tenant-duplicate-badge { width: fit-content; padding: 1px var(--legacy-space-1); border: var(--border-hair) solid var(--warning); border-radius: var(--legacy-radius-sm); color: var(--warning) !important; background: transparent; font-size: var(--ui-small-size); font-weight: var(--ui-label-weight); letter-spacing: .03em; text-transform: uppercase; } .tenant-duplicate-badge { width: fit-content; padding: 1px var(--legacy-space-1); border: var(--border-hair) solid var(--warning); border-radius: var(--legacy-radius-sm); color: var(--warning) !important; background: transparent; font-size: var(--ui-small-size); font-weight: var(--ui-label-weight); letter-spacing: .03em; text-transform: uppercase; }
.tenant-scan-delete { min-width: 64px; padding-inline: 8px; } .tenant-scan-delete { min-width: 64px; padding-inline: 8px; }
.datahub-dialog-form { display: grid; gap: 16px; } .datahub-dialog-form { display: grid; gap: 16px; }
.datahub-dialog-form label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-label-size); font-weight: 750; } .datahub-dialog-form label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); }
.datahub-dialog-form label small { color: var(--muted); font-weight: 400; } .datahub-dialog-form label small { color: var(--muted); font-weight: 400; }
.datahub-dialog-form textarea { min-height: 100px; resize: vertical; } .datahub-dialog-form textarea { min-height: 100px; resize: vertical; }
.datahub-dialog-form footer, .datahub-delete-warning footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; padding-top: 16px; border-top: 1px solid var(--line); } .datahub-dialog-form footer, .datahub-delete-warning footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; padding-top: 16px; border-top: 1px solid var(--line); }
.datahub-delete-warning { display: grid; gap: 10px; } .datahub-delete-warning { display: grid; gap: 10px; }
.datahub-delete-warning > strong { color: var(--heading); font-size: var(--ui-heading-size); } .datahub-delete-warning > strong { color: var(--heading); font-size: var(--ui-heading-size); }
.datahub-delete-warning p { color: var(--muted); line-height: 1.6; } .datahub-delete-warning p { color: var(--muted); line-height: 1.6; }
@media (max-width: 1050px) { .tenant-file-head { display: none; } .tenant-file-row { grid-template-columns: 1fr 1fr; gap: 8px 18px; padding: 16px; } .tenant-file-row > span::before { content: attr(data-label); display: block; margin-bottom: 3px; color: var(--muted); font-size: var(--ui-label-size); font-weight: 800; letter-spacing: .05em; text-transform: uppercase; } } @media (max-width: 1050px) { .tenant-file-head { display: none; } .tenant-file-row { grid-template-columns: 1fr 1fr; gap: 8px 18px; padding: 16px; } .tenant-file-row > span::before { content: attr(data-label); display: block; margin-bottom: 3px; color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; } }
@media (max-width: 800px) { .tenant-header { grid-template-columns: 1fr; } .tenant-actions { flex-wrap: wrap; } .tenant-file-row { grid-template-columns: 1fr; } .datahub-card-actions { flex-wrap: wrap; } } @media (max-width: 800px) { .tenant-header { grid-template-columns: 1fr; } .tenant-actions { flex-wrap: wrap; } .tenant-file-row { grid-template-columns: 1fr; } .datahub-card-actions { flex-wrap: wrap; } }
/* Full-page tenant editor */ /* Full-page tenant editor */
.datahub-card.active { border-color: var(--line-strong); background: var(--panel-hover); } .datahub-card.active { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); background: var(--tile-surface-hover); }
.tenant-editor-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--legacy-space-5); padding-bottom: var(--legacy-space-6); border-bottom: var(--border-hair) solid var(--line); } .tenant-editor-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--legacy-space-5); padding-bottom: var(--legacy-space-6); border-bottom: var(--border-hair) solid var(--line); }
.tenant-editor-header .tenant-back { grid-column: 1 / -1; } .tenant-editor-header .tenant-back { grid-column: 1 / -1; }
.tenant-editor-header h2 { margin: var(--legacy-space-1) 0; color: var(--heading); font-size: clamp(17px, 2vw, 22px); line-height: 1.15; letter-spacing: -.01em; } .tenant-editor-header h2 { margin: var(--legacy-space-1) 0; color: var(--heading); font-size: clamp(17px, 2vw, 22px); line-height: 1.15; letter-spacing: -.01em; }
@ -81,7 +81,7 @@
.tenant-editor { margin-top: var(--legacy-space-6); padding: var(--legacy-space-5); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); } .tenant-editor { margin-top: var(--legacy-space-6); padding: var(--legacy-space-5); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); }
.tenant-editor-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--legacy-space-4); } .tenant-editor-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--legacy-space-4); }
.tenant-editor-form label { display: grid; grid-template-columns: auto 1fr; gap: var(--legacy-space-2); color: var(--heading); font-size: var(--ui-control-size); font-weight: var(--ui-label-weight); } .tenant-editor-form label { display: grid; grid-template-columns: auto 1fr; gap: var(--legacy-space-2); color: var(--heading); font-size: var(--ui-control-size); font-weight: var(--ui-label-weight); }
.tenant-editor-form label > span { justify-self: end; color: var(--muted); font-size: var(--ui-small-size); font-weight: 500; } .tenant-editor-form label > span { justify-self: end; color: var(--muted); font-size: var(--ui-small-size); font-weight: var(--fw-semibold); }
.tenant-editor-form .input { grid-column: 1 / -1; } .tenant-editor-form .input { grid-column: 1 / -1; }
.tenant-editor-description { grid-column: 1 / -1; } .tenant-editor-description { grid-column: 1 / -1; }
.tenant-editor-description textarea { min-height: 118px; resize: vertical; } .tenant-editor-description textarea { min-height: 118px; resize: vertical; }
@ -101,7 +101,8 @@
.viewer.app-area-data-hub, .viewer.app-area-dashboard, .viewer.app-area-db-structure { overflow-x: hidden; overflow-y: auto; background: var(--surface-app); scrollbar-color: var(--line-strong) var(--surface-app); scrollbar-width: thin; } .viewer.app-area-data-hub, .viewer.app-area-dashboard, .viewer.app-area-db-structure { overflow-x: hidden; overflow-y: auto; background: var(--surface-app); scrollbar-color: var(--line-strong) var(--surface-app); scrollbar-width: thin; }
.viewer.app-area-data-hub { scrollbar-gutter: stable; } .viewer.app-area-data-hub { scrollbar-gutter: stable; }
.datahub-page { box-sizing: border-box; min-height: 100%; background: var(--surface-app); } .datahub-page { box-sizing: border-box; min-height: 100%; background: var(--surface-app); }
.datahub-card, .tenant-editor, .tenant-file-table, .tenant-import-pending { border-radius: var(--legacy-radius-sm); box-shadow: var(--shadow); } .datahub-card { border-radius: var(--tile-radius); box-shadow: var(--tile-card-shadow); }
.tenant-editor, .tenant-file-table, .tenant-import-pending { border-radius: var(--legacy-radius-sm); box-shadow: var(--shadow); }
.tenant-editor-header { align-items: center; } .tenant-editor-header { align-items: center; }
.tenant-editor-header h2 { font-weight: var(--ui-strong-weight); letter-spacing: -.01em; } .tenant-editor-header h2 { font-weight: var(--ui-strong-weight); letter-spacing: -.01em; }
.tenant-editor { padding: var(--legacy-space-5); } .tenant-editor { padding: var(--legacy-space-5); }
@ -164,33 +165,35 @@
.dashboard-page { box-sizing: border-box; display: grid; align-content: start; gap: var(--space-4); width: 100%; min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); } .dashboard-page { box-sizing: border-box; display: grid; align-content: start; gap: var(--space-4); width: 100%; min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); }
.dashboard-context { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-5); margin: 0; padding: 0 var(--space-2); color: var(--muted); font-size: var(--ui-label-size); } .dashboard-context { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-5); margin: 0; padding: 0 var(--space-2); color: var(--muted); font-size: var(--ui-label-size); }
.dashboard-context strong { color: var(--heading); font-size: var(--ui-body-size); } .dashboard-context strong { color: var(--heading); font-size: var(--ui-body-size); }
.dashboard-status-rack { display: grid; grid-template-columns: minmax(270px, 1.45fr) repeat(3, minmax(180px, .9fr)) minmax(250px, 1.2fr); gap: var(--space-2); } .dashboard-status-rack { display: grid; grid-template-columns: 290px repeat(3, 210px) 250px; justify-content: start; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.dashboard-status-metric { display: grid; grid-template-columns: 98px minmax(0, 1fr); align-items: center; gap: var(--space-4); min-width: 0; min-height: 190px; padding: var(--space-4); border: var(--border-hair) solid var(--line); border-radius: 0; color: var(--text); background: var(--panel); text-align: left; } .dashboard-status-metric { display: grid; grid-template-columns: 98px minmax(0, 1fr); align-items: center; gap: var(--space-3); min-width: 0; min-height: 190px; padding: var(--space-3); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; }
button.dashboard-status-metric { cursor: pointer; } button.dashboard-status-metric { cursor: pointer; }
button.dashboard-status-metric:hover { border-color: currentColor; } button.dashboard-status-metric:hover { border-color: currentColor; background: var(--tile-surface-hover); }
button.dashboard-status-metric:focus-visible { outline: var(--border-hair) solid var(--focus-ring); outline-offset: -1px; } button.dashboard-status-metric:focus-visible { outline: var(--border-hair) solid var(--focus-ring); outline-offset: -1px; }
button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis) solid currentColor; outline-offset: calc(-1 * var(--bw-emphasis)); } button.dashboard-status-metric[aria-pressed="true"] { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); }
.dashboard-status-metric-large { grid-template-columns: 154px minmax(0, 1fr); } .dashboard-status-metric-large { grid-template-columns: 154px minmax(0, 1fr); }
.dashboard-status-ring { position: relative; width: 98px; height: 98px; color: var(--donut-all); } .dashboard-status-ring { position: relative; width: 98px; height: 98px; color: var(--donut-all); }
.dashboard-status-metric-large .dashboard-status-ring { width: 154px; height: 154px; } .dashboard-status-metric-large .dashboard-status-ring { width: 154px; height: 154px; }
.dashboard-status-ring svg { display: block; width: 100%; height: 100%; transform: rotate(-90deg); } .dashboard-status-ring svg { display: block; width: 100%; height: 100%; transform: rotate(-90deg); }
.dashboard-status-ring circle { fill: none; stroke-width: 12; } .dashboard-status-ring circle { fill: none; stroke-width: var(--donut-ring-stroke); }
.dashboard-status-metric-large .dashboard-status-ring circle { stroke-width: 10; } .dashboard-status-ring-track { stroke: var(--donut-track); }
.dashboard-status-ring-track { stroke: var(--donut-border); }
.dashboard-status-ring-progress { stroke: currentColor; } .dashboard-status-ring-progress { stroke: currentColor; }
.dashboard-status-ring > span { position: absolute; inset: 0; display: flex; align-items: baseline; justify-content: center; padding-top: 36px; color: var(--heading); font-variant-numeric: tabular-nums; } .dashboard-status-ring > span { position: absolute; inset: 0; display: flex; align-items: flex-start; justify-content: center; padding-top: 36px; color: var(--heading); font-variant-numeric: tabular-nums; }
.dashboard-status-metric-large .dashboard-status-ring > span { padding-top: 57px; } .dashboard-status-metric-large .dashboard-status-ring > span { padding-top: 57px; }
.dashboard-status-ring > span strong { font: var(--fw-semibold) var(--donut-value-size) / 1 var(--font-donut-display); } .dashboard-donut-primary { position: relative; display: block; line-height: 1; }
.dashboard-status-ring > span strong { display: block; font: var(--fw-semibold) var(--donut-value-size) / 1 var(--font-donut-display); }
.dashboard-status-metric-large .dashboard-status-ring > span strong { font-size: calc(var(--donut-value-size) * 1.45); } .dashboard-status-metric-large .dashboard-status-ring > span strong { font-size: calc(var(--donut-value-size) * 1.45); }
.dashboard-status-ring > span small { margin-left: var(--space-1); color: var(--heading); font-size: var(--ui-body-size); font-weight: var(--fw-semibold); } .dashboard-status-ring > span small { position: absolute; left: calc(100% + var(--space-1)); bottom: 0; color: var(--heading); font-size: var(--donut-percent-size); font-weight: var(--fw-semibold); }
.dashboard-status-ring .dashboard-donut-primary.is-compact strong, .dashboard-risk-donut .dashboard-donut-primary.is-compact strong { font-size: calc(var(--donut-value-size) * .85); }
.dashboard-status-ring .dashboard-donut-primary.is-compact small, .dashboard-risk-donut .dashboard-donut-primary.is-compact small { font-size: var(--type-meta-size); }
.dashboard-status-ring-deviation { color: var(--donut-error); } .dashboard-status-ring-deviation { color: var(--donut-error); }
.dashboard-status-ring-passed { color: var(--donut-passed); } .dashboard-status-ring-passed { color: var(--donut-passed); }
.dashboard-status-ring-not-run { color: var(--donut-deviation); } .dashboard-status-ring-not-run { color: var(--donut-deviation); }
.dashboard-status-copy { display: grid; gap: var(--space-3); min-width: 0; } .dashboard-status-copy { display: grid; gap: var(--space-3); min-width: 0; max-width: 100%; overflow: hidden; }
.dashboard-status-copy > span { color: var(--heading); font-size: var(--ui-body-size); } .dashboard-status-copy > span { min-width: 0; color: var(--heading); font-size: var(--ui-body-size); overflow-wrap: normal; word-break: normal; hyphens: manual; }
.dashboard-status-copy > strong { color: var(--heading); font-size: calc(var(--ui-heading-size) * 1.35); font-variant-numeric: tabular-nums; } .dashboard-status-copy > strong { display: grid; gap: var(--space-1); min-width: 0; color: var(--heading); font-size: calc(var(--ui-heading-size) * 1.35); font-variant-numeric: tabular-nums; white-space: normal; }
.dashboard-status-copy > strong small, .dashboard-status-copy > small { color: var(--muted); font-size: var(--ui-body-size); font-weight: var(--fw-regular); line-height: 1.35; } .dashboard-status-copy > strong small, .dashboard-status-copy > small { display: block; color: var(--muted); font-size: var(--ui-body-size); font-weight: var(--fw-regular); line-height: 1.35; overflow-wrap: normal; word-break: normal; hyphens: manual; }
.dashboard-status-summary { display: grid; grid-template-columns: minmax(0, 1fr) minmax(100px, .8fr); align-items: stretch; min-width: 0; min-height: 190px; padding: var(--space-4); border: var(--border-hair) solid var(--line); background: var(--panel); } .dashboard-status-summary { display: grid; grid-template-columns: minmax(0, 1fr) minmax(100px, .8fr); align-items: stretch; min-width: 0; min-height: 190px; padding: var(--space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
.dashboard-status-summary-stack { display: grid; align-content: center; padding-right: var(--space-4); border-right: var(--border-hair) solid var(--line-strong); } .dashboard-status-summary-stack { display: grid; align-content: center; padding-right: var(--space-4); border-right: var(--border-hair) solid var(--line-strong); }
.dashboard-status-summary-stack button { display: grid; gap: var(--space-2); min-height: 64px; padding: var(--space-2); border: 0; border-radius: 0; color: var(--text); background: transparent; text-align: left; cursor: pointer; } .dashboard-status-summary-stack button { display: grid; gap: var(--space-2); min-height: 64px; padding: var(--space-2); border: 0; border-radius: 0; color: var(--text); background: transparent; text-align: left; cursor: pointer; }
.dashboard-status-summary-stack button + button { border-top: var(--border-hair) solid var(--line); } .dashboard-status-summary-stack button + button { border-top: var(--border-hair) solid var(--line); }
@ -202,9 +205,9 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
.dashboard-ioe-summary:focus-visible { outline: var(--border-hair) solid var(--focus-ring); outline-offset: -1px; } .dashboard-ioe-summary:focus-visible { outline: var(--border-hair) solid var(--focus-ring); outline-offset: -1px; }
.dashboard-ioe-summary:disabled { cursor: default; } .dashboard-ioe-summary:disabled { cursor: default; }
.dashboard-ioe-summary strong { color: var(--accent); font: var(--fw-semibold) calc(var(--donut-value-size) * 1.45) / 1 var(--font-donut-display); font-variant-numeric: tabular-nums; } .dashboard-ioe-summary strong { color: var(--accent); font: var(--fw-semibold) calc(var(--donut-value-size) * 1.45) / 1 var(--font-donut-display); font-variant-numeric: tabular-nums; }
.dashboard-distribution-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); } .dashboard-distribution-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.dashboard-lower-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(420px, 1fr); align-items: stretch; gap: var(--space-3); } .dashboard-lower-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(420px, 1fr); align-items: stretch; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.dashboard-environment { min-width: 0; margin: 0; padding: var(--space-4); border: var(--border-hair) solid var(--line); border-left: 3px solid var(--legacy-accent); border-radius: var(--legacy-radius-sm); background: var(--panel); } .dashboard-environment { min-width: 0; margin: 0; padding: var(--space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-left: 3px solid var(--legacy-accent); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
.dashboard-environment .datahub-section-heading h2, .dashboard-block h2 { margin: 0; color: var(--heading); font-size: var(--ui-body-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; } .dashboard-environment .datahub-section-heading h2, .dashboard-block h2 { margin: 0; color: var(--heading); font-size: var(--ui-body-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; }
.dashboard-coverage-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: var(--space-4); padding-bottom: var(--space-4); border-bottom: var(--border-hair) solid var(--line); } .dashboard-coverage-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: var(--space-4); padding-bottom: var(--space-4); border-bottom: var(--border-hair) solid var(--line); }
.dashboard-coverage-summary > span { display: grid; gap: var(--space-1); min-width: 0; padding: 0 var(--space-3); text-align: center; } .dashboard-coverage-summary > span { display: grid; gap: var(--space-1); min-width: 0; padding: 0 var(--space-3); text-align: center; }
@ -215,10 +218,10 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
.dashboard-facts div { min-width: 0; } .dashboard-facts div { min-width: 0; }
.dashboard-facts dt { margin-bottom: var(--legacy-space-1); color: var(--muted); font-size: var(--ui-label-size); } .dashboard-facts dt { margin-bottom: var(--legacy-space-1); color: var(--muted); font-size: var(--ui-label-size); }
.dashboard-facts dd { margin: 0; color: var(--heading); font-size: var(--ui-body-size); overflow-wrap: anywhere; } .dashboard-facts dd { margin: 0; color: var(--heading); font-size: var(--ui-body-size); overflow-wrap: anywhere; }
.dashboard-block { min-width: 0; padding: var(--space-4); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); } .dashboard-block { min-width: 0; padding: var(--space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
.dashboard-distribution { display: grid; align-content: start; gap: var(--space-4); } .dashboard-distribution { display: grid; align-content: start; gap: var(--space-4); }
.dashboard-segmented-bar { display: flex; width: 100%; height: 28px; overflow: hidden; background: var(--surface-active); } .dashboard-segmented-bar { display: flex; width: 100%; height: 28px; overflow: hidden; border-radius: var(--tile-radius); background: var(--surface-active); }
.dashboard-segment { flex: 0 0 var(--share); min-width: 2px; height: 100%; background: var(--severity, var(--viz-1)); } .dashboard-segment { --share: 0%; flex: 0 0 var(--share); min-width: 2px; height: 100%; background: var(--severity, var(--viz-1)); }
.dashboard-segment + .dashboard-segment { border-left: var(--border-hair) solid var(--surface-panel); } .dashboard-segment + .dashboard-segment { border-left: var(--border-hair) solid var(--surface-panel); }
.dashboard-distribution-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--space-2); margin: 0; padding: 0; list-style: none; } .dashboard-distribution-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.dashboard-legend-button { display: flex; align-items: flex-start; gap: var(--space-2); width: 100%; min-height: 52px; padding: var(--space-2); border: var(--border-hair) solid transparent; border-radius: var(--btn-radius); color: var(--text); background: transparent; text-align: left; cursor: pointer; } .dashboard-legend-button { display: flex; align-items: flex-start; gap: var(--space-2); width: 100%; min-height: 52px; padding: var(--space-2); border: var(--border-hair) solid transparent; border-radius: var(--btn-radius); color: var(--text); background: transparent; text-align: left; cursor: pointer; }
@ -255,20 +258,24 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
.dashboard-risk-categories span, .dashboard-risk-legend span { min-width: 0; overflow: hidden; font-size: var(--ui-body-size); text-overflow: ellipsis; white-space: nowrap; } .dashboard-risk-categories span, .dashboard-risk-legend span { min-width: 0; overflow: hidden; font-size: var(--ui-body-size); text-overflow: ellipsis; white-space: nowrap; }
.dashboard-risk-categories strong, .dashboard-risk-legend strong { color: var(--heading); font-size: var(--ui-body-size); font-variant-numeric: tabular-nums; } .dashboard-risk-categories strong, .dashboard-risk-legend strong { color: var(--heading); font-size: var(--ui-body-size); font-variant-numeric: tabular-nums; }
.dashboard-risk-severity { display: flex; align-items: center; justify-content: center; gap: var(--space-5); min-width: 0; padding-left: var(--space-5); } .dashboard-risk-severity { display: flex; align-items: center; justify-content: center; gap: var(--space-5); min-width: 0; padding-left: var(--space-5); }
.dashboard-risk-donut { flex: 0 0 150px; width: 150px; height: 150px; overflow: visible; transform: rotate(-90deg); } .dashboard-risk-donut { position: relative; flex: 0 0 150px; width: 150px; height: 150px; overflow: visible; }
.dashboard-risk-donut circle { fill: none; stroke-width: 13; } .dashboard-risk-donut svg { display: block; width: 100%; height: 100%; overflow: visible; transform: rotate(-90deg); }
.dashboard-risk-donut-track { stroke: var(--surface-active); } .dashboard-risk-donut circle { fill: none; stroke-width: var(--donut-ring-stroke); }
.dashboard-risk-donut-track { stroke: var(--donut-track); }
.dashboard-risk-donut-segment { stroke: var(--severity, var(--info)); } .dashboard-risk-donut-segment { stroke: var(--severity, var(--info)); }
.dashboard-risk-donut > span { position: absolute; inset: 0; display: flex; align-items: flex-start; justify-content: center; padding-top: 57px; color: var(--heading); font-variant-numeric: tabular-nums; }
.dashboard-risk-donut > span strong { display: block; font: var(--fw-semibold) calc(var(--donut-value-size) * 1.45) / 1 var(--font-donut-display); }
.dashboard-risk-donut > span small { position: absolute; left: calc(100% + var(--space-1)); bottom: 0; font-size: var(--donut-percent-size); font-weight: var(--fw-semibold); }
.dashboard-risk-legend { flex: 1 1 auto; min-width: 120px; } .dashboard-risk-legend { flex: 1 1 auto; min-width: 120px; }
.dashboard-risk-legend button { grid-template-columns: 12px minmax(0, 1fr) auto; gap: var(--space-3); } .dashboard-risk-legend button { grid-template-columns: 12px minmax(0, 1fr) auto; gap: var(--space-3); }
.dashboard-risk-legend i { width: 12px; height: 12px; background: var(--severity, var(--info)); } .dashboard-risk-legend i { width: 12px; height: 12px; background: var(--severity, var(--info)); }
@media (max-width: 1200px) { @media (max-width: 1400px) {
.dashboard-status-rack { grid-template-columns: repeat(2, minmax(0, 1fr)); } .dashboard-status-rack { grid-template-columns: repeat(2, minmax(0, 290px)); }
.dashboard-status-summary { grid-column: 1 / -1; } .dashboard-status-summary { grid-column: 1 / -1; }
.dashboard-distribution-grid, .dashboard-lower-grid { grid-template-columns: 1fr; } .dashboard-distribution-grid, .dashboard-lower-grid { grid-template-columns: 1fr; }
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.dashboard-status-rack { grid-template-columns: 1fr; } .dashboard-status-rack { grid-template-columns: minmax(0, 290px); }
.dashboard-status-summary { grid-column: auto; } .dashboard-status-summary { grid-column: auto; }
.dashboard-coverage-summary, .dashboard-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } .dashboard-coverage-summary, .dashboard-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dashboard-risk-body { grid-template-columns: 1fr; } .dashboard-risk-body { grid-template-columns: 1fr; }
@ -283,10 +290,10 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
eigenen Aussenabstand. */ eigenen Aussenabstand. */
.db-structure-meta { margin: 0; color: var(--muted); font-size: var(--ui-body-size); } .db-structure-meta { margin: 0; color: var(--muted); font-size: var(--ui-body-size); }
.page-actions-row { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-4); margin-top: var(--legacy-space-4); } .page-actions-row { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-4); margin-top: var(--legacy-space-4); }
.db-structure-layers { display: grid; gap: var(--legacy-space-5); margin-top: var(--legacy-space-5); width: 100%; } .db-structure-layers { display: grid; gap: var(--segment-gap); margin-top: var(--legacy-space-5); width: 100%; }
.db-structure-layer { border: var(--border-hair) solid var(--line); border-left: 3px solid var(--legacy-accent); border-radius: var(--legacy-radius-sm); padding: var(--legacy-space-4); background: var(--panel); } .db-structure-layer { padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-left: 3px solid var(--legacy-accent); border-radius: var(--segment-radius); background: var(--segment-surface); }
.db-structure-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--legacy-space-2); } .db-structure-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--legacy-space-2); }
.db-structure-card { border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel-raised); overflow: hidden; } .db-structure-card { overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
.db-structure-card-header { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-2); width: 100%; padding: var(--legacy-space-3); border: 0; color: inherit; background: transparent; text-align: left; cursor: pointer; } .db-structure-card-header { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-2); width: 100%; padding: var(--legacy-space-3); border: 0; color: inherit; background: transparent; text-align: left; cursor: pointer; }
.db-structure-card-header:hover { background: var(--panel-hover); } .db-structure-card-header:hover { background: var(--panel-hover); }
.db-structure-card-title { display: grid; gap: var(--legacy-space-1); min-width: 0; } .db-structure-card-title { display: grid; gap: var(--legacy-space-1); min-width: 0; }
@ -708,8 +715,9 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
} }
.tenant-detail-page .tenant-identity h2 { .tenant-detail-page .tenant-identity h2 {
padding: 0; padding: 0;
font-size: calc(var(--fs-xl) * 2); font-size: var(--type-h2-size);
line-height: 1.1; font-weight: var(--fw-semibold);
line-height: 1.15;
letter-spacing: -.03em; letter-spacing: -.03em;
} }
.tenant-detail-page .tenant-identity p { .tenant-detail-page .tenant-identity p {
@ -736,11 +744,11 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
} }
.tenant-timeslots-heading { .tenant-timeslots-heading {
margin: 0; margin: 0;
color: var(--text-secondary); color: var(--text-primary);
font-size: var(--fs-md); font-size: var(--type-h3-size);
font-weight: var(--fw-semibold); font-weight: var(--fw-semibold);
letter-spacing: var(--ls-caps); letter-spacing: 0;
text-transform: uppercase; text-transform: none;
} }
.tenant-timeslot-add { .tenant-timeslot-add {
flex: 0 0 auto; flex: 0 0 auto;
@ -851,7 +859,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
min-height: calc(var(--row-h) + var(--space-2)); min-height: calc(var(--row-h) + var(--space-2));
color: var(--text-secondary); color: var(--text-secondary);
background: var(--surface-panel); background: var(--surface-panel);
font-size: var(--fs-sm); font-size: var(--type-table-head-size);
font-weight: var(--fw-semibold); font-weight: var(--fw-semibold);
letter-spacing: var(--ls-caps); letter-spacing: var(--ls-caps);
text-transform: uppercase; text-transform: uppercase;
@ -872,7 +880,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
.tenant-report-copy small { .tenant-report-copy small {
overflow: hidden; overflow: hidden;
color: var(--text-secondary); color: var(--text-secondary);
font-size: var(--fs-sm); font-size: var(--type-meta-size);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -880,7 +888,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
.tenant-report-imported { .tenant-report-imported {
overflow: hidden; overflow: hidden;
color: var(--text-primary); color: var(--text-primary);
font-size: var(--fs-md); font-size: var(--type-body-size);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -891,7 +899,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
gap: var(--space-2); gap: var(--space-2);
min-width: 0; min-width: 0;
color: var(--text-primary); color: var(--text-primary);
font-size: var(--fs-md); font-size: var(--type-body-size);
font-weight: var(--fw-regular); font-weight: var(--fw-regular);
white-space: nowrap; white-space: nowrap;
} }
@ -973,11 +981,11 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
justify-content: flex-end; justify-content: flex-end;
} }
.tenant-information-title > strong { .tenant-information-title > strong {
color: var(--text-secondary); color: var(--text-primary);
font-size: var(--fs-md); font-size: var(--type-h3-size);
font-weight: var(--fw-semibold); font-weight: var(--fw-semibold);
letter-spacing: var(--ls-caps); letter-spacing: 0;
text-transform: uppercase; text-transform: none;
} }
.tenant-edit-client, .tenant-edit-client,
.tenant-save-client { .tenant-save-client {
@ -1005,10 +1013,10 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
padding: 0; padding: 0;
color: var(--text-primary); color: var(--text-primary);
background: transparent; background: transparent;
font-size: var(--fs-md); font-size: var(--type-label-size);
font-weight: var(--fw-semibold); font-weight: var(--fw-semibold);
letter-spacing: var(--ls-caps); letter-spacing: 0;
text-transform: uppercase; text-transform: none;
} }
.tenant-detail-page .tenant-editor-form .tenant-field-label i { .tenant-detail-page .tenant-editor-form .tenant-field-label i {
color: var(--text-secondary); color: var(--text-secondary);
@ -1020,7 +1028,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
display: block; display: block;
justify-self: start; justify-self: start;
color: var(--text-primary); color: var(--text-primary);
font-size: var(--fs-lg); font-size: var(--type-body-size);
font-weight: var(--fw-regular); font-weight: var(--fw-regular);
letter-spacing: normal; letter-spacing: normal;
line-height: var(--lh-prose); line-height: var(--lh-prose);

View File

@ -30,7 +30,7 @@
.import-preview-heading { display: flex; align-items: center; gap: 9px; min-width: 0; } .import-preview-heading { display: flex; align-items: center; gap: 9px; min-width: 0; }
.import-preview-main { min-width: 0; } .import-preview-main { min-width: 0; }
.import-preview-heading strong { min-width: 0; overflow-wrap: anywhere; } .import-preview-heading strong { min-width: 0; overflow-wrap: anywhere; }
.import-status { flex: 0 0 auto; font-size: var(--ui-label-size); font-weight: 700; } .import-status { flex: 0 0 auto; font-size: var(--ui-label-size); font-weight: var(--fw-semibold); }
.import-status-ready { color: var(--low); } .import-status-ready { color: var(--low); }
.import-status-replacement { color: var(--low); } .import-status-replacement { color: var(--low); }
.import-status-warning, .import-status-duplicate { color: var(--medium); } .import-status-warning, .import-status-duplicate { color: var(--medium); }

View File

@ -1,8 +1,7 @@
.app-shell { display: grid; grid-template-rows: 124px minmax(0, 1fr); height: 100vh; min-height: 0; } .app-shell { display: grid; grid-template-rows: 124px minmax(0, 1fr); height: 100vh; min-height: 0; }
.topbar { display: flex; align-items: flex-start; gap: 10px; min-width: 0; height: 124px; padding: 6px 12px; background: var(--header); overflow: hidden; } .topbar { display: flex; align-items: flex-start; gap: 10px; min-width: 0; height: 124px; padding: 6px 12px; background: var(--header); overflow: hidden; }
.brand { display: flex; align-items: center; gap: 12px; flex: 0 0 204px; min-width: 204px; padding: 10px 14px 10px 16px; margin-top: 2px; white-space: nowrap; } .brand { display: flex; align-items: center; gap: 12px; flex: 0 0 204px; min-width: 204px; min-height: 112px; padding: 6px 14px 6px 16px; margin-top: 0; white-space: nowrap; }
.app-shell:not(.app-shell-work) .brand { min-height: 112px; padding-block: 6px; margin-top: 0; } .brand h1 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-bold); line-height: 1; letter-spacing: .045em; }
.brand h1 { margin: 0; color: var(--heading); font-size: var(--ui-heading-size); line-height: 1; letter-spacing: .045em; }
.brand-copy { display: grid; gap: var(--space-1); } .brand-copy { display: grid; gap: var(--space-1); }
.brand-version { color: var(--muted); font-size: var(--fs-micro); font-variant-numeric: tabular-nums; line-height: 1; letter-spacing: var(--ls-caps); } .brand-version { color: var(--muted); font-size: var(--fs-micro); font-variant-numeric: tabular-nums; line-height: 1; letter-spacing: var(--ls-caps); }
.brand-logo { position: relative; display: block; flex: 0 0 48px; width: 48px; height: 48px; } .brand-logo { position: relative; display: block; flex: 0 0 48px; width: 48px; height: 48px; }
@ -23,9 +22,9 @@ html[data-theme="light"] .brand-logo-light { opacity: 1; }
.toolbar, .header-export-row, .live-summary, .search-tools, .view-tabs { display: flex; align-items: center; gap: 6px; min-width: 0; } .toolbar, .header-export-row, .live-summary, .search-tools, .view-tabs { display: flex; align-items: center; gap: 6px; min-width: 0; }
.toolbar { flex: 0 0 auto; justify-content: flex-end; margin: 0; padding: 0; border: 0; min-inline-size: 0; } .toolbar { flex: 0 0 auto; justify-content: flex-end; margin: 0; padding: 0; border: 0; min-inline-size: 0; }
.live-summary { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; height: 30px; margin-left: 0; padding: 3px 8px; gap: 10px; border: 1px solid var(--border-default); border-radius: 0; background: var(--surface-panel-raised); white-space: nowrap; } .live-summary { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; height: 30px; margin-left: 0; padding: 3px 8px; gap: 10px; border: 1px solid var(--border-default); border-radius: 0; background: var(--surface-panel-raised); white-space: nowrap; }
.live-summary span { display: inline-flex; align-items: center; gap: 8px; color: var(--text-secondary); font-weight: 500; font-size: var(--ui-label-size); } .live-summary span { display: inline-flex; align-items: center; gap: 8px; color: var(--text-secondary); font-weight: var(--fw-regular); font-size: var(--ui-label-size); }
.live-summary span + span { border-left: 1px solid var(--border-subtle); padding-left: 10px; } .live-summary span + span { border-left: 1px solid var(--border-subtle); padding-left: 10px; }
.live-summary b { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-variant-numeric: tabular-nums; font-weight: 700; line-height: 1; text-align: center; } .live-summary b { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); line-height: 1; text-align: center; }
.delimiter-select { width: 220px; } .delimiter-select { width: 220px; }
.view-tabs { flex: 0 0 auto; gap: 0; border: 1px solid var(--button-border); border-radius: 0; overflow: visible; background: var(--surface-input); } .view-tabs { flex: 0 0 auto; gap: 0; border: 1px solid var(--button-border); border-radius: 0; overflow: visible; background: var(--surface-input); }
.tab-btn { flex: 0 0 auto; min-width: 72px; padding: 2px 8px; border: 0; border-right: 1px solid var(--button-border); border-radius: 0; color: var(--button-text); background: var(--button-bg); font-size: var(--ui-label-size); font-weight: var(--fw-medium); line-height: 1.15; white-space: nowrap; transition: background-color .12s ease, color .12s ease; } .tab-btn { flex: 0 0 auto; min-width: 72px; padding: 2px 8px; border: 0; border-right: 1px solid var(--button-border); border-radius: 0; color: var(--button-text); background: var(--button-bg); font-size: var(--ui-label-size); font-weight: var(--fw-medium); line-height: 1.15; white-space: nowrap; transition: background-color .12s ease, color .12s ease; }
@ -49,12 +48,12 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
.theme-switcher { display: flex; align-items: center; justify-content: space-between; gap: 4px; padding: 4px; border: 1px solid var(--button-border); border-radius: 10px; background: var(--surface-input); } .theme-switcher { display: flex; align-items: center; justify-content: space-between; gap: 4px; padding: 4px; border: 1px solid var(--button-border); border-radius: 10px; background: var(--surface-input); }
.theme-switcher-btn { display: grid; place-items: center; flex: 1 1 0; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 7px; color: var(--button-text); background: transparent; box-shadow: none; font-size: var(--ui-body-size); line-height: 1; transition: background-color .12s ease, color .12s ease, border-color .12s ease; cursor: pointer; } .theme-switcher-btn { display: grid; place-items: center; flex: 1 1 0; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 7px; color: var(--button-text); background: transparent; box-shadow: none; font-size: var(--ui-body-size); line-height: 1; transition: background-color .12s ease, color .12s ease, border-color .12s ease; cursor: pointer; }
.theme-switcher-btn:hover:not(.active) { color: var(--button-text); background: var(--button-bg-hover); } .theme-switcher-btn:hover:not(.active) { color: var(--button-text); background: var(--button-bg-hover); }
.theme-switcher-btn.active { border-color: var(--button-border); color: var(--button-marked-text); background: var(--button-marked-bg); box-shadow: none; font-weight: 700; } .theme-switcher-btn.active { border-color: var(--button-border); color: var(--button-marked-text); background: var(--button-marked-bg); box-shadow: none; font-weight: var(--fw-semibold); }
.sidebar-toggle-inline { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 7px; color: var(--button-text); background: transparent; font-size: 16px; font-weight: bold; cursor: pointer; } .sidebar-toggle-inline { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 7px; color: var(--button-text); background: transparent; font-size: 16px; font-weight: var(--fw-bold); cursor: pointer; }
.sidebar-toggle-inline:hover { background: var(--button-bg-hover); color: var(--text-primary); } .sidebar-toggle-inline:hover { background: var(--button-bg-hover); color: var(--text-primary); }
.sidebar-primary-actions { display: flex; flex-direction: column; gap: 8px; margin: 10px 0 0 0; padding: 0 0 12px 10px; border-top: 0; border-bottom: 0; } .sidebar-primary-actions { display: flex; flex-direction: column; gap: 8px; margin: 10px 0 0 0; padding: 0 0 12px 10px; border-top: 0; border-bottom: 0; }
.sidebar-tab-btn { position: relative; display: flex; align-items: center; gap: 13px; width: calc(100% - 10px); min-height: 42px; padding: 8px 14px; border: 1px solid transparent; border-radius: 8px; color: var(--text-secondary); background: transparent; box-shadow: none; font-size: var(--ui-heading-size); font-weight: 500; letter-spacing: 0; text-align: left; text-shadow: none; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease; } .sidebar-tab-btn { position: relative; display: flex; align-items: center; gap: 13px; width: calc(100% - 10px); min-height: 42px; padding: 8px 14px; border: 1px solid transparent; border-radius: 8px; color: var(--text-secondary); background: transparent; box-shadow: none; font-size: var(--ui-heading-size); font-weight: var(--fw-regular); letter-spacing: 0; text-align: left; text-shadow: none; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease; }
.sidebar-tab-btn svg { flex: 0 0 22px; width: 22px; height: 22px; color: var(--text-muted); } .sidebar-tab-btn svg { flex: 0 0 22px; width: 22px; height: 22px; color: var(--text-muted); }
.sidebar-tab-btn span { min-width: 0; } .sidebar-tab-btn span { min-width: 0; }
.sidebar-tab-btn:hover:not(.active):not(:disabled) { color: var(--text-primary); background: var(--surface-hover); border-color: transparent; box-shadow: none; transform: none; } .sidebar-tab-btn:hover:not(.active):not(:disabled) { color: var(--text-primary); background: var(--surface-hover); border-color: transparent; box-shadow: none; transform: none; }
@ -74,7 +73,7 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
background: var(--surface-app); background: var(--surface-app);
box-shadow: none; box-shadow: none;
outline: 0; outline: 0;
font-weight: 700; font-weight: var(--fw-semibold);
} }
.sidebar-tab-btn.active svg { .sidebar-tab-btn.active svg {
color: var(--band-surface); color: var(--band-surface);
@ -89,7 +88,7 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
.toolbar:disabled, .workbar:disabled { opacity: .5; } .toolbar:disabled, .workbar:disabled { opacity: .5; }
.toolbar:disabled *, .workbar:disabled * { pointer-events: none; } .toolbar:disabled *, .workbar:disabled * { pointer-events: none; }
.search-tools { grid-column: 1 / -1; display: flex; align-items: center; gap: 6px; flex: 1 1 auto; flex-wrap: wrap; margin: 0; padding: 0; } .search-tools { grid-column: 1 / -1; display: flex; align-items: center; gap: 6px; flex: 1 1 auto; flex-wrap: wrap; margin: 0; padding: 0; }
.standard-filter-btn { flex: 0 0 auto; min-width: 96px; margin-left: 0; font-weight: 700; } .standard-filter-btn { flex: 0 0 auto; min-width: 96px; margin-left: 0; font-weight: var(--fw-semibold); }
.title-select { flex: 0 0 215px; width: 215px; } .title-select { flex: 0 0 215px; width: 215px; }
.sort-select { flex: 0 0 345px; width: 345px; } .sort-select { flex: 0 0 345px; width: 345px; }
.status-select { flex: 0 0 190px; width: 190px; } .status-select { flex: 0 0 190px; width: 190px; }
@ -122,16 +121,17 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
.area-kicker { display: block; margin-bottom: 10px; color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; } .area-kicker { display: block; margin-bottom: 10px; color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; }
.area-landing-card h2 { margin: 0 0 12px; color: var(--heading); font-size: var(--ui-glyph-size); } .area-landing-card h2 { margin: 0 0 12px; color: var(--heading); font-size: var(--ui-glyph-size); }
.area-landing-card p { max-width: 510px; margin: 0 auto 24px; color: var(--muted); line-height: 1.6; } .area-landing-card p { max-width: 510px; margin: 0 auto 24px; color: var(--muted); line-height: 1.6; }
.area-landing-card .hidden-input + .btn { min-width: 170px; font-weight: 800; } .area-landing-card .hidden-input + .btn { min-width: 170px; font-weight: var(--fw-semibold); }
.records-layout { display: grid; grid-template-columns: clamp(420px, 27vw, 520px) minmax(0, 1fr); height: 100%; min-height: 0; background: var(--surface-app); } .records-layout { display: grid; grid-template-columns: clamp(420px, 27vw, 520px) minmax(0, 1fr); height: 100%; min-height: 0; background: var(--surface-app); }
.record-list { min-width: 0; min-height: 0; padding: 8px 6px; background: var(--surface-app); overflow-y: auto; scrollbar-color: var(--line-strong) var(--surface-inset); scrollbar-width: thin; } .record-list { min-width: 0; min-height: 0; padding: 8px 6px; background: var(--surface-app); overflow-y: auto; scrollbar-color: var(--line-strong) var(--surface-inset); scrollbar-width: thin; }
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); align-content: start; gap: 14px; min-height: 100%; padding: var(--panel-inset); background: var(--surface-app); } .tile-view-scroll { padding: var(--panel-inset); }
.tile-grid { box-sizing: border-box; display: grid; grid-template-columns: repeat(auto-fill, minmax(0, var(--tile-content-width))); align-content: start; justify-content: start; gap: var(--segment-gap); min-height: 100%; padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.table-scroll { max-width: 100%; overflow: auto; background: var(--surface-app); } .table-scroll { max-width: 100%; overflow: auto; background: var(--surface-app); }
.raw-view { min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); font: var(--fs-lg)/1.55 var(--font-mono); } .raw-view { min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); font: var(--fs-lg)/1.55 var(--font-mono); }
@media (max-width: 1500px) { .topbar-kpi { min-width: 74px; padding-inline: 10px; } .topbar-kpi.kpi-format { min-width: 190px; } .delimiter-select { width: 170px; } .tab-btn { min-width: 68px; } } @media (max-width: 1500px) { .topbar-kpi { min-width: 74px; padding-inline: 10px; } .topbar-kpi.kpi-format { min-width: 190px; } .delimiter-select { width: 170px; } .tab-btn { min-width: 68px; } }
@media (max-width: 1200px) { .status-line-tools { position: static; width: min(520px, 100%); margin-left: auto; transform: none; } } @media (max-width: 1200px) { .status-line-tools { position: static; width: min(520px, 100%); margin-left: auto; transform: none; } }
@media (max-width: 1050px) { .workbar { grid-template-columns: minmax(0, 1fr); } .workbar-action { grid-column: 1; justify-content: flex-end; } .records-layout { grid-template-columns: minmax(320px, 42%) minmax(0, 1fr); } } @media (max-width: 1050px) { .workbar { grid-template-columns: minmax(0, 1fr); } .workbar-action { grid-column: 1; justify-content: flex-end; } .records-layout { grid-template-columns: minmax(320px, 42%) minmax(0, 1fr); } }
@media (max-width: 760px) { .app-shell > main { grid-template-columns: 0 minmax(0, 1fr); } .app-shell > main > aside { visibility: hidden; } .sidebar-toggle { left: 0; } .records-layout { grid-template-columns: 1fr; } .record-list { max-height: 280px; border-right: 0; border-bottom: 1px solid var(--line); } .tile-grid { grid-template-columns: 1fr; padding: var(--panel-inset); } } @media (max-width: 760px) { .app-shell > main { grid-template-columns: 0 minmax(0, 1fr); } .app-shell > main > aside { visibility: hidden; } .sidebar-toggle { left: 0; } .records-layout { grid-template-columns: 1fr; } .record-list { max-height: 280px; border-right: 0; border-bottom: 1px solid var(--line); } .tile-grid { grid-template-columns: minmax(0, 1fr); } }
.app-sidebar { display: flex; flex-direction: column; min-height: 100%; padding: 0; } .app-sidebar { display: flex; flex-direction: column; min-height: 100%; padding: 0; }
.sidebar-primary-actions { order: 0; } .sidebar-primary-actions { order: 0; }
.sidebar-severity-rack { order: 1; } .sidebar-severity-rack { order: 1; }
@ -144,7 +144,7 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
.connection-unavailable .connection-state-square.is-offline, .connection-unavailable .connection-state-square.is-offline,
.connection-checking .connection-state-square.is-offline { opacity: 1; } .connection-checking .connection-state-square.is-offline { opacity: 1; }
.area-topbar { display: flex; align-items: center; gap: 14px; flex: 1; min-width: 0; } .area-topbar { display: flex; align-items: center; gap: 14px; flex: 1; min-width: 0; }
.area-topbar strong { margin-right: auto; color: var(--heading); font-size: calc(var(--fs-xl) * 4); font-weight: var(--fw-semibold); line-height: 1.15; letter-spacing: -.04em; } .area-topbar strong { margin-right: auto; color: var(--heading); font-size: var(--type-h1-size); font-weight: var(--fw-semibold); line-height: 1.1; letter-spacing: -.04em; }
.area-topbar-heading { display: flex; align-items: center; gap: 18px; min-width: 0; margin-right: auto; overflow: hidden; } .area-topbar-heading { display: flex; align-items: center; gap: 18px; min-width: 0; margin-right: auto; overflow: hidden; }
.area-topbar-heading > strong { flex: 0 0 auto; margin-right: 0; } .area-topbar-heading > strong { flex: 0 0 auto; margin-right: 0; }
.pathfinder-filter-trail { display: flex; align-items: center; gap: 12px; min-width: 0; color: var(--heading); font-size: calc(var(--fs-xl) * 1.5); font-weight: var(--fw-semibold); line-height: 1.15; white-space: nowrap; } .pathfinder-filter-trail { display: flex; align-items: center; gap: 12px; min-width: 0; color: var(--heading); font-size: calc(var(--fs-xl) * 1.5); font-weight: var(--fw-semibold); line-height: 1.15; white-space: nowrap; }

View File

@ -206,16 +206,16 @@ button, input, select, textarea { font-family: inherit; }
.reader-nav { margin-bottom: var(--legacy-space-6); color: var(--muted); font-size: var(--ui-control-size); } .reader-nav { margin-bottom: var(--legacy-space-6); color: var(--muted); font-size: var(--ui-control-size); }
.reader-nav button { color: var(--text); } .reader-nav button { color: var(--text); }
.reader-heading { padding-bottom: var(--legacy-space-5); border-color: var(--line); } .reader-heading { padding-bottom: var(--legacy-space-5); border-color: var(--line); }
/* Der Fundname steht auf derselben Stufe wie die Finding-Ueberschrift. */ /* Der Fundname steht als kompakter Detailtitel eine Stufe ueber dem Inhalt. */
.reader-heading h2 { .reader-heading h2 {
color: var(--heading); color: var(--heading);
font-size: var(--ui-reading-heading-size); font-size: var(--type-detail-title-size);
font-weight: var(--ui-strong-weight); font-weight: var(--ui-strong-weight);
line-height: 1.3; line-height: 1.3;
letter-spacing: 0; letter-spacing: 0;
} }
.reader-title-hex { width: 26px; height: 30px; } .reader-title-hex { width: 26px; height: 30px; }
.finding-badges, .mitre-line { gap: var(--legacy-space-2); margin-top: var(--legacy-space-3); } .finding-badges, .mitre-line { gap: var(--legacy-space-2); margin-top: var(--legacy-space-3); font-size: var(--ui-badge-size); }
.status-badge, .severity-badge, .mitre-badge, .reader-severity { .status-badge, .severity-badge, .mitre-badge, .reader-severity {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -226,7 +226,7 @@ button, input, select, textarea { font-family: inherit; }
border-radius: var(--legacy-radius-sm); border-radius: var(--legacy-radius-sm);
color: var(--text); color: var(--text);
background: transparent; background: transparent;
font-size: var(--ui-label-size); font-size: var(--ui-badge-size);
font-weight: var(--ui-label-weight); font-weight: var(--ui-label-weight);
line-height: 1.18; line-height: 1.18;
letter-spacing: var(--ui-label-tracking); letter-spacing: var(--ui-label-tracking);
@ -244,7 +244,7 @@ button, input, select, textarea { font-family: inherit; }
.reader-summary, .review-grid, .all-fields { border-color: var(--line); } .reader-summary, .review-grid, .all-fields { border-color: var(--line); }
/* Die Ueberschrift des Finding-Abschnitts traegt die Pruefbeschreibung der /* Die Ueberschrift des Finding-Abschnitts traegt die Pruefbeschreibung der
Regel und ist deshalb ein Satz, keine Bereichsmarke - entsprechend gesetzt. */ Regel und ist deshalb ein Satz, keine Bereichsmarke - entsprechend gesetzt. */
.reader-summary h3 { color: var(--heading); font-size: var(--ui-reading-heading-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; } .reader-summary h3 { color: var(--heading); font-size: var(--type-h3-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; }
/* Der Finding-Abschnitt ist die Lesefläche des Berichts. Beschriftungen und /* Der Finding-Abschnitt ist die Lesefläche des Berichts. Beschriftungen und
Fliesstext stehen deshalb auf der Lesestufe, nicht auf der dichten Fliesstext stehen deshalb auf der Lesestufe, nicht auf der dichten
Werkzeugtypografie der Leisten, Kacheln und Raster. */ Werkzeugtypografie der Leisten, Kacheln und Raster. */
@ -264,12 +264,16 @@ button, input, select, textarea { font-family: inherit; }
flach mit Haarlinien. */ flach mit Haarlinien. */
.tile-grid { .tile-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(0, var(--tile-content-width)));
align-items: stretch; align-items: stretch;
gap: var(--legacy-space-5); align-content: start;
justify-content: start;
gap: var(--segment-gap);
min-height: 100%; min-height: 100%;
padding: var(--panel-inset); padding: var(--segment-inset);
background: var(--bg); border: var(--border-hair) solid var(--segment-border);
border-radius: var(--segment-radius);
background: var(--segment-surface);
} }
.data-tile { .data-tile {
position: relative; position: relative;
@ -280,23 +284,22 @@ button, input, select, textarea { font-family: inherit; }
min-width: 0; min-width: 0;
min-height: 112px; min-height: 112px;
padding: var(--legacy-space-4); padding: var(--legacy-space-4);
border: var(--border-hair) solid var(--line); border: var(--border-hair) solid var(--tile-border);
border-radius: var(--radius-lg); border-radius: var(--tile-radius);
color: var(--text); color: var(--text);
background: var(--panel-raised); background: var(--tile-surface);
box-shadow: var(--shadow-raised); box-shadow: var(--tile-card-shadow);
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
text-align: left; text-align: left;
transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .12s ease; transition: background .12s ease, border-color .12s ease;
} }
.data-tile:hover, .data-tile:focus-visible { .data-tile:hover, .data-tile:focus-visible {
border-color: var(--line-strong); border-color: var(--line-strong);
border-radius: var(--radius-lg); border-radius: var(--tile-radius);
background: var(--panel-hover); background: var(--tile-surface-hover);
box-shadow: var(--tile-card-shadow);
outline: 0; outline: 0;
transform: translateY(-2px);
box-shadow: var(--shadow-raised-hover);
} }
.data-tile-hex { .data-tile-hex {
position: relative; position: relative;
@ -324,9 +327,9 @@ button, input, select, textarea { font-family: inherit; }
.data-tile.severity-low .data-tile-hex { background: var(--low); } .data-tile.severity-low .data-tile-hex { background: var(--low); }
.data-tile.severity-info .data-tile-hex { background: var(--info); } .data-tile.severity-info .data-tile-hex { background: var(--info); }
.data-tile-content { min-width: 0; } .data-tile-content { min-width: 0; }
/* Bleibt bewusst gleich gross wie `.record-title`: dieselbe Datenmenge darf in /* Matrix- und Listentitel teilen die kompakte Inhaltsstufe, damit lange
zwei Ansichten nicht unterschiedlich gewichtet wirken. */ Scannerbezeichnungen nicht die Kachelstruktur verdraengen. */
.data-tile-content h3 { margin: 0; color: var(--heading); font-size: var(--ui-heading-size); font-weight: var(--ui-strong-weight); line-height: 1.35; overflow-wrap: anywhere; } .data-tile-content h3 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--ui-strong-weight); line-height: 1.35; overflow-wrap: normal; word-break: normal; hyphens: manual; }
/* Die Groesse gilt dem Outcome-Text ("IOE Found"), der sonst die 14 px des /* Die Groesse gilt dem Outcome-Text ("IOE Found"), der sonst die 14 px des
Rumpfes erbte und damit groesser stand als dieselbe Aussage in der Rumpfes erbte und damit groesser stand als dieselbe Aussage in der
Trefferliste. Die Plaketten daneben tragen ihre eigene Groesse. */ Trefferliste. Die Plaketten daneben tragen ihre eigene Groesse. */
@ -395,9 +398,9 @@ button, input, select, textarea { font-family: inherit; }
padding: var(--panel-inset); padding: var(--panel-inset);
color: var(--text); color: var(--text);
background: var(--panel); background: var(--panel);
font-family: "Cascadia Code", Consolas, monospace; font-family: var(--font-ui);
font-size: var(--fs-lg); font-size: var(--fs-lg);
font-weight: 400; font-weight: var(--fw-regular);
line-height: 1.5; line-height: 1.5;
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: anywhere; overflow-wrap: anywhere;
@ -416,7 +419,7 @@ button, input, select, textarea { font-family: inherit; }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.data-tile, .record-card { transition: none; } .data-tile, .record-card { transition: none; }
.data-tile:hover, .data-tile:focus-visible, .record-card:hover { transform: none; } .record-card:hover { transform: none; }
} }

View File

@ -4,9 +4,9 @@
/* Steht im gemeinsamen Kopfcontainer der Shell. */ /* Steht im gemeinsamen Kopfcontainer der Shell. */
.finding-path-controls { display: flex; align-items: end; gap: 10px; min-width: 0; height: 100%; padding: 8px 0 0; background: transparent; overflow-x: auto; } .finding-path-controls { display: flex; align-items: end; gap: 10px; min-width: 0; height: 100%; padding: 8px 0 0; background: transparent; overflow-x: auto; }
.finding-path-title { display: grid; align-self: stretch; align-content: center; min-width: 240px; margin-right: auto; } .finding-path-title { display: grid; align-self: stretch; align-content: center; min-width: 240px; margin-right: auto; }
.finding-path-title span { color: var(--heading); font-size: var(--ui-heading-size); font-weight: 800; } .finding-path-title span { color: var(--heading); font-size: var(--ui-heading-size); font-weight: var(--fw-semibold); }
.finding-path-title strong { color: var(--muted); font-size: var(--ui-control-size); font-weight: 650; } .finding-path-title strong { color: var(--muted); font-size: var(--ui-control-size); font-weight: var(--fw-semibold); }
.finding-path-controls label { display: grid; gap: 6px; min-width: 180px; color: var(--muted); font-size: var(--ui-value-size); font-weight: 750; letter-spacing: .045em; text-transform: uppercase; } .finding-path-controls label { display: grid; gap: 6px; min-width: 180px; color: var(--muted); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); letter-spacing: 0; text-transform: none; }
/* Dieselben Masse wie die Filter im Command Band von Work: dort sind Controls /* Dieselben Masse wie die Filter im Command Band von Work: dort sind Controls
`--control-height` hoch und tragen `--ui-control-size`. Mit 40 px und `--control-height` hoch und tragen `--ui-control-size`. Mit 40 px und
`--ui-body-size` waren die Graph-Filter als einzige Ansicht groesser. */ `--ui-body-size` waren die Graph-Filter als einzige Ansicht groesser. */
@ -14,44 +14,44 @@
.finding-path-search { min-width: 260px !important; } .finding-path-search { min-width: 260px !important; }
.finding-path-layout { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; width: 100%; max-width: 100%; min-width: 0; min-height: 0; overflow: hidden; } .finding-path-layout { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; width: 100%; max-width: 100%; min-width: 0; min-height: 0; overflow: hidden; }
.finding-path-main { min-width: 0; min-height: 0; padding: 16px; overflow: auto; background-image: radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--line) 55%, transparent) 1px, transparent 0); background-size: 22px 22px; } .finding-path-main { min-width: 0; min-height: 0; padding: 16px; overflow: auto; background-image: radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--line) 55%, transparent) 1px, transparent 0); background-size: 22px 22px; }
.active-finding-path { min-width: max-content; margin-bottom: 18px; padding: 14px; border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-md); background: color-mix(in srgb, var(--panel) 94%, transparent); } .active-finding-path { min-width: max-content; margin-bottom: 18px; padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.active-finding-path > header { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 13px; } .active-finding-path > header { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 13px; }
.active-finding-path > header div { display: grid; } .active-finding-path > header div { display: grid; }
.active-finding-path > header span:first-child { color: var(--heading); font-size: var(--ui-heading-size); font-weight: 850; } .active-finding-path > header span:first-child { color: var(--heading); font-size: var(--ui-heading-size); font-weight: var(--fw-semibold); }
.active-finding-path > header strong { color: var(--muted); font-size: var(--ui-control-size); font-weight: 600; } .active-finding-path > header strong { color: var(--muted); font-size: var(--ui-control-size); font-weight: 600; }
.path-method { color: var(--muted); font-size: var(--ui-value-size); text-transform: uppercase; } .path-method { color: var(--muted); font-size: var(--ui-value-size); text-transform: uppercase; }
.active-path-track { display: flex; align-items: stretch; gap: 8px; } .active-path-track { display: flex; align-items: stretch; gap: 8px; }
.active-path-step-wrap { display: flex; align-items: center; gap: 8px; } .active-path-step-wrap { display: flex; align-items: center; gap: 8px; }
.active-path-arrow { color: var(--muted); font-size: var(--ui-glyph-size); font-weight: 300; } .active-path-arrow { color: var(--muted); font-size: var(--ui-glyph-size); font-weight: var(--fw-regular); }
.active-path-step { position: relative; display: grid; gap: 5px; width: 280px; min-height: 126px; padding: 12px 13px 11px 16px; border: 1px solid var(--line); border-radius: 4px; color: var(--text); background: var(--panel); text-align: left; } .active-path-step { position: relative; display: grid; gap: 5px; width: 280px; min-width: 0; min-height: 126px; padding: 12px 13px 11px 16px; overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; }
.active-path-step::before { content: ''; position: absolute; inset: 0 auto 0 0; width: var(--marker-width); background: var(--severity); } .active-path-step::before { content: ''; position: absolute; inset: 0 auto 0 0; width: var(--marker-width); background: var(--severity); }
.active-path-step:hover { border-color: var(--severity); background: var(--panel-hover); } .active-path-step:hover { border-color: var(--severity); background: var(--panel-hover); }
.active-path-step small { color: var(--severity); font-size: var(--ui-value-size); font-weight: 850; letter-spacing: .04em; text-transform: uppercase; } .active-path-step small { color: var(--severity); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; }
.active-path-step strong { color: var(--heading); font-size: var(--ui-body-size); line-height: 1.35; } .active-path-step strong { color: var(--heading); font-size: var(--ui-body-size); line-height: 1.35; }
.active-path-step span { align-self: end; color: var(--muted); font-size: var(--ui-value-size); } .active-path-step span { align-self: end; color: var(--muted); font-size: var(--ui-value-size); }
.finding-tactic-board { display: flex; align-items: flex-start; gap: 12px; min-width: max-content; } .finding-tactic-board { display: flex; align-items: flex-start; gap: 12px; min-width: max-content; }
.finding-tactic-column { width: 310px; border: 1px solid var(--line); border-radius: 4px; background: color-mix(in srgb, var(--panel) 92%, transparent); } .finding-tactic-column { width: 310px; border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.finding-tactic-column > header { display: flex; align-items: center; justify-content: space-between; min-height: 42px; padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--panel-soft); } .finding-tactic-column > header { display: flex; align-items: center; justify-content: space-between; min-height: 42px; padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--panel-soft); }
.finding-tactic-column > header strong { color: var(--heading); font-size: var(--ui-body-size); } .finding-tactic-column > header strong { color: var(--heading); font-size: var(--ui-body-size); }
.finding-tactic-column > header span { display: grid; place-items: center; min-width: 26px; height: 26px; border-radius: var(--radius-pill); color: var(--muted); background: var(--surface-inset); font-size: var(--ui-value-size); font-weight: 800; } .finding-tactic-column > header span { display: grid; place-items: center; min-width: 26px; height: 26px; border-radius: var(--radius-pill); color: var(--muted); background: var(--surface-inset); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); }
.finding-tactic-column > div { display: grid; gap: 7px; max-height: 560px; padding: 8px; overflow-y: auto; } .finding-tactic-column > div { display: grid; gap: 7px; max-height: 560px; padding: 8px; overflow-y: auto; }
.finding-path-card { position: relative; display: grid; gap: 7px; width: 100%; min-height: 126px; padding: 11px 11px 10px 14px; border: 1px solid var(--line); border-radius: 3px; color: var(--text); background: var(--panel); text-align: left; } .finding-path-card { position: relative; display: grid; gap: 7px; width: 100%; min-width: 0; min-height: 126px; padding: 11px 11px 10px 14px; overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; }
.finding-path-card:hover { border-color: var(--line-strong); background: var(--panel-hover); } .finding-path-card:hover { border-color: var(--line-strong); background: var(--tile-surface-hover); }
.finding-path-card.is-path-step { border-left: var(--marker-width) solid var(--severity); padding-left: 7px; } .finding-path-card.is-path-step { border-left: var(--marker-width) solid var(--severity); padding-left: 7px; }
.finding-path-card.is-selected { outline: 2px solid var(--legacy-accent); outline-offset: -2px; } .finding-path-card.is-selected { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); }
.finding-path-card-head { display: flex; align-items: center; gap: 7px; color: var(--severity); font-size: var(--ui-label-size); font-weight: 850; letter-spacing: .04em; text-transform: uppercase; } .finding-path-card-head { display: flex; align-items: center; gap: 7px; color: var(--severity); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; }
.finding-path-card-head i { width: 8px; height: 8px; border-radius: 50%; background: var(--severity); } .finding-path-card-head i { width: 8px; height: 8px; border-radius: 50%; background: var(--severity); }
.finding-path-card-head b { margin-left: auto; color: var(--success); font-size: var(--ui-label-size); } .finding-path-card-head b { margin-left: auto; color: var(--success); font-size: var(--ui-label-size); }
.finding-path-card > strong { color: var(--heading); font-size: var(--ui-body-size); line-height: 1.4; } .finding-path-card > strong { color: var(--heading); font-size: var(--ui-body-size); line-height: 1.4; }
.finding-path-metrics, .finding-techniques { color: var(--muted); font-size: var(--ui-value-size); } .finding-path-metrics, .finding-techniques { color: var(--muted); font-size: var(--ui-value-size); }
.finding-techniques { color: var(--text); } .finding-techniques { color: var(--text); }
.finding-path-detail { display: grid; grid-template-columns: minmax(260px, 1.35fr) minmax(320px, 1fr); column-gap: 40px; align-items: start; width: 100%; min-width: 0; min-height: 320px; box-sizing: border-box; padding: 28px 32px 40px; border-top: 1px solid var(--line); background: var(--panel); overflow: visible; } .finding-path-detail { display: grid; grid-template-columns: minmax(260px, 1.35fr) minmax(320px, 1fr); column-gap: 40px; align-items: start; width: 100%; min-width: 0; min-height: 320px; box-sizing: border-box; padding: 28px 32px 40px; border-top: 1px solid var(--line); background: var(--panel); overflow: visible; }
.finding-detail-severity { justify-self: start; display: inline-block; padding: 5px 9px; color: var(--severity-ink); background: var(--severity); font-size: var(--ui-value-size); font-weight: 850; text-transform: uppercase; } .finding-detail-severity { justify-self: start; display: inline-block; padding: 5px 9px; color: var(--severity-ink); background: var(--severity); font-size: var(--ui-value-size); font-weight: var(--fw-bold); text-transform: uppercase; }
.finding-path-detail h2 { grid-column: 1; margin: 14px 0 22px; color: var(--heading); font-size: var(--ui-glyph-size); line-height: 1.35; } .finding-path-detail h2 { grid-column: 1; margin: 14px 0 22px; color: var(--heading); font-size: var(--ui-glyph-size); line-height: 1.35; }
.finding-path-detail dl { grid-column: 1; display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px 18px; margin: 0; } .finding-path-detail dl { grid-column: 1; display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px 18px; margin: 0; }
.finding-path-detail dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: 800; text-transform: uppercase; } .finding-path-detail dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); text-transform: none; }
.finding-path-detail dd { margin: 0; color: var(--text); font-size: var(--ui-body-size); overflow-wrap: anywhere; } .finding-path-detail dd { margin: 0; color: var(--text); font-size: var(--ui-body-size); overflow-wrap: anywhere; }
.finding-open-detail { grid-column: 2; grid-row: 1 / span 2; align-self: end; width: 100%; min-height: 42px; margin: 0; font-size: var(--ui-body-size); font-weight: 750; } .finding-open-detail { grid-column: 2; grid-row: 1 / span 2; align-self: end; width: 100%; min-height: 42px; margin: 0; font-size: var(--ui-body-size); font-weight: var(--fw-semibold); }
.finding-path-detail p { grid-column: 2; margin: 18px 0 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.65; } .finding-path-detail p { grid-column: 2; margin: 18px 0 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.65; }
.finding-path-empty { display: grid; place-content: center; gap: 8px; min-height: 100%; color: var(--muted); text-align: center; } .finding-path-empty { display: grid; place-content: center; gap: 8px; min-height: 100%; color: var(--muted); text-align: center; }
.finding-path-empty strong { color: var(--heading); font-size: var(--ui-heading-size); } .finding-path-empty strong { color: var(--heading); font-size: var(--ui-heading-size); }
@ -79,10 +79,10 @@
.finding-detail-main { min-width: 0; } .finding-detail-main { min-width: 0; }
.finding-detail-main h2 { margin: 14px 0 22px; color: var(--heading); font-size: var(--ui-glyph-size); line-height: 1.35; } .finding-detail-main h2 { margin: 14px 0 22px; color: var(--heading); font-size: var(--ui-glyph-size); line-height: 1.35; }
.finding-detail-main dl { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px 18px; margin: 0; } .finding-detail-main dl { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px 18px; margin: 0; }
.finding-detail-main dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: 800; text-transform: uppercase; } .finding-detail-main dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); text-transform: none; }
.finding-detail-main dd { margin: 0; color: var(--text); font-size: var(--ui-body-size); overflow-wrap: anywhere; } .finding-detail-main dd { margin: 0; color: var(--text); font-size: var(--ui-body-size); overflow-wrap: anywhere; }
.finding-detail-actions { display: grid; align-content: start; gap: 18px; min-width: 0; padding-top: 34px; } .finding-detail-actions { display: grid; align-content: start; gap: 18px; min-width: 0; padding-top: 34px; }
.finding-detail-actions .finding-open-detail { grid-column: auto; grid-row: auto; align-self: auto; width: 100%; min-height: 44px; margin: 0; font-size: var(--ui-body-size); font-weight: 750; } .finding-detail-actions .finding-open-detail { grid-column: auto; grid-row: auto; align-self: auto; width: 100%; min-height: 44px; margin: 0; font-size: var(--ui-body-size); font-weight: var(--fw-semibold); }
.finding-detail-actions p { grid-column: auto; grid-row: auto; margin: 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.65; } .finding-detail-actions p { grid-column: auto; grid-row: auto; margin: 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.65; }
@media (max-width: 1050px) { @media (max-width: 1050px) {
.finding-path-detail { grid-template-columns: 1fr; } .finding-path-detail { grid-template-columns: 1fr; }
@ -116,7 +116,7 @@
.finding-tactic-heading { display: grid; min-width: 0; } .finding-tactic-heading { display: grid; min-width: 0; }
.finding-tactic-heading strong { color: var(--heading); font-size: var(--ui-heading-size) !important; font-weight: 800; line-height: 1.25; } .finding-tactic-heading strong { color: var(--heading); font-size: var(--ui-heading-size) !important; font-weight: var(--fw-semibold); line-height: 1.25; }
.finding-tactic-tools { display: flex; align-items: center; gap: 9px; margin-left: auto; padding-left: 10px; } .finding-tactic-tools { display: flex; align-items: center; gap: 9px; margin-left: auto; padding-left: 10px; }
.finding-tactic-tools > span { min-width: 26px; height: 26px; margin: 0; } .finding-tactic-tools > span { min-width: 26px; height: 26px; margin: 0; }
.finding-tactic-workspace { display: grid; place-items: center; width: 26px; height: 26px; cursor: pointer; } .finding-tactic-workspace { display: grid; place-items: center; width: 26px; height: 26px; cursor: pointer; }
@ -172,7 +172,7 @@
margin: 0; margin: 0;
color: var(--heading); color: var(--heading);
font-size: var(--ui-heading-size); font-size: var(--ui-heading-size);
font-weight: 850; font-weight: var(--fw-semibold);
line-height: 1.2; line-height: 1.2;
} }
.finding-path-title span { .finding-path-title span {
@ -196,12 +196,12 @@
.meter-label { .meter-label {
color: var(--muted); color: var(--muted);
font-size: var(--ui-value-size); font-size: var(--ui-value-size);
font-weight: 700; font-weight: var(--fw-semibold);
} }
.meter-val { .meter-val {
color: var(--heading); color: var(--heading);
font-size: var(--ui-body-size); font-size: var(--ui-body-size);
font-weight: 850; font-weight: var(--fw-bold);
} }
.meter-gauge { .meter-gauge {
width: 90px; width: 90px;
@ -225,7 +225,7 @@
border-color: var(--legacy-accent); border-color: var(--legacy-accent);
background: var(--legacy-accent-active); background: var(--legacy-accent-active);
color: var(--text-on-accent); color: var(--text-on-accent);
font-weight: 800; font-weight: var(--fw-semibold);
} }
/* ---------- SVG Canvas & Board ---------- */ /* ---------- SVG Canvas & Board ---------- */
@ -263,7 +263,7 @@
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--legacy-radius-sm); border-radius: var(--legacy-radius-sm);
font-size: var(--ui-label-size); font-size: var(--ui-label-size);
font-weight: 750; font-weight: var(--fw-semibold);
} }
.origin-pill.native { .origin-pill.native {
color: var(--text-on-accent); color: var(--text-on-accent);
@ -317,7 +317,7 @@
margin: 0; margin: 0;
color: var(--heading); color: var(--heading);
font-size: var(--ui-body-size); font-size: var(--ui-body-size);
font-weight: 800; font-weight: var(--fw-semibold);
} }
.box-body { .box-body {
display: flex; display: flex;
@ -337,7 +337,7 @@
.mitre-ref-link { .mitre-ref-link {
color: var(--legacy-accent); color: var(--legacy-accent);
font-size: var(--ui-body-size); font-size: var(--ui-body-size);
font-weight: 700; font-weight: var(--fw-semibold);
text-decoration: none; text-decoration: none;
} }
.mitre-ref-link:hover { .mitre-ref-link:hover {

View File

@ -113,10 +113,7 @@
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-3); padding: var(--space-3);
overflow: hidden; overflow: hidden;
box-shadow: none;
} }
.obsidian-node-matrix-card .data-tile:hover,
.obsidian-node-matrix-card .data-tile:focus-visible { box-shadow: none; transform: none; }
.obsidian-node-matrix-card .data-tile-hex { width: 18px; height: 20px; } .obsidian-node-matrix-card .data-tile-hex { width: 18px; height: 20px; }
.obsidian-node-matrix-card .data-tile-content h3 { .obsidian-node-matrix-card .data-tile-content h3 {
display: -webkit-box; display: -webkit-box;
@ -161,14 +158,8 @@
gap: var(--legacy-space-3); gap: var(--legacy-space-3);
padding: var(--legacy-space-3); padding: var(--legacy-space-3);
overflow: hidden; overflow: hidden;
box-shadow: none;
cursor: pointer; cursor: pointer;
} }
.obsidian-node-card.data-tile:hover,
.obsidian-node-card.data-tile:focus-visible {
box-shadow: none;
transform: none;
}
.obsidian-node-card-technique .data-tile-hex, .obsidian-node-card-technique .data-tile-hex,
.obsidian-node-card-category .data-tile-hex, .obsidian-node-card-category .data-tile-hex,
.obsidian-node-card-defense .data-tile-hex, .obsidian-node-card-defense .data-tile-hex,
@ -206,7 +197,6 @@
border-color: var(--graph-selected); border-color: var(--graph-selected);
outline: 3px solid var(--graph-selected); outline: 3px solid var(--graph-selected);
outline-offset: -3px; outline-offset: -3px;
box-shadow: none;
} }
.obsidian-graph-toolbar, .obsidian-graph-toolbar,
@ -216,7 +206,7 @@
position: absolute; position: absolute;
z-index: 5; z-index: 5;
border: var(--bw-hairline) solid var(--border-default); border: var(--bw-hairline) solid var(--border-default);
border-radius: var(--radius-panel); border-radius: var(--radius-md);
color: var(--text-primary); color: var(--text-primary);
background: color-mix(in srgb, var(--surface-panel-raised) 94%, transparent); background: color-mix(in srgb, var(--surface-panel-raised) 94%, transparent);
backdrop-filter: blur(var(--space-2)); backdrop-filter: blur(var(--space-2));
@ -233,7 +223,7 @@
transform: translateX(-50%); transform: translateX(-50%);
} }
.obsidian-graph-search { display: grid; gap: var(--space-1); min-width: calc(var(--space-8) * 8); color: var(--text-secondary); font-size: var(--fs-sm); } .obsidian-graph-search { display: grid; gap: var(--space-1); min-width: calc(var(--space-8) * 8); color: var(--text-secondary); font-size: var(--fs-sm); }
.obsidian-graph-search input { min-height: var(--control-h); padding: 0 var(--space-2); border: var(--bw-hairline) solid var(--border-strong); border-radius: var(--radius-control); color: var(--text-primary); background: var(--surface-input); font-size: var(--fs-base); } .obsidian-graph-search input { min-height: var(--control-h); padding: 0 var(--space-2); border: var(--bw-hairline) solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); background: var(--surface-input); font-size: var(--fs-base); }
.obsidian-graph-count { align-self: center; min-width: max-content; padding-inline: var(--space-2); color: var(--text-secondary); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; } .obsidian-graph-count { align-self: center; min-width: max-content; padding-inline: var(--space-2); color: var(--text-secondary); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.obsidian-graph-branch-actions { .obsidian-graph-branch-actions {
display: flex; display: flex;
@ -280,7 +270,7 @@
transform: translateX(-50%); transform: translateX(-50%);
} }
.obsidian-graph-filters label { display: grid; gap: var(--space-1); color: var(--text-secondary); font-size: var(--fs-sm); } .obsidian-graph-filters label { display: grid; gap: var(--space-1); color: var(--text-secondary); font-size: var(--fs-sm); }
.obsidian-graph-filters select { min-height: var(--control-h); padding-inline: var(--space-2); border: var(--bw-hairline) solid var(--border-strong); border-radius: var(--radius-control); color: var(--text-primary); background: var(--surface-input); font-size: var(--fs-base); } .obsidian-graph-filters select { min-height: var(--control-h); padding-inline: var(--space-2); border: var(--bw-hairline) solid var(--border-strong); border-radius: var(--radius-sm); color: var(--text-primary); background: var(--surface-input); font-size: var(--fs-base); }
.obsidian-graph-zoom { top: var(--space-4); right: var(--space-4); display: grid; overflow: hidden; } .obsidian-graph-zoom { top: var(--space-4); right: var(--space-4); display: grid; overflow: hidden; }
.obsidian-graph-zoom button { display: grid; place-items: center; width: var(--space-8); height: var(--space-8); padding: 0; border: 0; border-bottom: var(--bw-hairline) solid var(--border-default); color: var(--text-primary); background: transparent; font-size: var(--fs-lg); cursor: pointer; } .obsidian-graph-zoom button { display: grid; place-items: center; width: var(--space-8); height: var(--space-8); padding: 0; border: 0; border-bottom: var(--bw-hairline) solid var(--border-default); color: var(--text-primary); background: transparent; font-size: var(--fs-lg); cursor: pointer; }
@ -315,7 +305,7 @@
min-height: var(--control-h); min-height: var(--control-h);
padding: 0 var(--space-4); padding: 0 var(--space-4);
border: var(--bw-hairline) solid var(--border-strong); border: var(--bw-hairline) solid var(--border-strong);
border-radius: var(--radius-control); border-radius: var(--radius-sm);
color: var(--text-primary); color: var(--text-primary);
background: color-mix(in srgb, var(--surface-panel-raised) 94%, transparent); background: color-mix(in srgb, var(--surface-panel-raised) 94%, transparent);
font-size: var(--fs-sm); font-size: var(--fs-sm);
@ -424,7 +414,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
min-height: var(--control-height); min-height: var(--control-h);
color: var(--text-primary); color: var(--text-primary);
font-size: var(--ui-label-size); font-size: var(--ui-label-size);
cursor: pointer; cursor: pointer;

View File

@ -8,8 +8,8 @@
.policy-profile-editor legend { padding-inline: var(--legacy-space-1); color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; } .policy-profile-editor legend { padding-inline: var(--legacy-space-1); color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; }
.policy-profile-editor .policy-profile-product-toggle { display: flex; grid-column: 1 / -1; grid-template-columns: auto 1fr; align-items: center; } .policy-profile-editor .policy-profile-product-toggle { display: flex; grid-column: 1 / -1; grid-template-columns: auto 1fr; align-items: center; }
.policy-profile-editor footer { display: flex; grid-column: 1 / -1; justify-content: flex-end; } .policy-profile-editor footer { display: flex; grid-column: 1 / -1; justify-content: flex-end; }
.policy-action-stages { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--legacy-space-3); margin-top: var(--legacy-space-4); } .policy-action-stages { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--segment-gap); margin-top: var(--legacy-space-4); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.policy-action-stages > section { min-width: 0; padding: var(--legacy-space-3); border: var(--border-hair) solid var(--line); background: var(--panel); } .policy-action-stages > section { min-width: 0; padding: var(--legacy-space-3); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
.policy-action-stages h4 { margin: 0 0 var(--legacy-space-2); color: var(--heading); font-size: var(--ui-body-size); } .policy-action-stages h4 { margin: 0 0 var(--legacy-space-2); color: var(--heading); font-size: var(--ui-body-size); }
.policy-action-stages section > p { margin: 0; color: var(--muted); font-size: var(--ui-control-size); } .policy-action-stages section > p { margin: 0; color: var(--muted); font-size: var(--ui-control-size); }
.policy-action-stages article { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-3); padding: var(--legacy-space-2) 0; border-top: var(--border-hair) solid var(--line); } .policy-action-stages article { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-3); padding: var(--legacy-space-2) 0; border-top: var(--border-hair) solid var(--line); }

View File

@ -3,8 +3,8 @@
.policy-analysis-panel { margin-top: var(--legacy-space-6); padding-top: var(--legacy-space-5); border-top: var(--border-hair) solid var(--line); } .policy-analysis-panel { margin-top: var(--legacy-space-6); padding-top: var(--legacy-space-5); border-top: var(--border-hair) solid var(--line); }
.policy-analysis-boundary { margin: 0 0 var(--legacy-space-4); padding: var(--legacy-space-3); border-left: 3px solid var(--warning); color: var(--heading); background: var(--panel-soft); font-size: var(--ui-control-size); line-height: 1.5; } .policy-analysis-boundary { margin: 0 0 var(--legacy-space-4); padding: var(--legacy-space-3); border-left: 3px solid var(--warning); color: var(--heading); background: var(--panel-soft); font-size: var(--ui-control-size); line-height: 1.5; }
.policy-analysis-empty { padding-block: 28px; } .policy-analysis-empty { padding-block: 28px; }
.policy-analysis-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--legacy-space-2); margin-bottom: var(--legacy-space-4); } .policy-analysis-summary { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start; gap: var(--segment-gap); margin-bottom: var(--legacy-space-4); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
.policy-analysis-summary article { display: grid; gap: var(--legacy-space-1); padding: var(--legacy-space-3); border: var(--border-hair) solid var(--line); background: var(--panel-raised); } .policy-analysis-summary article { display: grid; gap: var(--legacy-space-1); min-width: 0; padding: var(--legacy-space-3); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
.policy-analysis-summary span { color: var(--muted); font-size: var(--ui-control-size); } .policy-analysis-summary span { color: var(--muted); font-size: var(--ui-control-size); }
.policy-analysis-summary strong { color: var(--heading); font-family: var(--font-mono); font-size: var(--ui-heading-size); } .policy-analysis-summary strong { color: var(--heading); font-family: var(--font-mono); font-size: var(--ui-heading-size); }
.policy-analysis-columns { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: var(--legacy-space-4); align-items: start; } .policy-analysis-columns { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: var(--legacy-space-4); align-items: start; }

View File

@ -143,14 +143,15 @@
Alte rem-Skala auf die sechs Stufen der Designsprache abgebildet. Die Alte rem-Skala auf die sechs Stufen der Designsprache abgebildet. Die
Groessen bleiben dadurch unveraendert, nur ihre Quelle wechselt. */ Groessen bleiben dadurch unveraendert, nur ihre Quelle wechselt. */
--font: var(--font-ui); --font: var(--font-ui);
--ui-label-size: var(--fs-sm); --ui-label-size: var(--type-table-head-size);
--ui-small-size: var(--fs-sm); --ui-badge-size: var(--type-badge-size);
--ui-value-size: var(--fs-sm); --ui-small-size: var(--type-meta-size);
--ui-control-size: var(--fs-base); --ui-value-size: var(--type-label-size);
--ui-body-size: var(--fs-md); --ui-control-size: var(--type-button-size);
--ui-heading-size: var(--fs-lg); --ui-body-size: var(--type-body-size);
--ui-reading-size: var(--fs-lg); --ui-heading-size: var(--type-h3-size);
--ui-reading-heading-size: var(--fs-xl); --ui-reading-size: var(--type-body-size);
--ui-reading-heading-size: var(--type-h2-size);
/* Ausnahme: Glyphengroesse der Symbolschaltflaechen. Die Typografieskala /* Ausnahme: Glyphengroesse der Symbolschaltflaechen. Die Typografieskala
endet bei 18px, weil sie Text beschreibt; ein Symbol ist kein Text. endet bei 18px, weil sie Text beschreibt; ein Symbol ist kein Text.
Faellt mit der Umstellung auf .icon-btn. */ Faellt mit der Umstellung auf .icon-btn. */

View File

@ -24,20 +24,53 @@
:root { :root {
/* ---------- Typografie ---------- */ /* ---------- Typografie ---------- */
--font-ui: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif; /* Inter wird lokal mit dem Client-Build ausgeliefert. Auch Datenwerte und
--font-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace; Rohansichten verwenden dieselbe Familie; Hierarchie entsteht nicht durch
--font-donut-display: "Bahnschrift Condensed", "Arial Narrow", "Aptos Narrow", "Segoe UI", sans-serif; wechselnde Schriften, sondern ausschliesslich durch Groesse und Gewicht. */
--font-ui: "Inter Variable", sans-serif;
--font-mono: var(--font-ui);
--font-donut-display: var(--font-ui);
/* Semantische Typografierollen. Diese Rollen sind die verbindliche Quelle
fuer neue Ansichten; die darunterliegende fs-Skala bleibt nur als
Kompatibilitaetsschicht fuer bereits migrierte Komponenten bestehen. */
--type-h1-size: 56px;
--type-h2-size: 32px;
--type-h3-size: 18px;
--type-detail-title-size: 20px;
--type-h4-size: 16px;
--type-body-size: 16px;
--type-label-size: 14px;
--type-meta-size: 13px;
--type-badge-size: 12px;
--type-table-head-size: 13px;
--type-button-size: 14px;
/* ---------- Verbindlicher Donut-Standard ---------- */ /* ---------- Verbindlicher Donut-Standard ---------- */
/* Der Donut-Rack ist ein Panel wie jedes andere: Rack = Hintergrund, /* Der Donut-Rack ist ein Panel wie jedes andere: Rack = Hintergrund,
Karte = Element, Ringspur = eingesenkt. Geometrie und Datenfarben des Karte = Element, Ringspur = eingesenkt. Geometrie und Datenfarben des
Donut-Standards bleiben unveraendert, nur die Grundtoene folgen jetzt Donut-Standards bleiben unveraendert, nur die Grundtoene folgen jetzt
dem Panelstandard. */ dem Panelstandard. */
--donut-rack-surface: #242426; /* ---------- Verbindliche Segment-/Kachelgrammatik ---------- */
--donut-card-surface: #323238; --segment-surface: #242426;
--donut-card-surface-end: #323238; --segment-border: #383840;
--donut-track: #242426; --segment-radius: 10px;
--donut-border: #383840; --segment-inset: var(--space-3);
--segment-gap: var(--space-2);
--tile-surface: #323238;
--tile-surface-hover: #3A3A42;
--tile-border: #555B65;
--tile-selected-border: #A68AF9;
--tile-radius: 8px;
--tile-compact-width: 186px;
--tile-content-width: 320px;
--tile-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 2px 8px rgba(0, 0, 0, 0.24);
--donut-rack-surface: var(--segment-surface);
--donut-card-surface: var(--tile-surface);
--donut-card-surface-end: var(--tile-surface);
--donut-track: #555B65;
--donut-border: var(--segment-border);
--donut-value: #E2DFE6; --donut-value: #E2DFE6;
--donut-label: #B7BAC0; --donut-label: #B7BAC0;
--donut-all: #348CF2; --donut-all: #348CF2;
@ -46,26 +79,29 @@
--donut-not-applicable: #7B36F3; --donut-not-applicable: #7B36F3;
--donut-not-run: #8A8E95; --donut-not-run: #8A8E95;
--donut-error: #F23C47; --donut-error: #F23C47;
--donut-rack-shadow: 0 10px 28px rgba(0, 0, 0, 0.30), inset 0 1px 0 rgba(255, 255, 255, 0.04); --donut-rack-shadow: none;
--donut-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 2px 8px rgba(0, 0, 0, 0.24); --donut-card-shadow: var(--tile-card-shadow);
--donut-rack-radius: 10px; --donut-rack-radius: var(--segment-radius);
--donut-card-radius: 8px; --donut-card-radius: var(--tile-radius);
--donut-card-min-h: 196px; --donut-card-min-h: 196px;
--donut-ring-size: 128px; --donut-ring-size: 128px;
--donut-ring-stroke: 11px; --donut-ring-stroke: 12px;
--donut-progress-dash: 0 100; --donut-progress-dash: 0 100;
--donut-title-size: 16px; --donut-title-size: 16px;
--donut-value-size: 28px; --donut-value-size: 26px;
--donut-percent-size: 17px; --donut-percent-size: 14px;
--donut-compact-value-size: 22px;
--donut-compact-percent-size: 12px;
--donut-compact-unit-gap: 2px;
--datahub-readiness-dot-size: 14px; --datahub-readiness-dot-size: 14px;
--datahub-readiness-dot-inset: var(--space-1); --datahub-readiness-dot-inset: var(--space-1);
--fs-micro: 11px; /* Badges, Spaltenkoepfe, Zaehler */ --fs-micro: var(--type-meta-size);
--fs-sm: 12px; /* Tabellenzellen, Sekundaertext, Facetten */ --fs-sm: var(--type-meta-size);
--fs-base: 13px; /* Controls, Standardtext, Listenzeilen */ --fs-base: var(--type-label-size);
--fs-md: 14px; /* Objekttitel (Tile, ListRow, Inspector) */ --fs-md: var(--type-label-size);
--fs-lg: 16px; /* Panel- und Detailtitel */ --fs-lg: var(--type-body-size);
--fs-xl: 18px; /* Seitentitel in der Topbar. Groesster Wert im System. */ --fs-xl: var(--type-h3-size);
/* Pathfinder Micro Card: eigenstaendige, im Graphen begrenzt /* Pathfinder Micro Card: eigenstaendige, im Graphen begrenzt
bildschirmstabil skalierte Knotenkarte. Keine allgemeine bildschirmstabil skalierte Knotenkarte. Keine allgemeine
@ -80,8 +116,11 @@
--pathfinder-micro-action-highlight-mix: #FFFFFF; --pathfinder-micro-action-highlight-mix: #FFFFFF;
--fw-regular: 400; --fw-regular: 400;
--fw-medium: 500; --fw-content-medium: 500;
--fw-semibold: 600; --fw-semibold: 600;
--fw-bold: 700;
/* Kompatibilitaetsname: visuell gibt es keine vierte Gewichtsstufe. */
--fw-medium: var(--fw-semibold);
--lh-ui: 1.35; --lh-ui: 1.35;
--lh-prose: 1.55; --lh-prose: 1.55;
@ -376,11 +415,18 @@
color-scheme: light; color-scheme: light;
/* Donut-Standard: identische Geometrie, helle Referenzpalette. */ /* Donut-Standard: identische Geometrie, helle Referenzpalette. */
--donut-rack-surface: #E7E9ED; --segment-surface: #E7E9ED;
--donut-card-surface: #FFFFFF; --segment-border: #B9BEC7;
--donut-card-surface-end: #F3F4F6; --tile-surface: #FFFFFF;
--donut-track: #D5D8DE; --tile-surface-hover: #F3F4F6;
--donut-border: #B9BEC7; --tile-border: #B9BEC7;
--tile-selected-border: #D99A14;
--tile-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86), 0 2px 7px rgba(23, 25, 29, 0.12);
--donut-rack-surface: var(--segment-surface);
--donut-card-surface: var(--tile-surface);
--donut-card-surface-end: var(--tile-surface);
--donut-track: #B9BEC7;
--donut-border: var(--segment-border);
--donut-value: #20242A; --donut-value: #20242A;
--donut-label: #555B65; --donut-label: #555B65;
--donut-all: #1C70C9; --donut-all: #1C70C9;
@ -389,8 +435,8 @@
--donut-not-applicable: #6D2CE8; --donut-not-applicable: #6D2CE8;
--donut-not-run: #626873; --donut-not-run: #626873;
--donut-error: #D42836; --donut-error: #D42836;
--donut-rack-shadow: 0 8px 22px rgba(23, 25, 29, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.70); --donut-rack-shadow: none;
--donut-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86), 0 2px 7px rgba(23, 25, 29, 0.12); --donut-card-shadow: var(--tile-card-shadow);
--surface-app: #F1F1F4; --surface-app: #F1F1F4;
--surface-shell: #E4E4EA; --surface-shell: #E4E4EA;
--surface-panel: #FFFFFF; --surface-panel: #FFFFFF;

View File

@ -14,5 +14,7 @@ describe('ASSESSOR version in the brand header', () => {
expect(app).not.toContain('>v{CURRENT_ASSESSOR_PROCESSING_VERSION}'); expect(app).not.toContain('>v{CURRENT_ASSESSOR_PROCESSING_VERSION}');
expect(layout).toMatch(/\.brand-copy\s*\{[^}]*display:\s*grid;[^}]*gap:\s*var\(--space-1\);/s); expect(layout).toMatch(/\.brand-copy\s*\{[^}]*display:\s*grid;[^}]*gap:\s*var\(--space-1\);/s);
expect(layout).toMatch(/\.brand-version\s*\{[^}]*font-size:\s*var\(--fs-micro\);[^}]*font-variant-numeric:\s*tabular-nums;/s); expect(layout).toMatch(/\.brand-version\s*\{[^}]*font-size:\s*var\(--fs-micro\);[^}]*font-variant-numeric:\s*tabular-nums;/s);
expect(layout).toMatch(/\.brand\s*\{[^}]*min-height:\s*112px;[^}]*padding:\s*6px 14px 6px 16px;[^}]*margin-top:\s*0;/s);
expect(layout).not.toMatch(/\.app-shell[^,{]* \.brand\s*\{/s);
}); });
}); });

View File

@ -0,0 +1,38 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const main = readFileSync(new URL('../../src/main.tsx', import.meta.url), 'utf8');
const tokens = readFileSync(new URL('../../src/styles/ui-tokens.css', import.meta.url), 'utf8');
const base = readFileSync(new URL('../../src/styles/base.css', import.meta.url), 'utf8');
const datahub = readFileSync(new URL('../../src/styles/datahub.css', import.meta.url), 'utf8');
describe('binding ASSESSOR typography system', () => {
it('bundles one font family and exposes the approved semantic hierarchy', () => {
expect(main).toContain("import '@fontsource-variable/inter/wght.css';");
expect(tokens).toContain('--font-ui: "Inter Variable", sans-serif;');
expect(tokens).toContain('--font-mono: var(--font-ui);');
expect(tokens).toContain('--font-donut-display: var(--font-ui);');
expect(tokens).toContain('--type-h1-size: 56px;');
expect(tokens).toContain('--type-h2-size: 32px;');
expect(tokens).toContain('--type-h3-size: 18px;');
expect(tokens).toContain('--type-h4-size: 16px;');
expect(tokens).toContain('--type-body-size: 16px;');
expect(tokens).toContain('--type-label-size: 14px;');
expect(tokens).toContain('--type-meta-size: 13px;');
expect(tokens).toContain('--type-badge-size: 12px;');
expect(tokens).toContain('--type-table-head-size: 13px;');
expect(tokens).toContain('--type-button-size: 14px;');
expect(tokens).toContain('--fw-regular: 400;');
expect(tokens).toContain('--fw-semibold: 600;');
expect(tokens).toContain('--fw-bold: 700;');
});
it('uses title case for semantic headings and labels while reserving uppercase for table heads', () => {
expect(base).toMatch(/h1\s*\{[^}]*font-size:\s*var\(--type-h1-size\);[^}]*font-weight:\s*var\(--fw-semibold\);/s);
expect(base).toMatch(/td\s*\{[^}]*font-size:\s*var\(--type-body-size\);/s);
expect(base).toMatch(/th\s*\{[^}]*font-size:\s*var\(--type-table-head-size\);[^}]*text-transform:\s*uppercase;/s);
expect(datahub).toMatch(/\.tenant-timeslots-heading\s*\{[^}]*font-size:\s*var\(--type-h3-size\);[^}]*text-transform:\s*none;/s);
expect(datahub).toMatch(/\.tenant-information-title > strong\s*\{[^}]*font-size:\s*var\(--type-h3-size\);[^}]*text-transform:\s*none;/s);
expect(datahub).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-label\s*\{[^}]*font-size:\s*var\(--type-label-size\);[^}]*text-transform:\s*none;/s);
});
});

View File

@ -4,25 +4,39 @@ import { describe, expect, it } from 'vitest';
import { DonutMetricCard } from '../../src/components/DonutMetricCard/DonutMetricCard'; import { DonutMetricCard } from '../../src/components/DonutMetricCard/DonutMetricCard';
const styles = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8'); const styles = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
const tileContract = readFileSync(new URL('../../src/styles/tile-contract.css', import.meta.url), 'utf8');
const tokens = readFileSync(new URL('../../src/styles/ui-tokens.css', import.meta.url), 'utf8'); const tokens = readFileSync(new URL('../../src/styles/ui-tokens.css', import.meta.url), 'utf8');
describe('DonutMetricCard binding design contract', () => { describe('DonutMetricCard binding design contract', () => {
it('renders the reference card structure, 270-degree progress and visible values', () => { it('renders the shared full-circle reference with centered percentage and supporting count', () => {
const html = renderToStaticMarkup( const html = renderToStaticMarkup(
<DonutMetricCard label="Deviation" value={453} percentage={51} tone="deviation" /> <DonutMetricCard label="Deviation" value={453} percentage={51} tone="deviation" />
); );
expect(html).toContain('class="donut-metric-card donut-metric-card--deviation"'); expect(html).toContain('class="donut-metric-card donut-metric-card--deviation"');
expect(html).toContain('class="donut-metric-card__title">Deviation</h3>'); expect(html).toContain('class="donut-metric-card__title">Deviation</h3>');
expect(html).toContain('stroke-dasharray="75 25"'); expect(html).toContain('class="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100"');
expect(html).toContain('stroke-dasharray="38.25 61.75"'); expect(html).toContain('stroke-dasharray="51 49"');
expect(html).toContain('<strong>453</strong>'); expect(html).toContain('<div class="donut-metric-card__primary"><strong>51</strong><span>%</span></div>');
expect(html).toContain('<span>51%</span>'); expect(html).toContain('<small>453</small>');
expect(html).toContain('--donut-progress-dash:38.25 61.75'); expect(html).toContain('--donut-progress-dash:51 49');
expect(styles).toContain('transform: rotate(-90deg);');
expect(styles).toContain('left: calc(100% + var(--space-1));');
expect(styles).toMatch(/\.donut-metric-card__primary\s*\{[^}]*position:\s*relative;/s);
expect(styles).not.toContain('stroke-linecap: round;');
expect(styles).toContain('@keyframes donut-progress-build'); expect(styles).toContain('@keyframes donut-progress-build');
expect(styles).toContain('@keyframes donut-accent-build'); expect(styles).toContain('@keyframes donut-accent-build');
expect(styles).toContain('animation: donut-progress-build var(--dur-data-build) var(--ease-data-build) both;'); expect(styles).toContain('animation: donut-progress-build var(--dur-data-build) var(--ease-data-build) both;');
expect(styles).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(6, minmax\(0, 1fr\)\);/s);
expect(tileContract).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s);
expect(styles).toMatch(/@media \(max-width: 1100px\)\s*\{[^}]*repeat\(3, minmax\(0, 1fr\)\)/s);
expect(styles).toMatch(/@media \(max-width: 760px\)\s*\{[^}]*repeat\(2, minmax\(0, 1fr\)\)/s);
expect(tokens).toContain('--dur-data-build: 700ms;'); expect(tokens).toContain('--dur-data-build: 700ms;');
expect(tokens).toContain('--donut-value-size: 26px;');
expect(tokens).toContain('--donut-percent-size: 14px;');
expect(tokens).toContain('--donut-compact-value-size: 22px;');
expect(tokens).toContain('--donut-compact-percent-size: 12px;');
expect(styles).toMatch(/\.sidebar-severity-card \.donut-metric-card__primary span\s*\{[^}]*left:\s*calc\(100% \+ var\(--donut-compact-unit-gap\)\);/s);
}); });
it('renders an empty progress arc at zero and clamps percentages', () => { it('renders an empty progress arc at zero and clamps percentages', () => {
@ -34,9 +48,10 @@ describe('DonutMetricCard binding design contract', () => {
); );
expect(zero).toContain('stroke-dasharray="0 100"'); expect(zero).toContain('stroke-dasharray="0 100"');
expect(zero).toContain('<span>0%</span>'); expect(zero).toContain('<strong>0</strong><span>%</span>');
expect(clamped).toContain('stroke-dasharray="75 25"'); expect(clamped).toContain('stroke-dasharray="100 0"');
expect(clamped).toContain('<span>100%</span>'); expect(clamped).toContain('donut-metric-card__primary is-compact');
expect(clamped).toContain('<strong>100</strong><span>%</span>');
expect(zero).not.toContain('<animate'); expect(zero).not.toContain('<animate');
}); });
@ -46,7 +61,7 @@ describe('DonutMetricCard binding design contract', () => {
); );
expect(html).toContain('<strong>36%</strong>'); expect(html).toContain('<strong>36%</strong>');
expect(html).not.toContain('<span>36%</span>'); expect(html).not.toContain('<span>%</span>');
}); });
it('becomes an accessible active filter control when clickable', () => { it('becomes an accessible active filter control when clickable', () => {

View File

@ -85,17 +85,16 @@ const prohibitions = [
label: 'einen Schatten ausserhalb der Overlay-Rollen', label: 'einen Schatten ausserhalb der Overlay-Rollen',
/* `inset` bleibt ausgenommen: eingesenkte Wertfelder sind Flaechen- /* `inset` bleibt ausgenommen: eingesenkte Wertfelder sind Flaechen-
* modellierung, kein schwebendes Element. */ * modellierung, kein schwebendes Element. */
/* Der bindende Donut-Vertrag vom 2026-08-05 traegt zwei eigene, /* Der bindende Segment-/Kachelvertrag erlaubt genau den gemeinsamen,
* tokenisierte Tiefenschatten. Sie sind Teil der Nutzerreferenz und * dezenten Kachelschatten. Segmente selbst bleiben flach. */
* ausschliesslich an DonutMetricCard/Rack gebunden. */ matches: line => line.includes('box-shadow') && !/box-shadow:\s*none|shadow-overlay|shadow-popover|inset|var\(--(?:donut-(?:rack|card)-shadow|tile-card-shadow)\)/.test(line),
matches: line => line.includes('box-shadow') && !/shadow-overlay|shadow-popover|inset|var\(--donut-(?:rack|card)-shadow\)/.test(line), remedy: 'Nur echte Overlays und Kacheln verwenden ihre gemeinsamen Schattentokens; Segmente und Panels grenzen ueber Haarlinien ab.',
remedy: 'Schatten tragen nur echte Overlays (--shadow-overlay, --shadow-popover). Panels grenzen ueber Haarlinien ab.',
budgets: new Map([ budgets: new Map([
['components.css', 6], ['components.css', 2],
['datahub.css', 2], ['datahub.css', 2],
['import-workflow.css', 1], ['import-workflow.css', 1],
['layout.css', 7], ['layout.css', 1],
['legacy-views.css', 6] ['legacy-views.css', 0]
]) ])
}, },
{ {
@ -310,13 +309,18 @@ assert.match(searchFilterControls, /placeholder="Search"/, 'Das Freitextfeld mus
assert.doesNotMatch(searchFilterControls, /Filter text/, 'Die alte Bezeichnung Filter text darf nicht sichtbar bleiben.'); assert.doesNotMatch(searchFilterControls, /Filter text/, 'Die alte Bezeichnung Filter text darf nicht sichtbar bleiben.');
assert.match( assert.match(
shellLayout, shellLayout,
/\.area-topbar strong\s*\{[^}]*font-size:\s*calc\(var\(--fs-xl\) \* 4\);[^}]*font-weight:\s*var\(--fw-semibold\);[^}]*line-height:\s*1\.15;[^}]*letter-spacing:\s*-\.04em;/, /\.area-topbar strong\s*\{[^}]*font-size:\s*var\(--type-h1-size\);[^}]*font-weight:\s*var\(--fw-semibold\);[^}]*line-height:\s*1\.1;[^}]*letter-spacing:\s*-\.04em;/,
'layout.css muss die Bereichsbezeichnungen aller regulaeren Kopfpanels wie den Mandantentitel darstellen.' 'layout.css muss die Bereichsbezeichnungen aller regulaeren Kopfpanels wie den Mandantentitel darstellen.'
); );
assert.match( assert.match(
shellLayout, shellLayout,
/\.app-shell:not\(\.app-shell-work\) \.brand\s*\{[^}]*min-height:\s*112px;[^}]*padding-block:\s*6px;[^}]*margin-top:\s*0;/, /\.brand\s*\{[^}]*min-height:\s*112px;[^}]*padding:\s*6px 14px 6px 16px;[^}]*margin-top:\s*0;/,
'layout.css muss die Marke mittig zur horizontalen Achse der grossen Bereichsbezeichnung ausrichten.' 'layout.css muss die Marke in allen Ansichten fest auf der Datahub-Position halten.'
);
assert.doesNotMatch(
shellLayout,
/\.app-shell[^,{]* \.brand\s*\{/,
'Ansichtsspezifische Brand-Geometrie ist nicht erlaubt, damit Logo und Schrift beim Ansichtswechsel nicht springen.'
); );
assert.doesNotMatch( assert.doesNotMatch(
shellLayout, shellLayout,

View File

@ -18,9 +18,13 @@ describe('dashboard reference layout', () => {
expect(component).toContain('<span>Error</span>'); expect(component).toContain('<span>Error</span>');
expect(component).toContain('className="dashboard-ioe-summary" type="button"'); expect(component).toContain('className="dashboard-ioe-summary" type="button"');
expect(component).toContain('setPathFindingIds(explicitIoeRecordIds)'); expect(component).toContain('setPathFindingIds(explicitIoeRecordIds)');
expect(styles).toMatch(/\.dashboard-status-rack\s*\{[^}]*repeat\(3,/s); expect(styles).toMatch(/\.dashboard-status-rack\s*\{[^}]*grid-template-columns:\s*290px repeat\(3, 210px\) 250px;/s);
expect(styles).toContain('.dashboard-status-ring-track { stroke: var(--donut-border); }'); expect(styles).toMatch(/\.dashboard-status-copy > strong\s*\{[^}]*display:\s*grid;[^}]*white-space:\s*normal;/s);
expect(styles).toContain('button.dashboard-status-metric:hover { border-color: currentColor; }'); expect(styles).toMatch(/\.dashboard-status-copy > strong small, \.dashboard-status-copy > small\s*\{[^}]*display:\s*block;/s);
expect(styles).toContain('grid-template-columns: repeat(2, minmax(0, 290px));');
expect(component).toContain("{value.toLocaleString('en-US')}<small>{roundedPercentage}%</small>");
expect(styles).toContain('.dashboard-status-ring-track { stroke: var(--donut-track); }');
expect(styles).toContain('button.dashboard-status-metric:hover { border-color: currentColor; background: var(--tile-surface-hover); }');
}); });
it('uses paired segmented distributions and a compact lower dashboard row', () => { it('uses paired segmented distributions and a compact lower dashboard row', () => {
@ -39,6 +43,10 @@ describe('dashboard reference layout', () => {
expect(component).toContain('className="dashboard-risk-tool"'); expect(component).toContain('className="dashboard-risk-tool"');
expect(component).toContain('<option value={fullRiskProfileId}>Full Risk Profile</option>'); expect(component).toContain('<option value={fullRiskProfileId}>Full Risk Profile</option>');
expect(component).toContain('className="dashboard-risk-donut"'); expect(component).toContain('className="dashboard-risk-donut"');
expect(component).toContain('<span className="dashboard-donut-primary is-compact"><strong>100</strong><small>%</small></span>');
expect(styles).toContain('.dashboard-risk-donut-track { stroke: var(--donut-track); }');
expect(styles).toMatch(/\.dashboard-donut-primary\s*\{[^}]*position:\s*relative;/s);
expect(styles).toMatch(/\.dashboard-status-ring > span small\s*\{[^}]*left:\s*calc\(100% \+ var\(--space-1\)\);/s);
expect(component).toContain('categories.slice(0, 4)'); expect(component).toContain('categories.slice(0, 4)');
expect(component).toContain('matchesDynamicFacets(record, [sourceFacet]'); expect(component).toContain('matchesDynamicFacets(record, [sourceFacet]');
expect(component).not.toContain('dashboard-review-block'); expect(component).not.toContain('dashboard-review-block');

View File

@ -16,7 +16,7 @@ const baselines = readFileSync(new URL('../../src/features/project-import/Micros
describe('Datahub centered reference rack layout', () => { describe('Datahub centered reference rack layout', () => {
it('uses the binding six-card donut design for the selected timeslot', () => { it('uses the binding six-card donut design for the selected timeslot', () => {
expect(datahubStyles).toContain('.datahub-page > .datahub-rack-module { width: min(1180px, 100%); margin-inline: auto; }'); expect(datahubStyles).toContain('.datahub-page > .datahub-rack-module { width: min(1180px, 100%); margin-inline: auto; }');
expect(datahubStyles).toContain('.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));'); expect(datahubStyles).toContain('.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start;');
expect(datahubStyles).toContain('.datahub-tenant-card.is-selected'); expect(datahubStyles).toContain('.datahub-tenant-card.is-selected');
expect(datahubStyles).toContain('.datahub-tenant-processing-dot { position: absolute; z-index: 3; top: var(--datahub-readiness-dot-inset); left: var(--datahub-readiness-dot-inset);'); expect(datahubStyles).toContain('.datahub-tenant-processing-dot { position: absolute; z-index: 3; top: var(--datahub-readiness-dot-inset); left: var(--datahub-readiness-dot-inset);');
expect(datahubStyles).toContain('background: var(--state-online); pointer-events: none;'); expect(datahubStyles).toContain('background: var(--state-online); pointer-events: none;');
@ -24,10 +24,13 @@ describe('Datahub centered reference rack layout', () => {
expect(datahubStyles).toContain('.datahub-tenant-processing-dot[data-status="error"] { background: var(--state-offline); }'); expect(datahubStyles).toContain('.datahub-tenant-processing-dot[data-status="error"] { background: var(--state-offline); }');
expect(datahubStyles).toContain('.datahub-tenant-show[data-status="update"]'); expect(datahubStyles).toContain('.datahub-tenant-show[data-status="update"]');
expect(datahubStyles).toContain('border-color: var(--warning); color: var(--warning);'); expect(datahubStyles).toContain('border-color: var(--warning); color: var(--warning);');
expect(datahubStyles).toContain('outline: var(--bw-emphasis) solid var(--button-marked-bg);'); expect(datahubStyles).toContain('outline: var(--bw-emphasis) solid var(--tile-selected-border);');
expect(datahubStyles).toContain('.datahub-tenant-card::before { content: none; }'); expect(datahubStyles).toContain('.datahub-tenant-card::before { content: none; }');
expect(datahubStyles).toContain('transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .12s ease;'); expect(datahubStyles).toContain('transition: background .12s ease, border-color .12s ease;');
expect(datahubStyles).toMatch(/\.datahub-tenant-card:hover,\s*\.datahub-tenant-card:focus-within\s*\{[^}]*border-color:\s*var\(--line-strong\);[^}]*background:\s*var\(--panel-hover\);[^}]*box-shadow:\s*var\(--shadow-raised-hover\);[^}]*transform:\s*translateY\(-2px\);/s); expect(datahubStyles).toMatch(/\.datahub-tenant-card:hover,\s*\.datahub-tenant-card:focus-within\s*\{[^}]*border-color:\s*var\(--line-strong\);[^}]*background:\s*var\(--tile-surface-hover\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
expect(datahubStyles).not.toMatch(/\.datahub-tenant-card\s*\{[^}]*box-shadow/s);
expect(datahubStyles).toContain('box-shadow: var(--tile-card-shadow);');
expect(datahubStyles).not.toContain('translateY(-2px)');
expect(datahubStyles).toContain('.datahub-tenant-select { position: absolute; inset: 0;'); expect(datahubStyles).toContain('.datahub-tenant-select { position: absolute; inset: 0;');
expect(datahubStyles).toContain('grid-template-rows: minmax(0, 1fr) auto auto;'); expect(datahubStyles).toContain('grid-template-rows: minmax(0, 1fr) auto auto;');
expect(datahubStyles).toContain('font: var(--fw-semibold) calc(var(--donut-title-size) * 1.35)'); expect(datahubStyles).toContain('font: var(--fw-semibold) calc(var(--donut-title-size) * 1.35)');
@ -44,12 +47,13 @@ describe('Datahub centered reference rack layout', () => {
expect(componentStyles).toContain('Verbindlicher Donut-Standard: Referenz vom 2026-08-05.'); expect(componentStyles).toContain('Verbindlicher Donut-Standard: Referenz vom 2026-08-05.');
expect(componentStyles).toContain('grid-template-columns: repeat(6, minmax(0, 1fr));'); expect(componentStyles).toContain('grid-template-columns: repeat(6, minmax(0, 1fr));');
expect(componentStyles).toContain('padding: var(--space-3);'); expect(componentStyles).toContain('padding: var(--segment-inset);');
expect(componentStyles).toContain('.donut-metric-card::before'); expect(componentStyles).toContain('.donut-metric-card::before');
expect(componentStyles).toContain('border-radius: var(--donut-card-radius);'); expect(componentStyles).toContain('border-radius: var(--tile-radius);');
expect(componentStyles).toContain('background: linear-gradient(145deg, var(--donut-card-surface), var(--donut-card-surface-end));'); expect(componentStyles).toContain('background: var(--tile-surface);');
expect(componentStyles).toContain('.donut-metric-card__progress'); expect(componentStyles).toContain('.donut-metric-card__progress');
expect(componentStyles).toContain('stroke-linecap: round;'); expect(componentStyles).not.toContain('stroke-linecap: round;');
expect(componentStyles).toContain('transform: rotate(-90deg);');
expect(componentStyles).toContain('.donut-metric-card--all { color: var(--donut-all); }'); expect(componentStyles).toContain('.donut-metric-card--all { color: var(--donut-all); }');
expect(componentStyles).toContain('.donut-metric-card--deviation { color: var(--donut-deviation); }'); expect(componentStyles).toContain('.donut-metric-card--deviation { color: var(--donut-deviation); }');
expect(componentStyles).toContain('.donut-metric-card--passed { color: var(--donut-passed); }'); expect(componentStyles).toContain('.donut-metric-card--passed { color: var(--donut-passed); }');
@ -57,16 +61,19 @@ describe('Datahub centered reference rack layout', () => {
expect(componentStyles).toContain('.donut-metric-card--not-run { color: var(--donut-not-run); }'); expect(componentStyles).toContain('.donut-metric-card--not-run { color: var(--donut-not-run); }');
expect(componentStyles).toContain('.donut-metric-card--error { color: var(--donut-error); }'); expect(componentStyles).toContain('.donut-metric-card--error { color: var(--donut-error); }');
expect(tokens).toContain('--font-donut-display: "Bahnschrift Condensed"'); expect(tokens).toContain('--font-ui: "Inter Variable", sans-serif;');
expect(tokens).toContain('--font-donut-display: var(--font-ui);');
expect(tokens).toContain('--donut-rack-surface:'); expect(tokens).toContain('--donut-rack-surface:');
expect(tokens).toContain('--donut-ring-size: 128px;'); expect(tokens).toContain('--donut-ring-size: 128px;');
expect(tokens).toContain('--donut-ring-stroke: 12px;');
expect(tokens).toContain('--datahub-readiness-dot-size: 14px;'); expect(tokens).toContain('--datahub-readiness-dot-size: 14px;');
expect(tokens).toContain('--datahub-readiness-dot-inset: var(--space-1);'); expect(tokens).toContain('--datahub-readiness-dot-inset: var(--space-1);');
expect(tokens).toContain('Donut-Standard: identische Geometrie, helle Referenzpalette.'); expect(tokens).toContain('Donut-Standard: identische Geometrie, helle Referenzpalette.');
expect(tokens).toContain('--donut-rack-surface: #E7E9ED;'); expect(tokens).toContain('--segment-surface: #E7E9ED;');
expect(tokens).toContain('--donut-rack-surface: var(--segment-surface);');
expect(tokens).toContain('--donut-deviation: #9A6500;'); expect(tokens).toContain('--donut-deviation: #9A6500;');
expect(donut).toContain('strokeDasharray="75 25"'); expect(donut).toContain('<circle className="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100" />');
expect(donut).toContain('const arcLength = boundedPercentage * 0.75;'); expect(donut).toContain("const dashArray = String(boundedPercentage) + ' ' + String(100 - boundedPercentage);");
expect(donut).toContain("const progressStyle = { '--donut-progress-dash': dashArray } as CSSProperties;"); expect(donut).toContain("const progressStyle = { '--donut-progress-dash': dashArray } as CSSProperties;");
expect(donut).toContain('key={label + \'-\' + value + \'-\' + boundedPercentage}'); expect(donut).toContain('key={label + \'-\' + value + \'-\' + boundedPercentage}');
expect(donut).not.toContain('<animate'); expect(donut).not.toContain('<animate');

View File

@ -69,7 +69,7 @@ describe('Mandant detail reference', () => {
expect(dataHub).toContain('className="tenant-field-value tenant-field-prose"'); expect(dataHub).toContain('className="tenant-field-value tenant-field-prose"');
expect(dataHub).toContain('<span className="tenant-field-label">Name'); expect(dataHub).toContain('<span className="tenant-field-label">Name');
expect(dataHub).not.toContain('tenant-identity-name-input'); expect(dataHub).not.toContain('tenant-identity-name-input');
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-value\s*\{[^}]*justify-self:\s*start;[^}]*font-size:\s*var\(--fs-lg\);[^}]*text-align:\s*left;/s); expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-value\s*\{[^}]*justify-self:\s*start;[^}]*font-size:\s*var\(--type-body-size\);[^}]*text-align:\s*left;/s);
expect(dataHub).toContain('const [excludedScanRunIds, setExcludedScanRunIds]'); expect(dataHub).toContain('const [excludedScanRunIds, setExcludedScanRunIds]');
expect(dataHub).toMatch(/const openEdit = \(project: AssessmentProject\) => \{[\s\S]*?setSelectedTimeslotIds\(latestTimeslot \? \[latestTimeslot\.id\] : \[\]\);[\s\S]*?setExpandedTimeslotIds\(affectedTimeslotIds\);/); expect(dataHub).toMatch(/const openEdit = \(project: AssessmentProject\) => \{[\s\S]*?setSelectedTimeslotIds\(latestTimeslot \? \[latestTimeslot\.id\] : \[\]\);[\s\S]*?setExpandedTimeslotIds\(affectedTimeslotIds\);/);
expect(dataHub).toContain('const toggleScanReport ='); expect(dataHub).toContain('const toggleScanReport =');
@ -81,9 +81,9 @@ describe('Mandant detail reference', () => {
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);/s); expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);/s);
expect(styles).toMatch(/accent-color:\s*var\(--state-active\);/s); expect(styles).toMatch(/accent-color:\s*var\(--state-active\);/s);
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form label\s*\{[^}]*display:\s*block;[^}]*border-bottom:/s); expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form label\s*\{[^}]*display:\s*block;[^}]*border-bottom:/s);
expect(styles).toMatch(/\.tenant-field-label\s*\{[^}]*background:\s*transparent;[^}]*text-transform:\s*uppercase;/s); expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-label\s*\{[^}]*background:\s*transparent;[^}]*font-size:\s*var\(--type-label-size\);[^}]*text-transform:\s*none;/s);
expect(styles).toMatch(/\.tenant-report-copy b\s*\{[^}]*font-size:\s*var\(--fs-lg\);/s); expect(styles).toMatch(/\.tenant-report-copy b\s*\{[^}]*font-size:\s*var\(--fs-lg\);/s);
expect(styles).toMatch(/\.tenant-report-copy small\s*\{[^}]*font-size:\s*var\(--fs-sm\);/s); expect(styles).toMatch(/\.tenant-report-copy small\s*\{[^}]*font-size:\s*var\(--type-meta-size\);/s);
expect(styles).toMatch(/\.tenant-report-row\s*\{[^}]*grid-template-columns:[^;}]*calc\(var\(--space-8\) \* 4\.25\)/s); expect(styles).toMatch(/\.tenant-report-row\s*\{[^}]*grid-template-columns:[^;}]*calc\(var\(--space-8\) \* 4\.25\)/s);
expect(dataHub).toContain('rows={10} placeholder="Notes about the client or project"'); expect(dataHub).toContain('rows={10} placeholder="Notes about the client or project"');
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form textarea\.input\s*\{[^}]*width:\s*100%;/s); expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form textarea\.input\s*\{[^}]*width:\s*100%;/s);

View File

@ -0,0 +1,28 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const components = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
const legacyViews = readFileSync(new URL('../../src/styles/legacy-views.css', import.meta.url), 'utf8');
const tokens = readFileSync(new URL('../../src/styles/tokens.css', import.meta.url), 'utf8');
describe('Finding title typography', () => {
it('uses the emphasized 20px detail-title role in the reader', () => {
expect(components).toMatch(/\.reader-heading h2\s*\{[^}]*font-size:\s*var\(--type-detail-title-size\);/s);
expect(components).toMatch(/\.reader-summary h3\s*\{[^}]*font-size:\s*var\(--type-h3-size\);/s);
expect(legacyViews).toMatch(/\.reader-heading h2\s*\{[^}]*font-size:\s*var\(--type-detail-title-size\);/s);
expect(legacyViews).toMatch(/\.reader-summary h3\s*\{[^}]*font-size:\s*var\(--type-h3-size\);/s);
});
it('uses the readable 16px content-title role in list and matrix cards', () => {
expect(components).toMatch(/\.record-list \.finding-card h3\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
expect(components).toMatch(/\.record-list \.finding-card h3\s*\{[^}]*font-weight:\s*var\(--fw-content-medium\);/s);
expect(components).toMatch(/\.finding-card h3\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
expect(legacyViews).toMatch(/\.data-tile-content h3\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
});
it('uses the compact badge role for finding labels and outcomes', () => {
expect(tokens).toContain('--ui-badge-size: var(--type-badge-size);');
expect(legacyViews).toMatch(/\.finding-badges, \.mitre-line\s*\{[^}]*font-size:\s*var\(--ui-badge-size\);/s);
expect(legacyViews).toMatch(/\.status-badge, \.severity-badge, \.mitre-badge, \.reader-severity\s*\{[^}]*font-size:\s*var\(--ui-badge-size\);/s);
});
});

View File

@ -224,8 +224,8 @@ describe('Obsidian-style interactive graph', () => {
expect(graphStyles).toMatch(/\.obsidian-graph-card\.is-selected \.obsidian-node-card,[\s\S]*?\.obsidian-graph-card\.is-selected \.obsidian-node-matrix-card \.data-tile\s*\{/s); expect(graphStyles).toMatch(/\.obsidian-graph-card\.is-selected \.obsidian-node-card,[\s\S]*?\.obsidian-graph-card\.is-selected \.obsidian-node-matrix-card \.data-tile\s*\{/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;/s); expect(graphStyles).toMatch(/\.obsidian-node-matrix-card\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;/s);
expect(graphStyles).not.toContain('--graph-card-content-scale'); expect(graphStyles).not.toContain('--graph-card-content-scale');
expect(graphStyles).toMatch(/\.obsidian-node-card\.data-tile:hover,[\s\S]*?\{[^}]*box-shadow:\s*none;[^}]*transform:\s*none;/); expect(graphStyles).not.toContain('box-shadow');
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile:hover,[\s\S]*?\{[^}]*box-shadow:\s*none;[^}]*transform:\s*none;/); expect(graphStyles).not.toContain('translateY(-2px)');
expect(graphStyles).toMatch(/\.obsidian-node-card \.data-tile-content h3\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-title-size\);/s); expect(graphStyles).toMatch(/\.obsidian-node-card \.data-tile-content h3\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-title-size\);/s);
expect(graphStyles).toMatch(/\.obsidian-node-inline-label\s*\{[^}]*font-size:\s*var\(--fs-lg\);/s); expect(graphStyles).toMatch(/\.obsidian-node-inline-label\s*\{[^}]*font-size:\s*var\(--fs-lg\);/s);
expect(graphStyles).toMatch(/\.obsidian-node-card \.reader-severity\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-relation-badge-size\);/s); expect(graphStyles).toMatch(/\.obsidian-node-card \.reader-severity\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-relation-badge-size\);/s);

View File

@ -17,7 +17,7 @@ describe('rounded panel content inset standard', () => {
}); });
it('uses the shared inset in every Work presentation including responsive tiles', () => { it('uses the shared inset in every Work presentation including responsive tiles', () => {
expect(layout).toMatch(/\.tile-grid\s*\{[^}]*padding:\s*var\(--panel-inset\);/s); expect(layout).toMatch(/\.tile-view-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
expect(layout).toMatch(/\.raw-view\s*\{[^}]*padding:\s*var\(--panel-inset\);/s); expect(layout).toMatch(/\.raw-view\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
expect(legacy).toMatch(/\.records-layout\s*\{[^}]*padding:\s*var\(--panel-inset\);/s); expect(legacy).toMatch(/\.records-layout\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
expect(legacy).toMatch(/\.table-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s); expect(legacy).toMatch(/\.table-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);

View File

@ -0,0 +1,75 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const read = (path: string) => readFileSync(new URL(path, import.meta.url), 'utf8');
const tokens = read('../../src/styles/ui-tokens.css');
const components = read('../../src/styles/components.css');
const tileContract = read('../../src/styles/tile-contract.css');
const datahub = read('../../src/styles/datahub.css');
const datahubRedesign = read('../../src/styles/datahub-redesign.css');
const legacy = read('../../src/styles/legacy-views.css');
const layout = read('../../src/styles/layout.css');
const uiComponents = read('../../src/styles/ui-components.css');
const policy = read('../../src/styles/policy-analysis.css');
const pathfinder = read('../../src/styles/mitre-graph.css');
const viewer = read('../../src/features/dataset-viewer/DatasetViewer.tsx');
const overview = read('../../src/features/project-import/DatahubHistoryOverview.tsx');
const guidance = read('../../AGENTS.md');
describe('binding segment and tile grammar', () => {
it('defines one semantic geometry and surface contract for both themes', () => {
for (const token of [
'--segment-surface:', '--segment-border:', '--segment-radius: 10px;',
'--segment-inset: var(--space-3);', '--segment-gap: var(--space-2);',
'--tile-surface:', '--tile-surface-hover:', '--tile-border:',
'--tile-selected-border:', '--tile-radius: 8px;',
'--tile-compact-width: 186px;', '--tile-content-width: 320px;',
'--tile-card-shadow:'
]) expect(tokens).toContain(token);
expect(tokens).toContain('--donut-rack-surface: var(--segment-surface);');
expect(tokens).toContain('--donut-card-surface: var(--tile-surface);');
expect(tokens).toContain('--donut-card-shadow: var(--tile-card-shadow);');
});
it('binds the shared grammar to Datahub, dashboard, sidebar, Work, policy and Pathfinder tiles', () => {
expect(components).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(6, minmax\(0, 1fr\)\);[^}]*padding:\s*var\(--segment-inset\);[^}]*border-radius:\s*var\(--segment-radius\);/s);
expect(tileContract).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s);
expect(components).toMatch(/\.donut-metric-card\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*background:\s*var\(--tile-surface\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
expect(components).toMatch(/\.sidebar-severity-rack\s*\{[^}]*gap:\s*var\(--segment-gap\);[^}]*background:\s*var\(--segment-surface\);/s);
expect(datahubRedesign).toMatch(/\.datahub-tenant-card-rack\s*\{[^}]*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s);
expect(datahubRedesign).toMatch(/\.datahub-tenant-card\.is-selected\s*\{[^}]*border-color:\s*var\(--tile-selected-border\);/s);
expect(overview).toContain('donut-metric-card donut-metric-card--all datahub-tenant-card datahub-tenant-add');
expect(datahub).toMatch(/\.dashboard-status-rack\s*\{[^}]*padding:\s*var\(--segment-inset\);[^}]*background:\s*var\(--segment-surface\);/s);
expect(datahub).toMatch(/\.dashboard-status-metric\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
expect(datahub).toMatch(/\.dashboard-distribution-grid\s*\{[^}]*border-radius:\s*var\(--segment-radius\);/s);
expect(datahub).toMatch(/\.db-structure-card\s*\{[^}]*background:\s*var\(--tile-surface\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
expect(layout).toMatch(/\.tile-view-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
expect(legacy).toMatch(/\.tile-grid\s*\{[^}]*justify-content:\s*start;[^}]*border-radius:\s*var\(--segment-radius\);/s);
expect(legacy).toMatch(/\.data-tile\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
expect(viewer).toContain('gridTemplateColumns: `repeat(${tileColumns}, minmax(var(--tile-content-width), 1fr))`');
expect(policy).toContain('grid-template-columns: repeat(auto-fill, var(--tile-compact-width));');
expect(pathfinder).toMatch(/\.finding-path-card\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
});
it('keeps the rules binding and prevents status-filled or stretched substitute tiles', () => {
expect(guidance).toContain('Treat segments and tiles as a binding two-level visual grammar independent of their content.');
expect(guidance).toContain('never stretch a partial final row merely to fill the container');
expect(guidance).toContain('Never fill the complete tile with a status color.');
expect(guidance).toContain('Tile content must never cross the tile boundary or cover a neighboring tile.');
expect(guidance).toContain('never split ordinary words or numeric values across lines');
expect(datahub).toMatch(/\.dashboard-status-metric\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
expect(datahub).toMatch(/\.dashboard-status-copy\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
expect(datahub).toMatch(/\.dashboard-status-copy\s*>\s*span\s*\{[^}]*overflow-wrap:\s*normal;[^}]*word-break:\s*normal;/s);
expect(datahub).toMatch(/\.dashboard-status-copy\s*>\s*strong\s*\{[^}]*display:\s*grid;[^}]*white-space:\s*normal;/s);
expect(legacy).toMatch(/\.data-tile\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
expect(uiComponents).toMatch(/\.tile\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
expect(datahubRedesign).not.toMatch(/\.datahub-tenant-card(?::hover|:focus-within|\s*\{)[^}]*translateY/s);
expect(legacy).not.toMatch(/\.data-tile(?::hover|:focus-visible|\s*\{)[^}]*translateY/s);
expect(datahub).not.toMatch(/\.dashboard-status-metric(?::hover|\s*\{)[^}]*translateY/s);
});
});

View File

@ -44,7 +44,7 @@ assert.doesNotMatch(indexHtml, /<(?:script|link)\b[^>]+(?:src|href)=["']https?:/
const packageJson = JSON.parse(read('package.json')); const packageJson = JSON.parse(read('package.json'));
assert.deepEqual( assert.deepEqual(
Object.keys(packageJson.dependencies).sort(), Object.keys(packageJson.dependencies).sort(),
['jsdom', 'pg', 'react', 'react-dom', 'saxes', 'xlsx'], ['@fontsource-variable/inter', 'jsdom', 'pg', 'react', 'react-dom', 'saxes', 'xlsx'],
'production dependencies changed; review same-origin and local-processing guarantees before accepting new packages' 'production dependencies changed; review same-origin and local-processing guarantees before accepting new packages'
); );

View File

@ -104,4 +104,27 @@ describe('Node import runtime', () => {
severity: 'info' severity: 'info'
})); }));
}, 30_000); }, 30_000);
it('routes a 1.6 MB PingCastle-style HTML report through bounded extraction before the worker time limit', async () => {
const html = '<!doctype html><html><head><title>PingCastle Healthcheck</title></head><body>'
+ '<!--' + 'x'.repeat(1_600_000) + '-->'
+ '<table id="rules"><caption>Rules</caption>'
+ '<tr><th>Rule ID</th><th>Title</th><th>Severity</th></tr>'
+ '<tr><td>PC-BOUNDED-1</td><td>Bounded synthetic report</td><td>High</td></tr>'
+ '</table></body></html>';
const bytes = new TextEncoder().encode(html);
const dataset = await importSource({
metadata: { name: 'ad_hc_bounded.synthetic.html', sizeBytes: bytes.length, lastModified: 0 }, bytes
}, { profiles: importProfiles }, nodeImportRuntimeAdapters);
expect(dataset.format).toBe('HTML');
expect(dataset.profileId).toBe('pingcastle');
expect(dataset.sheets.find(sheet => sheet.name === 'Rules')?.rows[1]).toEqual([
'PC-BOUNDED-1', 'Bounded synthetic report', 'High'
]);
expect(dataset.intermediate?.diagnostics).toContainEqual(expect.objectContaining({
code: 'html-bounded-extraction',
severity: 'info'
}));
}, 30_000);
}); });