Die Druckansicht ist die einzige Vorlage für den Export: kein zweites Layout von Hand in einer PDF-Bibliothek. Der Playwright-Dienst ruft genau diese Seite auf und bekommt dafür ein signiertes, kurzlebiges Zeichen statt eines Sitzungscookies. Es gilt nur, solange der Auftrag läuft, ist an den Anfragenden gebunden, und die Stufe wird beim Einlösen frisch aufgelöst und auf die im Zeichen vermerkte gedeckelt. Der Export darf nie sauberer aussehen als die Akte: gesperrte Felder erscheinen als Balken mit Vermerk statt zu fehlen, geänderte Einträge tragen ihre Version, zurückgezogene stehen als Grabstein. Wasserzeichen mit Name, Zeitstempel und Stufe auf jeder Seite. Die Stufe steckt im Zwischenspeicherschlüssel. Ohne sie bekäme ein Leser der Stufe 1 das PDF, das für Stufe 4 gerendert wurde. Ab Stufe 3 wartet der Auftrag auf Freigabe durch ein zweites Augenpaar — und der Anfragende darf sich nicht selbst freigeben, auch nicht mit dem Recht dazu. Offene Freigaben stehen auf der Übersicht. Dazu Einzeldokumente als PDF (Aktenblatt bleibt Aktenblatt), ZIP-Sammelexport mit eigenem achtzigzeiligem Schreiber statt einer Abhängigkeit, und der JSON-Vollabzug ohne Stufenfilterung — die einzige Stelle, an der die Klassifizierung nicht greift, deshalb hinter einem eigenen Admin-Endpunkt. Zwei Funde unterwegs, beide in docs/13 festgehalten: Die Druckansicht warf notFound(), und das Skelett aus dem Wurzel-loading.tsx blieb im Browser stehen — der PDF-Dienst hätte leere Blätter gedruckt. Gefunden nur, weil der e2e-Test auf den Inhalt prüft und nicht auf den Statuscode. Sie rendert die Auskunft jetzt selbst. Und die Entdopplung der Anhänge verwies auf Dateien, ohne nachzusehen, ob sie noch daliegen. Nach einem geleerten /tmp zeigte jeder weitere Upload desselben Inhalts stumm ins Leere. 178 Tests im Web-Paket, 17 in der Datenbank, 7 im Worker, 44 e2e. Fallakte bei 119,8 von 120 kB — das Budget ist aufgebraucht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
563 lines
13 KiB
CSS
563 lines
13 KiB
CSS
@import 'tailwindcss';
|
|
@import './fonts.css';
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
Design-Tokens. Fachliche Grundlage: docs/10-design-system.md.
|
|
Bezugspunkt ist eine Registratur im Nachtdienst: lackierter Stahl,
|
|
Aktenpapier, Stempelfarbe. Die einzige Farbe kommt von der Klassifizierung.
|
|
--------------------------------------------------------------------------- */
|
|
|
|
@theme {
|
|
/* Grundpalette, dunkel ist der Auslieferungszustand */
|
|
--color-ink-900: #15181a; /* Grundflaeche, kuehles Anthrazit, leicht gruenstichig */
|
|
--color-ink-800: #1d2124; /* Panel */
|
|
--color-ink-700: #262b2f; /* erhoehte Flaeche, Eingabefelder */
|
|
--color-ink-500: #3a4147; /* Linien und Rahmen */
|
|
--color-paper: #e4e7e4; /* Primaertext, papierfarbenes Off-White */
|
|
--color-paper-dim: #939c99; /* Sekundaertext */
|
|
--color-steel: #7a97a6; /* interaktiv: Links, Fokus, aktive Zustaende */
|
|
|
|
/* Klassifizierungsfarben. Das einzige Buntsystem, nirgends sonst benutzt. */
|
|
--color-stamp-0: #8a918d; /* Offen, Grau */
|
|
--color-stamp-1: #6d8a9c; /* Intern, Stahlblau */
|
|
--color-stamp-2: #4e8f72; /* Vertraulich, Gruenspan */
|
|
--color-stamp-3: #b08637; /* Geheim, Messing */
|
|
--color-stamp-4: #b4543c; /* Streng geheim, Oxidrot */
|
|
--color-stamp-5: #7e5e96; /* Leitungssache, Tiefviolett */
|
|
|
|
/* Semantische Ebene. Das helle Thema tauscht nur diese Werte. */
|
|
--color-surface: var(--color-ink-900);
|
|
--color-panel: var(--color-ink-800);
|
|
--color-raised: var(--color-ink-700);
|
|
--color-line: var(--color-ink-500);
|
|
--color-text: var(--color-paper);
|
|
--color-text-dim: var(--color-paper-dim);
|
|
--color-accent: var(--color-steel);
|
|
|
|
/* Schrift. IBM Plex, selbst gehostet, drei Rollen aus einer Familie. */
|
|
--font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif;
|
|
--font-condensed: 'IBM Plex Sans Condensed', 'IBM Plex Sans', ui-sans-serif, sans-serif;
|
|
--font-mono: 'IBM Plex Mono', ui-monospace, monospace;
|
|
|
|
/* Skala 12 / 13 / 15 / 18 / 24 px */
|
|
--text-2xs: 0.75rem; /* 12 */
|
|
--text-xs: 0.8125rem; /* 13 */
|
|
--text-sm: 0.9375rem; /* 15 */
|
|
--text-base: 0.9375rem; /* 15, Fliesstext */
|
|
--text-lg: 1.125rem; /* 18 */
|
|
--text-xl: 1.5rem; /* 24 */
|
|
|
|
--text-2xs--line-height: 1.4;
|
|
--text-xs--line-height: 1.45;
|
|
--text-sm--line-height: 1.6;
|
|
--text-base--line-height: 1.6;
|
|
--text-lg--line-height: 1.45;
|
|
--text-xl--line-height: 1.3;
|
|
|
|
/* Struktur: Eckenradius 2 px, ueberall gleich. Akten sind nicht abgerundet. */
|
|
--radius-none: 0px;
|
|
--radius-sm: 2px;
|
|
--radius-DEFAULT: 2px;
|
|
--radius-md: 2px;
|
|
--radius-lg: 2px;
|
|
--radius-xl: 2px;
|
|
|
|
/* Keine Schatten ausser dem Fokusring. */
|
|
--shadow-sm: none;
|
|
--shadow-DEFAULT: none;
|
|
--shadow-md: none;
|
|
--shadow-lg: none;
|
|
|
|
/* Zeilenlaenge hoechstens 72 Zeichen */
|
|
--container-measure: 72ch;
|
|
|
|
/* Die 3 px breite Klassifizierungskante links am Objekt */
|
|
--spacing-edge: 3px;
|
|
}
|
|
|
|
/* Helles Thema. Verpflichtend mitzuliefern, ausgebaut in Phase 9. */
|
|
[data-theme='light'] {
|
|
--color-surface: #eceeea;
|
|
--color-panel: #e2e5e1;
|
|
--color-raised: #f4f5f2;
|
|
--color-line: #b9beb8;
|
|
--color-text: #1b1f21;
|
|
--color-text-dim: #545c58;
|
|
--color-accent: #3d5f70;
|
|
}
|
|
|
|
@layer base {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
border-color: var(--color-line);
|
|
}
|
|
|
|
html {
|
|
color-scheme: dark;
|
|
background-color: var(--color-surface);
|
|
}
|
|
|
|
[data-theme='light'] {
|
|
color-scheme: light;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--color-surface);
|
|
color: var(--color-text);
|
|
font-family: var(--font-sans);
|
|
font-size: var(--text-base);
|
|
line-height: 1.6;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* Spaltenkoepfe, Stempel und Reiter tragen den schmalen Schnitt.
|
|
Kein Versalsatz: der Stempelcharakter kommt aus Schnitt, Rahmen und Farbe. */
|
|
th,
|
|
legend {
|
|
font-family: var(--font-condensed);
|
|
font-weight: 500;
|
|
text-transform: none;
|
|
}
|
|
|
|
/* Aktenzeichen, Zeitstempel, Hashes und Dienstnummern in Mono. */
|
|
code,
|
|
kbd,
|
|
samp,
|
|
time {
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
h3 {
|
|
font-weight: 500;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
h1 {
|
|
font-size: var(--text-xl);
|
|
}
|
|
h2 {
|
|
font-size: var(--text-lg);
|
|
}
|
|
h3 {
|
|
font-size: var(--text-base);
|
|
}
|
|
|
|
a {
|
|
color: var(--color-accent);
|
|
text-underline-offset: 2px;
|
|
}
|
|
|
|
/* Sichtbarer Fokusring in --steel, der einzige Schatten im System. */
|
|
:focus-visible {
|
|
outline: 2px solid var(--color-accent);
|
|
outline-offset: 2px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
::selection {
|
|
background-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
|
|
}
|
|
}
|
|
|
|
@layer components {
|
|
/* Der eine mutige Moment: die Schwaerzung. Ein massiver Balken mit feiner
|
|
Rahmenlinie, der beim ersten Erscheinen einmal von links nach rechts
|
|
durchwischt. Sonst bewegt sich in der Oberflaeche nichts von selbst. */
|
|
.redaction {
|
|
display: inline-block;
|
|
position: relative;
|
|
overflow: hidden;
|
|
vertical-align: baseline;
|
|
background-color: var(--color-ink-900);
|
|
border: 1px solid var(--color-line);
|
|
border-radius: 2px;
|
|
color: transparent;
|
|
user-select: none;
|
|
}
|
|
|
|
.redaction::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background-color: var(--color-ink-700);
|
|
transform-origin: left center;
|
|
animation: redaction-wipe 150ms ease-out 1 both;
|
|
}
|
|
|
|
@keyframes redaction-wipe {
|
|
from {
|
|
transform: scaleX(1);
|
|
}
|
|
to {
|
|
transform: scaleX(0);
|
|
}
|
|
}
|
|
|
|
/* Zeitachse des Verlaufs: durchgehende senkrechte Linie. Der Chat hat keine.
|
|
Der Unterschied muss ohne Beschriftung erkennbar sein. */
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.4375rem;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 1px;
|
|
background-color: var(--color-line);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Das Aktenblatt.
|
|
|
|
Bewusst **ausserhalb** des Design-Systems der Oberflaeche: schwarz auf
|
|
weiss, harte Rahmen, Massangaben in Millimetern. Es ist kein Bildschirm-
|
|
element, sondern ein Formular, das gedruckt und weitergereicht wird und
|
|
auf Papier genauso aussehen muss wie hier. Deshalb steht hier kein
|
|
einziger Farb-Token der Anwendung.
|
|
|
|
Nachgebaut nach dem Formular, das die Personalabteilung bislang von Hand
|
|
ausgefuellt hat. Siehe docs/13-entscheidungen.md.
|
|
--------------------------------------------------------------------- */
|
|
.sheet {
|
|
width: 210mm;
|
|
min-height: 297mm;
|
|
box-sizing: border-box;
|
|
margin: 0 auto;
|
|
padding: 10mm;
|
|
background: #ffffff;
|
|
color: #000000;
|
|
font-family: var(--font-sans, sans-serif);
|
|
}
|
|
|
|
.sheet-frame {
|
|
border: 4px solid #000;
|
|
min-height: calc(297mm - 20mm);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.sheet-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 15px;
|
|
border-bottom: 4px solid #000;
|
|
}
|
|
|
|
.sheet-head h1 {
|
|
margin: 0 0 5px;
|
|
font-size: 28px;
|
|
font-weight: 900;
|
|
letter-spacing: 1px;
|
|
line-height: 1.1;
|
|
text-transform: uppercase;
|
|
color: #000;
|
|
}
|
|
|
|
.sheet-head h2 {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
font-weight: 900;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: #000;
|
|
}
|
|
|
|
.sheet-mark {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Rein dekorativ, deshalb aria-hidden im Markup. */
|
|
.sheet-barcode {
|
|
width: 140px;
|
|
height: 25px;
|
|
background: repeating-linear-gradient(
|
|
90deg,
|
|
#000,
|
|
#000 2px,
|
|
#fff 2px,
|
|
#fff 5px,
|
|
#000 5px,
|
|
#000 8px,
|
|
#fff 8px,
|
|
#fff 10px
|
|
);
|
|
}
|
|
|
|
.sheet-classbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 10px 15px;
|
|
border-bottom: 4px solid #000;
|
|
color: #fff;
|
|
font-size: 14px;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.sheet-grid {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.sheet-grid td {
|
|
border: 2px solid #000;
|
|
padding: 10px;
|
|
vertical-align: top;
|
|
word-wrap: break-word;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.sheet-label {
|
|
font-size: 10px;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
margin-bottom: 4px;
|
|
color: #000;
|
|
}
|
|
|
|
.sheet-value {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
min-height: 20px;
|
|
color: #000;
|
|
}
|
|
|
|
.sheet-value-emphasis {
|
|
color: #7f1d1d;
|
|
}
|
|
|
|
.sheet-photo {
|
|
width: 120px;
|
|
height: 160px;
|
|
margin: 0 auto;
|
|
border: 2px solid #000;
|
|
background: #f0f0f0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
color: #555;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sheet-photo img {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.sheet-signature {
|
|
border-bottom: 2px solid #000;
|
|
min-height: 30px;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
font-family: 'Times New Roman', Times, serif;
|
|
font-style: italic;
|
|
font-size: 18px;
|
|
color: #000;
|
|
}
|
|
|
|
.sheet-foot {
|
|
margin-top: auto;
|
|
border-top: 4px solid #000;
|
|
padding: 10px 15px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
font-family: var(--font-mono, monospace);
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
color: #000;
|
|
}
|
|
|
|
/* Schmale Fenster: das Blatt behaelt seine Proportionen und wird als Ganzes
|
|
verkleinert, statt umzubrechen. Ein umgebrochenes Formular waere ein
|
|
anderes Formular. */
|
|
@media (max-width: 230mm) {
|
|
.sheet-scroll {
|
|
overflow-x: auto;
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Die Druckansicht einer Akte.
|
|
|
|
Schwarz auf weiss wie das Aktenblatt, aber im Aufbau eines Berichts:
|
|
Deckblatt, Kopfdaten, Abschnitte, Verlauf, Anhangsverzeichnis. Das
|
|
Wasserzeichen setzt der PDF-Dienst in Kopf- und Fusszeile jeder Seite.
|
|
|
|
Ein Export darf nie sauberer aussehen als die Akte ist: geänderte
|
|
Einträge sind gekennzeichnet, zurückgezogene stehen als Grabstein, und
|
|
gesperrte Felder erscheinen als Balken mit Vermerk statt zu fehlen.
|
|
Siehe docs/08-suche-und-export.md.
|
|
--------------------------------------------------------------------- */
|
|
.print-sheet {
|
|
max-width: 180mm;
|
|
margin: 0 auto;
|
|
padding: 12mm 0;
|
|
background: #fff;
|
|
color: #000;
|
|
font-size: 10pt;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.print-sheet h1 {
|
|
font-size: 18pt;
|
|
font-weight: 900;
|
|
margin: 0 0 2mm;
|
|
color: #000;
|
|
}
|
|
|
|
.print-sheet h2 {
|
|
font-size: 12pt;
|
|
font-weight: 900;
|
|
margin: 8mm 0 2mm;
|
|
padding-bottom: 1mm;
|
|
border-bottom: 1.5pt solid #000;
|
|
color: #000;
|
|
break-after: avoid;
|
|
}
|
|
|
|
.print-sheet h3 {
|
|
font-size: 10pt;
|
|
font-weight: 900;
|
|
margin: 4mm 0 1mm;
|
|
color: #000;
|
|
break-after: avoid;
|
|
}
|
|
|
|
.print-cover {
|
|
border: 3pt solid #000;
|
|
padding: 8mm;
|
|
margin-bottom: 8mm;
|
|
}
|
|
|
|
.print-stamp {
|
|
display: inline-block;
|
|
border: 2pt solid #000;
|
|
padding: 1mm 3mm;
|
|
font-weight: 900;
|
|
letter-spacing: 1pt;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.print-meta {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 3mm;
|
|
}
|
|
|
|
.print-meta th,
|
|
.print-meta td {
|
|
border: 0.75pt solid #000;
|
|
padding: 1.5mm 2mm;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
font-size: 9pt;
|
|
}
|
|
|
|
.print-meta th {
|
|
width: 38mm;
|
|
font-weight: 900;
|
|
}
|
|
|
|
/* Ein Eintrag soll nicht über den Seitenrand zerrissen werden. */
|
|
.print-entry {
|
|
break-inside: avoid;
|
|
padding: 2mm 0;
|
|
border-bottom: 0.5pt dotted #666;
|
|
}
|
|
|
|
.print-label {
|
|
font-size: 8pt;
|
|
font-weight: 900;
|
|
text-transform: uppercase;
|
|
color: #000;
|
|
}
|
|
|
|
.print-note {
|
|
font-size: 8pt;
|
|
color: #444;
|
|
}
|
|
|
|
/* Die Schwärzung im Druck: ein voller Balken, kein Weglassen. */
|
|
.print-redacted {
|
|
display: inline-block;
|
|
min-width: 30mm;
|
|
background: #000;
|
|
color: #000;
|
|
user-select: none;
|
|
}
|
|
}
|
|
|
|
/* Drucken: nur das Blatt, ohne Rahmenwerk der Anwendung. Kopf- und Fusszeile
|
|
mit dem Wasserzeichen setzt der PDF-Dienst, siehe docs/08. */
|
|
@media print {
|
|
.no-print {
|
|
display: none !important;
|
|
}
|
|
|
|
body {
|
|
background: #fff !important;
|
|
}
|
|
|
|
.sheet {
|
|
margin: 0;
|
|
padding: 0;
|
|
width: auto;
|
|
}
|
|
|
|
.sheet-frame {
|
|
min-height: auto;
|
|
}
|
|
|
|
.sheet-classbar,
|
|
.print-redacted,
|
|
.print-stamp {
|
|
/* Sonst drucken die Flächen weiss auf weiss. */
|
|
print-color-adjust: exact;
|
|
-webkit-print-color-adjust: exact;
|
|
}
|
|
|
|
.print-sheet {
|
|
max-width: none;
|
|
padding: 0;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.redaction::after {
|
|
animation: none;
|
|
transform: scaleX(0);
|
|
}
|
|
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
}
|