Files
werkbaum/frontend/src/style.css
T
mhoennigandClaude 837922aaa7 frontend: Zeilennummern im Texteditor + Alt+Klick auch im Text
Zwei Lücken, die beim Arbeiten am Text auffallen:

**Zeilennummern (D33).** Die Warnungen nennen Zeilennummern („Zeile 12: …"),
das Textfeld zeigte keine — man musste abzählen. Der Streifen links vom
Textfeld hebt zusätzlich die Cursor-Zeile und Zeilen mit Warnung hervor.

Die Zahlen stehen auf **gemessenen** Höhen (Spiegel-`div` aus D25, ein Marker
je Zeile), nicht auf „Zeilenhöhe × n": Der Text bricht weich um, eine lange
Zeile belegt mehrere Bildzeilen und behält doch eine Nummer. Bei 375 px Breite
brechen 134 der 143 Zeilen um — gerechnet liefe der Streifen sofort davon.

Dabei fiel ein älterer Fehler im Spiegel auf: `width = src.clientWidth` bei
`content-box` machte ihn um genau die 32 px Innenabstand zu breit, er brach
später um als das Textfeld. Mit `box-sizing:border-box` stimmen die Umbrüche
nachweislich überein (170 Bildzeilen beiderseits) — das behebt nebenbei die
Zielgenauigkeit des Sprungs zu langen Zeilen (D25).

**Alt+Klick im Textfeld (Nachtrag zu D25).** Die Rückrichtung lief bisher
allein über die Cursor-Zeile — und die scrollt bewusst nur `nearest` und nur
beim Zeilenwechsel, sonst ruckelte das Diagramm beim Tippen. Als „zeig mir
das" taugt sie damit nicht. Alt+Klick im Text (Tastatur Alt+Enter) zentriert
den Knoten jetzt und gibt ihm den Fokus: derselbe Modifier in beide
Richtungen, und mit Fokus wird daraus ein vollständiger Hin- und Rückweg.

Die Legenden-Zeile nennt beide Richtungen (kein zehnter i18n-Schlüssel),
9 Sprachen nachgezogen. SPEC §9 ergänzt, D33 angelegt, Plan-Dokument und
frontend/CLAUDE.md nachgeführt.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 13:57:47 +02:00

1031 lines
55 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*! IBM Plex Sans & IBM Plex Mono — Copyright 2019 IBM Corp.
Licensed under the SIL Open Font License 1.1 — https://scripts.sil.org/OFL
(kein Reserved Font Name deklariert). Volltext: frontend/src/fonts/OFL.txt */
/* Lokal eingebettet (self-hosted) statt von Google Fonts geladen — kein
externer Request, keine IP-Übertragung an Dritte (Datenschutz, D20). Nur die
genutzten Schnitte (Sans 400/500/600, Mono 400/500), Subsets latin +
latin-ext (decken DE/EN/ES/FR/PL); andere Skripte fallen ohnehin auf
system-ui zurück. Der Build inlint die .woff2 als data:-URI (D19). */
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;
src:url(./fonts/ibm-plex-sans-latin-400-normal.woff2) format('woff2');
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;
src:url(./fonts/ibm-plex-sans-latin-ext-400-normal.woff2) format('woff2');
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;
src:url(./fonts/ibm-plex-sans-latin-500-normal.woff2) format('woff2');
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;
src:url(./fonts/ibm-plex-sans-latin-ext-500-normal.woff2) format('woff2');
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;
src:url(./fonts/ibm-plex-sans-latin-600-normal.woff2) format('woff2');
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;
src:url(./fonts/ibm-plex-sans-latin-ext-600-normal.woff2) format('woff2');
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;
src:url(./fonts/ibm-plex-mono-latin-400-normal.woff2) format('woff2');
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;
src:url(./fonts/ibm-plex-mono-latin-ext-400-normal.woff2) format('woff2');
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;
src:url(./fonts/ibm-plex-mono-latin-500-normal.woff2) format('woff2');
unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;
src:url(./fonts/ibm-plex-mono-latin-ext-500-normal.woff2) format('woff2');
unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
:root{
--paper:#F3F6F9;
--grid:rgba(36,52,71,.055);
--ink:#243447;
--line:#41556E;
--card:#FFFFFF;
--muted:#6B7A8C;
--or:#0F766E;
--warn:#B45309;
--r:8px;
/* Exakte sichtbare Höhe; JS überschreibt sie auf documentElement aus
window.visualViewport.height (setViewportHeight). Default = Fallback. */
--app-height:100dvh;
}
*{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. Wo der
Browser die dvh-Einheit kennt, übersteuert der @supports-Block unten auf
--app-height (von JS aus window.visualViewport gesetzt, Default 100dvh) —
das folgt der wirklich sichtbaren Fläche (auch bei Overlay-Leisten wie in
Brave). Bewusst per @supports getrennt statt gestapelter height-Zeilen,
damit keine „height wird überschrieben"-Warnung entsteht. */
height:100vh;
overflow:hidden;
display:flex;flex-direction:column;
padding:24px 20px 12px;
}
/* dvh-fähige Browser: exakte sichtbare Höhe statt 100vh (siehe body oben). */
@supports (height:100dvh){
body{height:var(--app-height,100dvh)}
}
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}
/* Build-Hinweis hinter dem Titel (nur nicht-produktive Builds, siehe app.js) */
.build-badge{margin-left:-.5em;font-size:.58em;line-height:1;vertical-align:middle;cursor:help;user-select:none;text-decoration:none}
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{
/* Defaults der laufzeit-gesetzten Custom-Properties (JS setzt sie als
Inline-Style: --col/--drow per Splitter, --pmin-d/-e per syncPanelMins).
Werte = die bisherigen var()-Fallbacks; die JS-Erkennung „hat der Nutzer
gezogen?" liest app.style (Inline) und bleibt davon unberührt. */
--col:360px;--drow:60%;--pmin-d:49px;--pmin-e:44px;
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}
/* Pad-Rahmen | interner Spiegel (D31). Eigener Bereich, damit die
Legenden-Aufteilung (D26) unangetastet bleibt. Nur in der Ansicht „beide"
bekommt der Spiegel eine feste Größe (`--pcol`/`--prow`) — sonst füllt er
wie bisher den Bereich. Die 85 %-Grenze steht zusätzlich hier im CSS, weil
der gespeicherte Wert ein px-Wert ist und der Bereich später schrumpfen
kann (dieselbe Falle wie bei der Legende, D26). */
.src-area{display:flex;align-items:stretch;flex:1 1 auto;min-width:0;min-height:0}
.padframe{flex:1 1 auto;min-width:0;min-height:0;border:0;background:var(--card)}
.padframe[hidden]{display:none}
/* Zeilennummern + Textfeld sind eine Spalte (`.src-wrap`). Der Pad-Splitter
teilt gegen diese Spalte — nicht gegen das nackte Textfeld, sonst stünde der
Zahlenstreifen außerhalb der geteilten Breite. */
.src-wrap{display:flex;align-items:stretch;flex:1 1 auto;min-width:0;min-height:0}
.src-area.pv-both>.src-wrap{
flex:0 0 var(--pcol,300px);max-width:85%;
border-left:1px dashed rgba(36,52,71,.18);
}
.src-area.pv-pad>.src-wrap{display:none}
.side .src-area,body.mobile .src-area{flex-direction:column}
.side .src-area.pv-both>.src-wrap,body.mobile .src-area.pv-both>.src-wrap{
flex:0 0 var(--prow,180px);max-width:none;max-height:85%;
border-left:0;border-top:1px dashed rgba(36,52,71,.18);
}
/* Legende: bewusst KEIN <details> mehr. Chrome legt den Details-Inhalt in
`::details-content`; dadurch war `.hint` kein Flex-Kind von `.agenda` mehr,
`flex:1/min-height:0` liefen ins Leere und der Inhalt wurde geclippt statt
scrollbar (D26). Als gewöhnlicher Container ist die Kette wieder dicht. */
.agenda{
flex:0 0 auto;
border-left:1px dashed rgba(36,52,71,.18);
display:flex;flex-direction:column;min-height:0;min-width:0;
}
/* Die Aufteilung steht als feste px-Größe. Schrumpft das Panel später (Zug am
großen Splitter, Drehung, Tastatur), begrenzt max-* die Legende, damit der
Editor nicht auf 0 gedrückt wird — gleiche Schranke wie beim Ziehen. */
.agenda.open{width:var(--hcol,300px);max-width:85%;overflow:hidden}
.agenda-summary{
flex:0 0 auto;
background:none;border:0;text-align:left;
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::before{content:'▸';display:inline-block;transition:transform .15s}
.agenda.open>.agenda-summary::before{transform:rotate(90deg)}
.agenda-summary:focus-visible{outline:2px solid var(--or);outline-offset:-2px}
.agenda:not(.open) .hint{display:none}
.agenda .hint{border-top:0;flex:1 1 auto;min-height:0;overflow:auto}
/* Splitter zwischen Editor und Legende — dieselbe Sprache wie der große
Splitter, nur schmaler. Ohne offene Legende gibt es nichts zu teilen. */
/* Neuheiten-Knopf im Diagramm-Kopf (D28): Sternchen + Anzahl, nur sichtbar,
wenn es etwas gibt. Gelb wie der Strahlenkranz am Knoten. */
.freshbtn{position:relative;color:#A16207;border-color:#FACC15;background:#FEFCE8}
.freshbtn:hover{color:#854D0E;border-color:#EAB308}
.freshbtn[hidden]{display:none}
.freshcount{
font-family:'IBM Plex Mono',monospace;font-size:.62rem;font-weight:600;
margin-left:3px;vertical-align:2px;
}
.hint-gutter{
flex:0 0 10px;align-self:stretch;
cursor:col-resize;touch-action:none;
display:flex;align-items:center;justify-content:center;
}
.hint-gutter[hidden]{display:none}
.hint-gutter::before{content:'';width:3px;height:38px;border-radius:2px;background:rgba(36,52,71,.18)}
.hint-gutter:hover::before,.hint-gutter.dragging::before{background:var(--or)}
.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;max-width:none;height:var(--hrow,220px);max-height:85%}
.side .hint-gutter{cursor:row-resize}
.side .hint-gutter::before{width:38px;height:3px}
/* Splitter Pad|Spiegel — gleiches Idiom wie der Legenden-Splitter. */
.pad-gutter{
flex:0 0 10px;align-self:stretch;
cursor:col-resize;touch-action:none;
display:flex;align-items:center;justify-content:center;
}
.pad-gutter[hidden]{display:none}
.pad-gutter::before{content:'';width:3px;height:38px;border-radius:2px;background:rgba(36,52,71,.18)}
.pad-gutter:hover::before,.pad-gutter.dragging::before{background:var(--or)}
.side .pad-gutter,body.mobile .pad-gutter{cursor:row-resize}
.side .pad-gutter::before,body.mobile .pad-gutter::before{width:38px;height:3px}
.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;position:relative;
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}
/* „im Pad bearbeiten" (D31) — nur bei einem ?etherpad=-Dokument sichtbar.
Das [hidden] braucht die eigene Regel, weil .copybtn ein display setzt. */
.padlink{text-decoration:none}
.padlink[hidden],.padrefresh[hidden],.padview[hidden]{display:none}
/* Der Ansichts-Wähler zeigt seinen Zustand als Position im Rahmen: geteilt,
links gefüllt (Pad) oder rechts gefüllt (Spiegel). Zwei Klassen, damit die
Regel die allgemeinere `.copybtn svg{display:block}` sicher schlägt. */
.copybtn.padview .ic-both,
.copybtn.padview .ic-pad,
.copybtn.padview .ic-text{display:none}
.copybtn.padview[data-view="both"] .ic-both,
.copybtn.padview[data-view="pad"] .ic-pad,
.copybtn.padview[data-view="text"] .ic-text{display:block}
/* Läuft ein Abruf, dreht das Symbol: bei gedrosselter Gegenseite (D31) können
das die vollen 20 s sein, und ein Knopf ohne Rückmeldung wirkt kaputt. */
/* Sperre nach einem Abruf (D31): sichtbar gedämpft, aber **klickbar** — der
Klick meldet den Grund, ein `disabled` erklärte nichts. */
.padrefresh.cooling{opacity:.45;cursor:not-allowed}
.padrefresh.busy{color:var(--or);border-color:var(--or);pointer-events:none}
.padrefresh.busy.cooling{opacity:1;cursor:default}
.padrefresh.busy svg{animation:padspin 1.1s linear infinite}
@keyframes padspin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
.padrefresh.busy svg{animation:none}
.padrefresh.busy{opacity:.55}
}
.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 */
/* Dokument-Wähler: der Name des aktiven Dokuments in der Editor-Titelzeile ist
zugleich der Auslöser eines Dropdowns zum Wechseln/Anlegen/Umbenennen/Löschen
(localStorage-Dokumente, noch kein Backend). */
.doctrigger{
display:inline-flex;align-items:center;gap:5px;cursor:pointer;
margin-right:auto;max-width:60%;color:var(--muted);
padding:2px 6px;margin-left:-6px;border-radius:6px;
text-transform:none;letter-spacing:0;font-family:'IBM Plex Mono',monospace;
}
.doctrigger:hover,.doctrigger[aria-expanded="true"]{color:var(--or)}
.doctrigger:focus-visible{outline:2px solid var(--or);outline-offset:2px}
.docname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
.doc-caret{width:13px;height:13px;flex:0 0 auto;opacity:.7}
.panel.collapsed .doc-caret{display:none}
.docmenu{
position:absolute;top:calc(100% + 4px);left:12px;z-index:50;min-width:210px;
background:var(--card);border:1px solid rgba(36,52,71,.15);border-radius:10px;
padding:6px;box-shadow:0 8px 24px rgba(36,52,71,.22);
text-transform:none;letter-spacing:0;font-family:'IBM Plex Sans',sans-serif;
}
.docmenu[hidden]{display:none}
.docmenu-list{display:flex;flex-direction:column;gap:2px;max-height:260px;overflow:auto}
.docitem{
display:flex;align-items:center;gap:8px;width:100%;
border:0;background:transparent;cursor:pointer;text-align:left;
padding:7px 9px;border-radius:7px;color:var(--ink);
font-family:inherit;font-size:.86rem;
}
.docitem:hover{background:rgba(15,118,110,.08)}
.docitem[aria-checked="true"]{color:var(--or);font-weight:600}
.docitem .doccheck{flex:0 0 auto;width:14px;text-align:center;opacity:0}
.docitem[aria-checked="true"] .doccheck{opacity:1}
.docitem.editing{cursor:default}
.docrename{
flex:1 1 auto;min-width:0;
border:1px solid var(--or);border-radius:6px;
padding:5px 7px;color:var(--ink);background:var(--card);
font-family:inherit;font-size:.86rem;outline:none;
}
.docmenu-actions{
display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;padding-top:6px;
border-top:1px solid rgba(36,52,71,.12);
}
.docact{
flex:1 1 auto;border:1px solid rgba(36,52,71,.2);border-radius:7px;
background:var(--card);color:var(--muted);cursor:pointer;
padding:5px 8px;font-family:inherit;font-size:.78rem;white-space:nowrap;
}
.docact:hover{border-color:var(--or);color:var(--or)}
.docdel:hover{border-color:#D897A8;color:#B4415E}
.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;
}
/* Pad-Dokument (D31): hier nur lesen, geschrieben wird im Pad. Abgesetzt über
den Hintergrund, damit der Schreibschutz nicht als hängendes Textfeld
erscheint — die Schrift bleibt aber Tinte: hier wird gelesen, der Plantext
ist der Hauptinhalt und kein ausgegrautes Formularfeld. */
textarea.readonly{background:#F1F4F7;cursor:default}
/* Zeilennummern (D33). Die Warnungen nennen Zeilennummern — ohne Streifen
muss man sie im Textfeld abzählen. Weich umbrochene Zeilen belegen mehrere
Bildzeilen, deshalb steht jede Zahl auf einer **gemessenen** Höhe
(`renderLineNos()` in app.js), nicht auf „Zeilenhöhe × n". Typografie muss
mit dem Textfeld übereinstimmen, sonst wandern die Zahlen weg. */
.lineno{
position:relative;overflow:hidden;flex:0 0 auto;
background:var(--card);border-right:1px solid rgba(36,52,71,.10);
font-family:'IBM Plex Mono',monospace;font-size:.85rem;line-height:1.7;
color:var(--muted);
-webkit-user-select:none;user-select:none;
}
.src-wrap.readonly .lineno{background:#F1F4F7}
.lineno-inner{position:absolute;top:0;left:0;right:0}
.lineno-inner span{
position:absolute;right:6px;
opacity:.5;font-variant-numeric:tabular-nums;
}
/* Cursor-Zeile und Zeilen mit Warnung heben sich ab — genau die beiden Fälle,
für die man im Streifen überhaupt nachsieht. */
.lineno-inner span.cur{opacity:1;color:var(--ink);font-weight:600}
.lineno-inner span.warn{opacity:1;color:var(--warn);font-weight:600}
.hint{
padding:12px 16px;border-top:1px dashed rgba(36,52,71,.18);
font-size:.8rem;color:var(--muted);line-height:1.7;
}
/* Bedienung statt Notation — abgesetzt, damit die Legende ihre Rolle behält. */
.hint-op{margin-top:8px;padding-top:8px;border-top:1px dashed rgba(36,52,71,.18)}
.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:6px;bottom:6px;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);
height:28px;
}
.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)}
.zoomToggle{display:none;padding:4px 8px}
.zoomToggle svg{width:16px;height:16px}
.zoomctl.collapsed .zoomToggle{display:block}
.zoomctl.collapsed .zoomcontrols{display:none}
.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)}
/* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge =
Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */
.node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)}
/* Knoten der Cursor-Zeile im Editor (D25). Weißer Halo + Tinte-Ring: hebt sich
von allen Pastell-Status UND vom dunklen Wurzelknoten ab und ist vom
petrolfarbenen Fokusring unterscheidbar. Rein visuelle Editierhilfe — nicht
im Druck. Der `#out`-Präfix ist nötig: `ul.or .node{box-shadow:none}`
(weiter unten) ist spezifischer als `.node.current` und schluckte den Ring
sonst überall unterhalb einer any-of-Gruppe. */
/* Die Fokusmarke `!!!` (SPEC §1) trägt bewusst DIESELBE Hervorhebung: Es gibt
nur einen Begriff „hier schauen", und ein dritter Ring neben Fokusring,
Strahlenkranz und Cursor-Ring wäre ein Zeichen zu viel. Der Unterschied
steht im `aria-label`. */
#out .node.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)}
/* Fokusmarke `!!!` (SPEC §1, D32): **Strahlenkranz in Petrol**, nach dem Muster
der Neuheiten-Markierung (D28), nur in der Akzentfarbe der App statt in Gelb.
Der geteilte Ring mit der Cursor-Zeile war zu leise — „das erkennt man zu
wenig" (Nutzer) —, und ein Ring allein wächst beim Zoomen nicht mit dem
Blickbedürfnis. Der Kranz löst zugleich die in D32 hingenommene Schwäche:
Cursor-Zeile (Tinte, innen) und fremde Marke (Petrol, außen) sind jetzt
unterscheidbar. Kein Blinken (WCAG 2.2.2/2.3.1, vgl. D28), und `z-index`,
weil der Schein AUSSERHALB der Knotenbox liegt — ohne ihn malt jedes später
kommende Geschwister seinen Hintergrund darüber und schneidet ihn ab. */
/* Der Schein ist **helles** Teal (#14B8A6), nicht das dunkle Petrol des Rings:
Ein dunkler Schein auf weißem Grund liest sich als Schatten, ein heller als
Licht — deshalb wirkte die erste Fassung leiser als der gelbe Kranz aus D28,
obwohl die Geometrie dieselbe war. Der Ring bleibt dunkles Petrol und ordnet
die Marke der Akzentfarbe zu; das Leuchten macht die Reichweite. */
#out .node.focusmark{box-shadow:0 0 0 3px var(--or),0 0 20px 7px rgba(20,184,166,.85);z-index:2}
/* Beides zugleich: Cursor-Zeile innen (Tinte), fremde Marke außen (Petrol). */
#out .node.focusmark.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink),0 0 20px 7px rgba(20,184,166,.85)}
/* Neu in Produktion seit dem letzten Besuch (D28): gelber Strahlenkranz um den
Knoten. Bewusst ein Schein nach AUSSEN statt Farbe im Knoten — die Füllung
gehört dem Status (SPEC §4) und bleibt lesbar. Kein Blinken: WCAG 2.2.2/2.3.1
und es zöge dauerhaft den Blick. `#out`-Präfix wegen `ul.or .node`. */
/* `z-index`, weil der Schein AUSSERHALB des Knotens liegt: ohne ihn malt jedes
später kommende Geschwister seinen undurchsichtigen Hintergrund darüber und
schneidet den Kranz einseitig ab (in engen Layouts bleibt nur eine gelbe
Kante stehen — sieht aus wie ein halber Hintergrund statt eines Rahmens). */
#out .node.fresh{box-shadow:0 0 0 2px #FACC15,0 0 16px 5px rgba(234,179,8,.65);z-index:2}
/* Beides zugleich: Cursor-Zeile innen (Tinte), Neuheit außen (gelb). */
#out .node.fresh.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink),0 0 18px 6px rgba(234,179,8,.65)}
/* Neu in Produktion UND jemand zeigt darauf: Ring in Petrol (der bewusste
Zuruf, innen), Kranz in Gelb (die stehende Nachricht, außen). Zwei Scheine
nach außen gingen nicht — einer muss der Ring sein. Steht bewusst **nach**
`.node.focusmark.current`: gleiche Spezifität (drei Klassen), und wenn alles
drei zutrifft, soll Zuruf + Neuheit gewinnen, nicht die eigene Cursor-Zeile. */
#out .node.fresh.focusmark{box-shadow:0 0 0 3px var(--or),0 0 20px 7px rgba(234,179,8,.65);z-index:2}
/* Alt gedrückt = Sprung-Geste scharf: Cursor an allen Knoten, Petrol-Ring am
Knoten unter dem Zeiger. Auf verlinkten Knoten ist das die einzige
Rückmeldung, weil dort der einfache Klick dem Link gehört (D25). */
#out.alt .node{cursor:alias}
#out.alt .node:hover{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--or)}
/* Langer Druck „scharf": derselbe Petrol-Ring wie unter dem Alt-Zeiger. */
#out .node.armed{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--or)}
/* Auf Touch ist der lange Druck die Sprung-Geste — die native Textauswahl und
das Link-Callout des Browsers starten zur selben Zeit und rissen sonst die
Auswahl im Textfeld wieder an sich (D25). */
@media (hover:none) and (pointer:coarse){
.node{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
}
.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;
}
.risk{
position:absolute;top:-10px;left:-10px;
width:18px;height:18px;
display:flex;align-items:center;justify-content:center;
font-size:.62rem;line-height:1;
background:var(--card);color:#F97316;
border:1.5px solid #F97316;border-radius:999px;
box-shadow:0 0 0 1.5px var(--card);
}
.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)}
/* „Neu in Produktion" darf die Pfad-Inversion nicht wegdimmen: opacity und
saturate treffen sonst auch den gelben Kranz (0,32 Deckkraft ⇒ praktisch
unsichtbar), und gerade bei einer nicht gewählten Alternative ist „das ist
jetzt live" die interessantere Nachricht (D28). */
.cheap-on .node:not(.cheap).fresh{opacity:1;filter:none}
/* Dasselbe für die Fokusmarke `!!!` (SPEC §1), und hier noch zwingender: Ein
Zeigefinger, der auf einen ausgeblassten Knoten zeigt, zeigt auf nichts —
und „sollten wir diese verworfene Alternative noch einmal ansehen?" ist einer
der wahrscheinlichsten Gründe, überhaupt zu zeigen. Anders als beim
optionalen Knoten (D29), wo das Zurücktreten die Aussage IST. */
.cheap-on .node:not(.cheap).focusmark{opacity:1;filter:none}
/* 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}
/* Der Stiel liegt bei `--stem-x` — normalerweise 50 % des <li>, was die
Knotenmitte ist, solange der Knoten im <li> zentriert steht. Bei
`li.has-or` ist er das NICHT (linksbündig, damit die any-of-Leiste unter
ihm aufsetzt) und der Teilbaum macht das <li> breiter als den Knoten: der
Stiel liefe daneben vorbei. `alignStems()` in app.js misst dort die
Knotenmitte und setzt `--stem-x` — D10 in der Waagerechten. */
ul.and>li::before,
ul.and>li::after{
content:'';position:absolute;top:0;
left:0;right:calc(100% - var(--stem-x, 50%));
border-top:2px solid var(--line);height:16px;
}
ul.and>li::after{left:var(--stem-x, 50%);right:0;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;
}
/* --- OPTIONAL (`+`, SPEC §3/D29): gestrichelter Abzweig + hohler Kreis ---
Gestrichelt wird NUR der Abzweig zum Knoten, nicht die Sammelleiste — die
gehört der ganzen Geschwistergruppe. Im Fächer ist der Abzweig der
senkrechte Stiel (border-left von ::after, beim letzten Kind border-right
von ::before), in den gestapelten Anordnungen der waagerechte Ast
(border-top von ::before); die Leiste ist jeweils die andere Kante.
Gestrichelt in TINTE — any-of ist gestrichelt in Grau (D15), die Farbe
hält beide auseinander, der Kreis unten bestätigt es. */
ul.and>li.opt::after{border-left-style:dashed}
ul.and>li.opt:last-child::before{border-right-style:dashed}
.tree.vertical ul.and>li.opt::before,
.tree.kompakt ul.and>li.opt::before,
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li.opt::before{
border-top-style:dashed;
}
/* Hohler Kreis am Abzweig ---
Kein dritter Linienstil: gestrichelt gehört any-of, und im kompakten Modus
codiert allein der Stil das Gate (D15). Stattdessen die FODA-Konvention —
gefüllter Punkt = erforderlich (hier: gar keiner), hohler Punkt = optional.
Der Kreis sitzt MITTIG AUF der Knotenkante, wo der Abzweig auftrifft.
Grundfall ist die gestapelte Anordnung (vertikal, kompakt, unter any-of):
Abzweig kommt von links auf halber Knotenhöhe. */
.node.opt::before{
content:'';position:absolute;
left:-5px;top:50%;transform:translateY(-50%);
width:10px;height:10px;border-radius:50%;
background:var(--card);border:2px solid var(--line);
box-sizing:border-box;
}
/* Horizontaler Fächer: der Abzweig kommt von oben mittig. */
.tree:not(.vertical):not(.kompakt) ul.and>li>.node.opt::before{
left:50%;top:-5px;transform:translateX(-50%);
}
/* …ausgenommen der gestapelte all-of-Teilbaum unter einer any-of-Gruppe
(siehe oben): dort kommt der Abzweig wieder von links. */
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li>.node.opt::before{
left:-5px;top:50%;transform:translateY(-50%);
}
/* --- TREPPE: aufeinanderfolgende optionale Endknoten (D29, Nachtrag 3) ---
Nur im horizontalen Fächer, wo jedes Geschwister sonst eine eigene Spalte
kostet. `applyOptStairs()` in app.js gruppiert sie in
`li.opt-group > ul.opt-stair` und nummeriert die Stufen als `--i` (0-basiert).
Geometrie: Stufe 0 hängt am Stiel von oben wie ein gewöhnliches Kind
(`--stem-x` zielt auf sie). Jede weitere Stufe steht `--stair-step` weiter
rechts und hängt an einem gestrichelten Winkel, der bündig an der linken
Kante der VORIGEN Stufe herabfällt und waagerecht in den eigenen Kreis
einbiegt. Das setzt voraus, dass die Zelle so hoch ist wie ihr Knoten —
deshalb nur Endknoten. */
.tree:not(.vertical):not(.kompakt) ul.opt-stair{
--stair-step:26px; --stair-gap:12px;
display:flex;flex-direction:column;align-items:flex-start;position:relative;
}
.tree:not(.vertical):not(.kompakt) ul.opt-stair>li{
align-items:flex-start;
padding:var(--stair-gap) 0 0 calc(var(--i) * var(--stair-step));
}
.tree:not(.vertical):not(.kompakt) ul.opt-stair>li:first-child{padding-top:0}
/* Winkel zur vorigen Stufe: senkrecht herab, dann waagerecht in den Knoten.
Der Knick liegt auf der linken Kante der vorigen Stufe. */
.tree:not(.vertical):not(.kompakt) ul.opt-stair>li+li::before,
.tree:not(.vertical):not(.kompakt) ul.opt-stair>li+li::after{
content:'';position:absolute;
left:calc((var(--i) - 1) * var(--stair-step));
}
.tree:not(.vertical):not(.kompakt) ul.opt-stair>li+li::after{
top:0;height:calc(var(--stair-gap) + 16px);
border-left:2px dashed var(--line);
}
.tree:not(.vertical):not(.kompakt) ul.opt-stair>li+li::before{
top:calc(var(--stair-gap) + 16px);width:var(--stair-step);
border-top:2px dashed var(--line);
}
/* Stufe 0 wird von oben getroffen (Kreis oben mittig), alle weiteren von
links — das ist bereits der Grundfall von `.node.opt::before`. */
.tree:not(.vertical):not(.kompakt) ul.opt-stair>li:first-child>.node.opt::before{
left:50%;top:-5px;transform:translateX(-50%);
}
/* ---------- 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-highrisk {background:#FFE5CC;border-color:#F97316;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-highrisk {background:#FFE5CC;border-color:#F97316}
.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;max-width:none;height:var(--hrow,220px);max-height:85%}
body.mobile .hint-gutter{cursor:row-resize}
body.mobile .hint-gutter::before{width:38px;height:3px}
/* 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);
}
/* ---------- Druck: nur das Diagramm (SPEC §9, TASKS Phase 2) ----------
Editor-Panel, Bedienelemente, Splitter, Kopfzeile, Warnungen und Footer
verschwinden; das Diagramm füllt die Seite und darf über mehrere Seiten
laufen. Statusfarben werden bewusst mitgedruckt (Browser strippen
Hintergründe sonst). Der Günstigster-Pfad-Overlay liegt in #out und wird
mitgedruckt. */
@media print{
html,body{height:auto!important;overflow:visible!important;background:#fff!important}
header,.panel.editor,.gutter,.panel.right .panel-head,
.warnings,.zoomctl,.site-footer{display:none!important}
.app,.app.side{display:block!important;max-width:none!important;width:auto!important;
height:auto!important;margin:0!important;gap:0!important}
.panel.right{grid-area:auto!important;background:#fff!important;border:0!important;
box-shadow:none!important;border-radius:0!important;
height:auto!important;min-height:0!important;overflow:visible!important}
.diagram{overflow:visible!important;height:auto!important;padding:8px 0!important}
/* Statusfarben & Pfad-Töne mitdrucken */
*{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
/* Knoten nicht über den Seitenrand zerschneiden */
#out li{break-inside:avoid}
/* Editierhilfe bzw. Zuruf, nicht drucken (D25, Fokusmarke SPEC §1) */
.node.current,.node.focusmark{box-shadow:none!important}
.node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */
@page{margin:12mm}
}