/*! 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} .src-area.pv-both>textarea{ flex:0 0 var(--pcol,300px);max-width:85%; border-left:1px dashed rgba(36,52,71,.18); } .src-area.pv-pad>textarea{display:none} .side .src-area,body.mobile .src-area{flex-direction:column} .side .src-area.pv-both>textarea,body.mobile .src-area.pv-both>textarea{ 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
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} .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. */ #out .node.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)} /* 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)} /* 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} /* 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
  • , was die Knotenmitte ist, solange der Knoten im
  • 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
  • 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} .node.current{box-shadow:none!important} /* Editierhilfe, nicht drucken (D25) */ .node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */ @page{margin:12mm} }