frontend: Projektgerüst mit Vite — Single-HTML in ES-Module + Bündelung

Erster Schritt der Modularisierung (ROADMAP „tragfähige Codebasis",
TASKS Phase 1). Der bisher einzelne index.html-Prototyp wird zur Vite-App:

- CSS -> src/style.css, gesamtes JS -> src/app.js (noch als ein Modul;
  Parser/Renderer werden in den folgenden Checkboxen herausgelöst).
- index.html ist der Vite-Entry (<script type="module" src="/src/app.js">).
- npm run build (vite + vite-plugin-singlefile) inlint JS+CSS+Favicon (als
  data:-URI) in EINE self-contained dist/index.html -> bleibt file://-tauglich.
- Vitest als Testrunner eingerichtet (tests/, passWithNoTests bis Schritt 2/3).
- Pages-Workflow: Node + npm ci + npm test + npm run build, deployt dist/;
  Favicon-sed entfällt (inline), nur ../LICENSE + Footer-Version bleiben.
- .claude/launch.json auf Vite-Dev-Server (Port 8137) umgestellt.

Entscheidung als D19 dokumentiert (weicht D11 „kein Build-Zwang" bewusst auf,
hält aber D16 file://-Versprechen über die gebündelte Datei). README (de/en)
und frontend/CLAUDE.md um Dev-/Build-/Test-Workflow ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-21 20:47:23 +02:00
co-authored by Claude Opus 4.8
parent ad57707dbc
commit 78d1000f61
14 changed files with 3938 additions and 1883 deletions
+1226
View File
File diff suppressed because it is too large Load Diff
+607
View File
@@ -0,0 +1,607 @@
:root{
--paper:#F3F6F9;
--grid:rgba(36,52,71,.055);
--ink:#243447;
--line:#41556E;
--card:#FFFFFF;
--muted:#6B7A8C;
--or:#0F766E;
--warn:#B45309;
--r:8px;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
font-family:'IBM Plex Sans',system-ui,sans-serif;
color:var(--ink);
background:
repeating-linear-gradient(0deg,transparent 0 23px,var(--grid) 23px 24px),
repeating-linear-gradient(90deg,transparent 0 23px,var(--grid) 23px 24px),
var(--paper);
/* Mobile Viewport-Höhe: 100vh ist die *große* Höhe (inkl. Fläche hinter
der unteren Browserleiste) — Footer/Editor-Titelzeile rutschen dahinter.
100dvh folgt der sichtbaren Fläche, versagt aber bei Browsern mit
Overlay-Leiste (z. B. Brave). Deshalb setzt JS zusätzlich --app-height
aus window.visualViewport.height (die wirklich sichtbare Höhe). Reihen-
folge = Fallback-Kaskade: vh → dvh → exakte JS-Höhe. */
height:100vh;
height:100dvh;
height:var(--app-height, 100dvh);
overflow:hidden;
display:flex;flex-direction:column;
padding:24px 20px 12px;
}
header{max-width:1200px;width:100%;margin:0 auto 16px;flex:0 0 auto}
.header-row{display:flex;align-items:center;justify-content:space-between;gap:16px}
.header-tools{display:flex;align-items:center;gap:12px}
.fsbtn{
display:inline-flex;align-items:center;justify-content:center;
width:30px;height:29px;padding:0;cursor:pointer;
border:1px solid rgba(36,52,71,.2);border-radius:8px;
background:var(--card);color:var(--muted);
}
.fsbtn svg{display:block}
.fsbtn:hover{border-color:var(--or);color:var(--or)}
.fsbtn.active{background:var(--or);color:#fff;border-color:var(--or)}
/* Vollbild: Panels nutzen fast die ganze Fensterbreite; es bleibt ein
schmaler Rand (halber Panel-Zwischenraum = 7 px), damit es nicht gedrängt
wirkt. */
body.fullscreen{padding-left:7px;padding-right:7px}
body.fullscreen header,
body.fullscreen .app,
body.fullscreen .site-footer{max-width:none}
h1{font-size:1.25rem;font-weight:600;letter-spacing:-.01em}
header p{margin-top:4px;color:var(--muted);font-size:.9rem}
.sub-short{display:none} /* Kurzfassung nur auf kleinem Bildschirm */
.langsel{
display:inline-flex;flex:0 0 auto;
border:1px solid rgba(36,52,71,.2);border-radius:999px;overflow:hidden;
}
.langsel button{
border:0;background:var(--card);cursor:pointer;color:var(--muted);
font-family:'IBM Plex Mono',monospace;font-size:.72rem;letter-spacing:.04em;
padding:5px 10px;
}
.langsel button + button{border-left:1px solid rgba(36,52,71,.14)}
.langsel button:hover:not(.active){color:var(--or)}
.langsel button.active{background:var(--or);color:#fff}
.langmore{letter-spacing:0;padding:5px 8px;font-weight:600}
.langsel .lang-extra{display:none} /* PL/RU/HI erst nach Aufklappen */
.langsel.expanded .lang-extra{display:block}
.site-footer{
max-width:1200px;margin:14px auto 0;
color:var(--muted);font-size:.78rem;text-align:center;
}
.site-footer a{color:var(--or);text-decoration:none}
.site-footer a:hover{text-decoration:underline}
.site-footer .vibe{cursor:help;text-decoration:underline dotted;text-underline-offset:2px}
/* Horizontal (Toggle aus): Diagramm oben, Editor unten — je volle Breite.
Vertikal (Toggle an, .side): Editor links, Diagramm rechts.
Zwischen beiden liegt ein verschiebbarer Gutter (Splitter). */
.app{
max-width:1200px;width:100%;margin:0 auto;
flex:1 1 auto;min-height:0;
display:grid;gap:0;
grid-template-columns:minmax(0,1fr);
grid-template-rows:var(--drow,60%) 14px minmax(0,1fr);
grid-template-areas:"diagram" "gutter" "editor";
align-items:stretch;
}
.app.side{
grid-template-columns:var(--col,360px) 14px minmax(0,1fr);
grid-template-rows:minmax(0,1fr);
grid-template-areas:"editor gutter diagram";
}
.panel.editor{grid-area:editor}
.panel.right{grid-area:diagram}
.gutter{grid-area:gutter}
/* Splitter/Gutter: stapelweise horizontal, seitlich vertikal. */
.gutter{
position:relative;align-self:stretch;justify-self:stretch;
height:14px;cursor:row-resize;touch-action:none;
}
.app.side .gutter{height:auto;width:14px;cursor:col-resize}
.gutter::before{
content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
width:46px;height:4px;border-radius:999px;background:rgba(36,52,71,.16);
transition:background .15s;
}
.app.side .gutter::before{width:4px;height:46px}
.gutter:hover::before,.gutter.dragging::before{background:var(--or)}
/* Minimieren: Panel auf seine Titelzeile schrumpfen, Körper ausblenden.
'a' = Diagramm minimiert (Editor maximal), 'b' = Editor minimiert. */
.panel.collapsed .editor-body,
.panel.collapsed .diagram,
.panel.collapsed .warnings{display:none}
.panel.collapsed .copybtn,
.panel.collapsed .switch,
.panel.collapsed .seg{display:none}
.panel.collapsed .panel-head{cursor:pointer}
.panel.collapsed .panel-head>span{cursor:pointer}
/* Gestapelt: kollabiertes Panel = Titelzeile (auto), anderes füllt. */
.app:not(.side).collapse-diagram{grid-template-rows:auto 14px minmax(0,1fr)}
.app:not(.side).collapse-editor{grid-template-rows:minmax(0,1fr) 14px auto}
/* Seitlich: schmaler vertikaler Streifen mit gedrehter Beschriftung. */
.app.side.collapse-editor{grid-template-columns:auto 14px minmax(0,1fr)}
.app.side.collapse-diagram{grid-template-columns:minmax(0,1fr) 14px auto}
.app.side .panel.collapsed{align-self:stretch}
.app.side .panel.collapsed .panel-head{
flex-direction:column;align-items:center;gap:12px;
height:100%;padding:12px 6px;border-bottom:0;
}
.app.side .panel.collapsed .panel-head>span{
writing-mode:vertical-rl;transform:rotate(180deg);
margin:0;white-space:nowrap;
}
.app.side .panel.collapsed .winbtns{flex-direction:column}
/* Schmaler Viewport: Seitenmodus fällt auf gestapelt zurück. */
@media (max-width:820px){
.app.side{
grid-template-columns:minmax(0,1fr);
grid-template-rows:var(--drow,60%) 14px minmax(0,1fr);
grid-template-areas:"diagram" "gutter" "editor";
}
.app.side.collapse-diagram{grid-template-columns:minmax(0,1fr);grid-template-rows:auto 14px minmax(0,1fr)}
.app.side.collapse-editor{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) 14px auto}
.app.side .panel.collapsed .panel-head{flex-direction:row;align-items:center;height:auto;padding:10px 16px}
.app.side .panel.collapsed .panel-head>span{writing-mode:horizontal-tb;transform:none}
.app.side .panel.collapsed .winbtns{flex-direction:row}
.app.side .gutter{height:14px;width:auto;cursor:row-resize}
.app.side .gutter::before{width:46px;height:4px}
}
/* Editor-Innenaufbau: Textarea + aufklappbare Agenda.
Horizontal nebeneinander, vertikal (.side) untereinander. */
.editor-body{display:flex;align-items:stretch;flex:1 1 auto;min-height:0}
.editor-body textarea{flex:1 1 auto;min-width:0;min-height:0}
.agenda{
flex:0 0 auto;
border-left:1px dashed rgba(36,52,71,.18);
display:flex;flex-direction:column;
}
.agenda[open]{width:300px}
.agenda>summary{
list-style:none;cursor:pointer;white-space:nowrap;
padding:10px 16px;
font-family:'IBM Plex Mono',monospace;
font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);
display:flex;align-items:center;gap:7px;
}
.agenda>summary::-webkit-details-marker{display:none}
.agenda>summary::before{content:'▸';display:inline-block;transition:transform .15s}
.agenda[open]>summary::before{transform:rotate(90deg)}
.agenda{min-height:0}
.agenda[open]{overflow:hidden}
.agenda .hint{border-top:0;flex:1 1 auto;min-height:0;overflow:auto}
.side .editor-body{flex-direction:column;align-items:stretch}
.side .agenda{border-left:0;border-top:1px dashed rgba(36,52,71,.18)}
.side .agenda[open]{width:auto}
.panel{
background:var(--card);
border:1px solid rgba(15,118,110,.30); /* dezentes Petrol */
border-radius:12px;
overflow:hidden;
display:flex;flex-direction:column;min-height:0;min-width:0;
}
.panel-head{
flex:0 0 auto;
padding:10px 16px;
display:flex;justify-content:space-between;align-items:center;gap:14px;
font-family:'IBM Plex Mono',monospace;
font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;
color:var(--muted);
border-bottom:1px solid rgba(36,52,71,.1);
flex-wrap:wrap;
}
.panel-head>span{margin-right:auto}
.switch{
display:flex;align-items:center;gap:7px;cursor:pointer;
text-transform:none;letter-spacing:0;
font-family:'IBM Plex Sans',sans-serif;font-size:.78rem;color:var(--muted);
}
.switch input{position:absolute;opacity:0;width:0;height:0}
.switch i{
width:30px;height:17px;border-radius:999px;background:#C6CFD8;
position:relative;transition:background .15s;flex:0 0 auto;
}
.switch i::before{
content:'';position:absolute;top:2px;left:2px;width:13px;height:13px;
border-radius:50%;background:#fff;transition:left .15s;
box-shadow:0 1px 2px rgba(36,52,71,.25);
}
.switch input:checked + i{background:var(--or)}
.switch input:checked + i::before{left:15px}
.seg{
display:inline-flex;align-items:stretch;
border:1px solid rgba(36,52,71,.2);border-radius:999px;overflow:hidden;
}
.seg label{display:flex;cursor:pointer}
.seg input{position:absolute;opacity:0;width:0;height:0}
.seg span{
display:grid;place-items:center;padding:4px 10px;color:var(--muted);
transition:background .15s,color .15s;
}
.seg span svg{display:block}
.seg label + label span{border-left:1px solid rgba(36,52,71,.14)}
.seg input:checked + span{background:var(--or);color:#fff}
.seg input:focus-visible + span{outline:2px solid var(--or);outline-offset:-2px}
.winbtns{display:inline-flex;gap:4px;align-items:center}
.winbtn{
width:19px;height:19px;display:grid;place-items:center;
border:1px solid rgba(36,52,71,.2);border-radius:5px;
background:var(--card);color:var(--muted);cursor:pointer;
font-size:.72rem;line-height:1;padding:0;
}
.winbtn:hover{border-color:var(--or);color:var(--or)}
.winbtn.active{background:var(--or);color:#fff;border-color:var(--or)}
.copybtn{
color:var(--muted);background:var(--card);
border:1px solid rgba(36,52,71,.2);border-radius:6px;
padding:3px 6px;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;
}
.copybtn:hover,.copybtn.done{border-color:var(--or);color:var(--or)}
.copybtn svg{display:block;width:15px;height:15px}
.copybtn .ic-done{display:none}
.copybtn.done .ic-copy{display:none}
.copybtn.done .ic-done{display:block}
.dlbtn{gap:3px}
.dl-label{font-family:'IBM Plex Mono',monospace;font-size:.62rem;font-weight:600;letter-spacing:.02em}
/* Download-Gruppe: Desktop zeigt SVG/PNG inline, der Trigger ist versteckt.
Auf Mobil (body.mobile, s. u.) klappt sie als Overlay-Menü auf. */
.dlgroup{display:inline-flex;align-items:center;gap:3px}
.dlmenu{display:inline-flex;gap:3px}
.dltrigger{display:none}
.legendbtn{display:none} /* nur auf kleinem Bildschirm sichtbar */
.togglebtn[aria-pressed="true"]{background:var(--or);color:#fff;border-color:var(--or)}
.togglebtn[aria-pressed="true"]:hover{color:#fff}
textarea{
display:block;width:100%;min-height:0;
border:0;outline:none;resize:none;
padding:14px 16px;
font-family:'IBM Plex Mono',monospace;
font-size:.85rem;line-height:1.7;
color:var(--ink);background:var(--card);
tab-size:2;
}
.hint{
padding:12px 16px;border-top:1px dashed rgba(36,52,71,.18);
font-size:.8rem;color:var(--muted);line-height:1.7;
}
.hint code{
font-family:'IBM Plex Mono',monospace;font-size:.75rem;
background:var(--paper);border:1px solid rgba(36,52,71,.12);
border-radius:4px;padding:0 5px;
}
.hint .or-code{color:var(--or);border-color:rgba(15,118,110,.4)}
.diagram{
flex:1 1 auto;min-height:0;
padding:28px 16px 24px;
overflow:auto;
background:transparent;
}
.panel.right{background:rgba(255,255,255,.6);position:relative}
.warnings{padding:0 16px 14px;font-size:.8rem;color:var(--warn)}
.warnings div{margin-top:4px}
.empty{color:var(--muted);font-size:.85rem;padding:8px 4px}
/* Zoom-Steuerung: schwebt unten rechts über dem Diagramm. */
.zoomctl{
position:absolute;right:14px;bottom:14px;z-index:5;
display:inline-flex;align-items:center;gap:2px;padding:2px;
background:var(--card);border:1px solid rgba(36,52,71,.18);
border-radius:8px;box-shadow:0 1px 4px rgba(36,52,71,.14);
}
.zoomctl button{
border:0;background:transparent;cursor:pointer;color:var(--muted);
font-family:'IBM Plex Mono',monospace;font-size:.82rem;line-height:1;
padding:4px 8px;border-radius:6px;min-width:30px;
}
#zoomReset{min-width:48px;font-size:.72rem}
.zoomctl button:hover{background:var(--paper);color:var(--or)}
.panel.collapsed .zoomctl{display:none}
/* ---------- Baum ---------- */
.tree{width:max-content;margin:0 auto;list-style:none;position:relative}
.tree ul{list-style:none}
.tree li{display:flex;flex-direction:column;align-items:center;position:relative}
.tree > li + li{margin-top:36px}
.node{
position:relative;
color:var(--ink);
background:var(--card);
border:1.5px solid var(--line);
border-radius:var(--r);
padding:7px 14px;
font-size:.88rem;font-weight:500;line-height:1.3;
white-space:nowrap;
box-shadow:0 1px 2px rgba(36,52,71,.08);
}
a.node{text-decoration:none;cursor:pointer}
a.node:hover{box-shadow:0 2px 8px rgba(36,52,71,.22)}
.ext{font-size:.72em;margin-left:5px;opacity:.55}
.size{
position:absolute;top:-9px;right:-9px;
font-family:'IBM Plex Mono',monospace;
font-size:.58rem;font-weight:500;letter-spacing:.02em;
background:var(--or);color:#fff;
border:1.5px solid var(--card);
border-radius:999px;padding:1px 6px;line-height:1.4;
}
.tags{
position:absolute;right:-9px;bottom:-9px;
display:flex;gap:3px;
}
.tag{
font-family:'IBM Plex Mono',monospace;
font-size:.58rem;font-weight:500;letter-spacing:.02em;
background:#EEF1F4;color:var(--ink);
border:1.5px solid var(--line);
border-radius:999px;padding:1px 6px;line-height:1.4;
box-shadow:0 0 0 1.5px var(--card);
}
.ghost-node{
position:relative;margin-top:14px;
border:1.5px dashed var(--warn);
color:var(--warn);background:rgba(180,83,9,.06);
border-radius:var(--r);padding:4px 12px;
font-size:.74rem;font-style:italic;white-space:nowrap;
}
.ghost-node::before{
content:'';position:absolute;top:-14px;left:50%;height:14px;
border-left:2px dashed var(--warn);
}
.root-node{background:var(--ink);border-color:var(--ink);color:#fff;font-weight:600;padding:9px 20px}
/* Günstigster Pfad (Inversion): nicht benötigte Knoten (nicht-gewählte
any-of-Alternativen samt Teilbaum) treten zurück (blass, entsättigt);
der günstige Pfad hebt sich dadurch von allein ab. */
.cheap-on .node:not(.cheap){opacity:.32;filter:saturate(.4)}
/* implizit als M angenommene Größe: invertiertes Badge (weiß statt petrol) */
.size.implicit{background:var(--card);color:var(--or);border-color:var(--or)}
/* Günstigster-Pfad-Linie: gestrichelt, geschwungen, petrol — fädelt durch
die Endknoten (Blätter) des Pfads. Zwei Overlays in #out (erben dessen
zoom): die kräftige Linie liegt HINTER den Knoten (nur in den Lücken
sichtbar), eine abgetönte Kopie liegt DAVOR (deutet den Verlauf beim
Durchschreiten von Knoten nur schwach an). Kleine Stationspunkte an den
Blättern zeigen, welche Knoten wirklich auf dem Pfad enden — durchquerte
Fremdknoten tragen keinen Punkt. Reihenfolge im DOM steuert vorn/hinten. */
.cheap-overlay{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
.cheap-path{fill:none;stroke:var(--or);stroke-width:2.5;stroke-linecap:round;
stroke-dasharray:8 6;opacity:.9}
.cheap-path.faint{opacity:.2}
/* Stationspunkt: groß, aber blass gefüllt — der Knotentext bleibt lesbar */
.cheap-dot{fill:var(--or);fill-opacity:.2;stroke:var(--or);stroke-opacity:.35;stroke-width:1.5}
/* --- ALL OF: Kinder nebeneinander, durchgezogener Verteiler --- */
ul.and{display:flex;align-items:flex-start;position:relative;padding-top:14px}
ul.and::before{
content:'';position:absolute;top:0;left:50%;height:14px;border-left:2px solid var(--line);
}
ul.and>li{padding:16px 12px 0}
ul.and>li::before,
ul.and>li::after{
content:'';position:absolute;top:0;right:50%;
border-top:2px solid var(--line);width:50%;height:16px;
}
ul.and>li::after{right:auto;left:50%;border-left:2px solid var(--line)}
ul.and>li:only-child{padding-top:14px}
ul.and>li:only-child::before,ul.and>li:only-child::after{border-top:none}
ul.and>li:only-child::after{border-left:2px solid var(--line);height:14px}
ul.and>li:first-child::before,
ul.and>li:last-child::after{border:0 none}
ul.and>li:last-child::before{border-right:2px solid var(--line);border-radius:0 6px 0 0}
ul.and>li:first-child::after{border-radius:6px 0 0 0}
/* --- ANY OF: durchgehend gestrichelte graue Linien (Haupt- + Abzweige) --- */
li.has-or{align-items:flex-start}
ul.or{
display:flex;flex-direction:column;
align-items:flex-start;
position:relative;
padding-top:14px;
}
ul.or::before{
content:'';position:absolute;top:0;left:9px;height:14px;border-left:2px dashed var(--muted);
}
ul.or>li{position:relative;align-items:flex-start;padding:5px 0 5px 30px}
/* Abzweig auf Höhe der Knoten-Mitte (5px li-Padding + halbe Knotenhöhe), nicht Mitte des Teilbaums */
ul.or>li::before{
content:'';position:absolute;left:9px;top:23px;width:21px;border-top:2px dashed var(--muted);
}
ul.or>li::after{
content:'';position:absolute;left:9px;top:0;height:100%;border-left:2px dashed var(--muted);
}
ul.or>li:last-child::after{height:23px}
/* all-of-Teilbaum UNTER einer any-of-Alternative (nur horizontal):
schmal transponiert stapeln — Kinder untereinander mit linkem, solidem
Verteiler (wie im kompakten Modus), statt breitem Fächer. Sonst zwingt der
breite Fächer ein Zentrieren, das den Alternativknoten (und damit den
ganzen Elternbaum) weit nach rechts schiebt. Passt zugleich stilistisch
zur gestapelten any-of-Spalte darüber. Vertikal/kompakt haben ihre eigene
`ul.and`-Behandlung und bleiben unberührt. */
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and{
flex-direction:column;align-items:flex-start;padding-top:14px;
}
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and::before{left:9px;height:14px}
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li{padding:5px 0 5px 30px}
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li::before{
left:9px;right:auto;top:23px;width:21px;height:0;
border:0;border-top:2px solid var(--line);border-radius:0;
}
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li::after{
left:9px;right:auto;top:0;width:0;height:100%;
border:0;border-left:2px solid var(--line);border-radius:0;
}
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li:last-child::after{height:23px}
ul.or .node{
border-color:var(--muted);
box-shadow:none;
font-weight:400;
}
/* ---------- Status (Pastell) ---------- */
.node.st-idee {background:#EBEDEF;border-color:#A2ABB5;color:var(--ink)}
.node.st-geplant {background:#EBE4F6;border-color:#A991D4;color:var(--ink)}
.node.st-arbeit {background:#FADDE4;border-color:#D897A8;color:var(--ink)}
.node.st-durchstich{background:#FBF2CE;border-color:#D9BE63;color:var(--ink)}
.node.st-fertig {background:#DCF1DE;border-color:#86C293;color:var(--ink)}
.node.st-prod {background:#DBEAF8;border-color:#85ACD7;color:var(--ink)}
.node.st-verworfen{
background:#F1F2F4;border-color:#B3BAC2;border-style:dashed;
color:var(--muted);text-decoration:line-through;box-shadow:none;
}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.chip{
font-family:'IBM Plex Mono',monospace;font-size:.72rem;
border:1.5px solid;border-radius:6px;padding:2px 8px;
color:var(--ink);white-space:nowrap;
}
.chip.st-idee {background:#EBEDEF;border-color:#A2ABB5}
.chip.st-geplant {background:#EBE4F6;border-color:#A991D4}
.chip.st-arbeit {background:#FADDE4;border-color:#D897A8}
.chip.st-durchstich{background:#FBF2CE;border-color:#D9BE63}
.chip.st-fertig {background:#DCF1DE;border-color:#86C293}
.chip.st-prod {background:#DBEAF8;border-color:#85ACD7}
.chip.st-verworfen{background:#F1F2F4;border-color:#B3BAC2;border-style:dashed;color:var(--muted);text-decoration:line-through}
/* ---------- Transponierte Modi (vertikal + kompakt) ---------- */
/* Gemeinsame Basis: Kinder untereinander, Sammelleiste links,
Abzweig auf Knotenmitte (23-px-Offset). */
.tree.vertical, .tree.kompakt{margin:0}
.tree.vertical li, .tree.kompakt li{align-items:flex-start}
.tree.vertical ul.and, .tree.kompakt ul.and{flex-direction:column;align-items:flex-start;padding-top:14px}
.tree.vertical ul.and::before, .tree.kompakt ul.and::before{left:9px;height:14px}
.tree.vertical ul.and>li, .tree.kompakt ul.and>li{padding:5px 0 5px 30px}
.tree.vertical ul.and>li::before, .tree.kompakt ul.and>li::before{
left:9px;right:auto;top:23px;width:21px;height:0;
border:0;border-top:2px solid var(--line);border-radius:0;
}
.tree.vertical ul.and>li::after, .tree.kompakt ul.and>li::after{
left:9px;right:auto;top:0;width:0;height:100%;
border:0;border-left:2px solid var(--line);border-radius:0;
}
.tree.vertical ul.and>li:last-child::after, .tree.kompakt ul.and>li:last-child::after{height:23px}
/* Nur vertikal: echter transponierter, ZENTRIERTER LR-Baum — analog zur
horizontalen Darstellung, nur um 90° gedreht. Der Überknoten sitzt
vertikal mittig zu seiner Kindergruppe; die all-of-Verbindung tritt rechts
auf halber Höhe aus, eine vertikale Sammelleiste verteilt auf die Kinder.
Kompakt bleibt unberührt (führt all-of nach unten). */
.tree.vertical li.has-and{flex-direction:row;align-items:center}
.tree.vertical li.has-and>ul.and{padding-top:0}
/* Stub von der Parent-Mitte (halbe Höhe) zur vertikalen Sammelleiste */
.tree.vertical li.has-and>ul.and::before{
left:0;top:50%;width:9px;height:0;
border-left:0;border-top:2px solid var(--line);
}
/* Abzweig trifft die tatsächliche Knotenmitte des Kindes:
Blatt- und Any-of-Kinder sitzen oben (Offset 23 px), all-of-Zwischenknoten
sind selbst vertikal zentriert (50 %). Ohne diese Unterscheidung liefen die
Linien zu zentrierten Zwischenknoten ins Leere. Die Enden der Sammelleiste
folgen demselben Abzweigpunkt des jeweiligen Rand-Kindes. */
.tree.vertical li.has-and>ul.and>li:first-child::after{top:23px;height:calc(100% - 23px)}
.tree.vertical li.has-and>ul.and>li:only-child::after{border:0}
.tree.vertical li.has-and>ul.and>li.has-and::before{top:50%}
.tree.vertical li.has-and>ul.and>li.has-and:first-child::after{top:50%;height:50%}
.tree.vertical li.has-and>ul.and>li.has-and:last-child::after{top:0;height:50%}
.tree.vertical li.has-and>ul.and>li.has-and:only-child::after{border:0}
/* Mehr Luft zwischen gestapelten Knoten (transponierte Modi): sonst
überlappt das Größen-Badge (oben rechts) eines Knotens mit den
Namens-Tags (unten rechts) des Knotens darüber. Nur zusätzlicher Abstand
NACH unten, damit der obere 23-px-Abzweig unverändert bleibt. */
.tree.kompakt ul.and>li,
.tree.kompakt ul.or>li,
.tree.vertical ul.or>li,
.tree.vertical ul.and>li:not(.has-and){
padding-bottom:20px;
}
/* Zentrierte all-of-Zwischenknoten (nur vertikal) brauchen den Abstand
symmetrisch, damit ihr 50%-Abzweig auf der Knotenmitte bleibt. */
.tree.vertical li.has-and>ul.and>li.has-and{
padding-top:16px;padding-bottom:16px;
}
/* Vertikal + any-of-Alternative, die selbst all-of zerlegt (z. B. „Web+Nativ“):
Der Knoten sitzt vertikal mittig zu seinem Rechts-Fächer, also muss auch der
EINKOMMENDE any-of-Abzweig auf die Knotenmitte (50 %) zeigen statt auf 23 px —
sonst ist der Knoten von seiner Linie von oben abgetrennt. Symmetrisches
Padding hält die Mitte bei 50 % (überschreibt das asymmetrische ul.or>li). */
.tree.vertical ul.or>li.has-and{padding-top:16px;padding-bottom:16px}
.tree.vertical ul.or>li.has-and::before{top:50%}
.tree.vertical ul.or>li.has-and:last-child::after{height:50%}
/* Kompakt: auch All-of läuft nach unten (spart Breite) — kein Rechts-Fächer.
Die graue, gestrichelte Any-of-Führung kommt aus der Basis und gilt hier
wie in allen Modi. Gate-Codierung: und = durchgezogen dunkel (Tinte),
oder = gestrichelt grau. */
/* ---------- Kleiner Bildschirm (Smartphone) ----------
body.mobile wird per matchMedia (<=640px) gesetzt. Standardmäßig ist ein
Bereich groß, der andere schrumpft auf seine Titelzeile und dient als
Umschalter (Tippen bringt ihn nach vorn). Die Min/Normal/Max-Buttons
entfallen; geteilt wird weiterhin über den **Splitter**, der sichtbar
bleibt. Das gestapelte Layout inkl. Gutter liefert bereits die
820-px-Regel weiter oben. Die Legende bekommt einen eigenen Umschalter
statt der dauerhaften „AGENDA“-Zeile. */
body.mobile .winbtns{display:none}
/* Mobil: freie Aufteilung per Splitter — beide Titelzeilen bleiben IMMER
stehen (Zeilen-Minimum --pmin ≈ Titelzeilenhöhe). Kein diskretes
Minimieren/Snap; ein zusammengeschobenes Panel schrumpft nur bis auf seine
volle Titelzeile (Körper flext auf 0, Panel ist overflow:hidden). Gilt auch
für den Seitenmodus, der auf Mobil ohnehin gestapelt ist. Der Drag setzt
--drow (JS klemmt), das Tippen auf eine Titelzeile maximiert das Panel. */
body.mobile .app,
body.mobile .app.side{
grid-template-columns:minmax(0,1fr);
grid-template-rows:minmax(var(--pmin-d,49px),var(--drow,60%)) 14px minmax(var(--pmin-e,44px),1fr);
grid-template-areas:"diagram" "gutter" "editor";
}
/* Untertitel: Kurzfassung, garantiert einzeilig (Ellipsis als Sicherung) */
body.mobile .sub-full{display:none}
body.mobile .sub-short{display:inline}
body.mobile header p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Editor immer gestapelt (Textarea oben, Legende darunter) */
body.mobile .editor-body{flex-direction:column;align-items:stretch}
body.mobile .agenda{border-left:0;border-top:1px dashed rgba(36,52,71,.18)}
body.mobile .agenda[open]{width:auto}
/* Legende: keine dauerhafte Summary-Zeile, dafür der #legendBtn im Kopf */
body.mobile .agenda>summary{display:none}
body.mobile .legendbtn{display:inline-flex}
/* Sprachwahl schlank: eingeklappt nur die aktive Sprache. Ein Tipp klappt die
volle Leiste als Overlay über die Kopfzeile auf (verdeckt fsToggle usw.,
statt die Zeile zu verbreitern); nach der Auswahl klappt sie wieder ein
(JS). Das „…“ entfällt hier — die aktive Sprache ist selbst der Auslöser. */
body.mobile .header-tools{position:relative}
body.mobile .langsel button[data-lang]{display:none}
body.mobile .langsel button[data-lang].active{display:inline-block}
body.mobile .langmore{display:none}
body.mobile .langsel.expanded{
position:absolute;top:0;right:0;z-index:40;
background:var(--card);
box-shadow:0 6px 18px rgba(36,52,71,.28);
}
body.mobile .langsel.expanded button[data-lang]{display:inline-block}
/* Modus-Wähler kompakt: nur das aktive Icon (ein Drittel Platz), Tippen
schaltet reihum weiter (horizontal → kompakt → vertikal → …, siehe JS). */
body.mobile .seg label{display:none}
body.mobile .seg label:has(input:checked){display:flex}
/* Download schlank: nur der Trigger-Icon-Button; ein Tipp klappt SVG/PNG als
Dropdown-Overlay unter der Kopfzeile auf (verhindert den Zeilenumbruch durch
die breiten „SVG“/„PNG“-Labels). Kopieren bleibt sichtbar. */
body.mobile .dlgroup{position:relative}
body.mobile .dltrigger{display:inline-flex}
body.mobile .dltrigger[aria-expanded="true"]{border-color:var(--or);color:var(--or)}
body.mobile .dlmenu{display:none}
body.mobile .dlgroup.open .dlmenu{
display:flex;flex-direction:column;gap:5px;
position:absolute;top:calc(100% + 6px);right:0;z-index:40;
background:var(--card);border:1px solid rgba(36,52,71,.15);
border-radius:8px;padding:5px;box-shadow:0 6px 18px rgba(36,52,71,.28);
}