/*! 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)} /* Neuigkeiten (D58). Der Knopf steht permanent in der Kopfzeile; **bernstein** heißt „da ist etwas, das du noch nicht gesehen hast" — dieselben Töne, die der Strahlenkranz am Knoten trägt (D28), damit Knopf und Knoten dasselbe sagen. Petrol (`.active`) heißt etwas anderes: Es wird gerade ein bestimmter Tag im Diagramm vorgeführt. */ .newswrap{position:relative;display:inline-flex;flex:0 0 auto} .newsbtn{width:auto;min-width:30px;padding:0 6px;gap:2px} .newsbtn.unseen{color:#A16207;border-color:#FACC15;background:#FEFCE8} .newsbtn.unseen:hover{color:#854D0E;border-color:#EAB308} .newsbtn .freshcount[hidden]{display:none} .newsmenu{ position:absolute;top:calc(100% + 6px);right:0;z-index:60; width:min(420px,calc(100vw - 24px));max-height:min(70vh,520px);overflow:auto; background:var(--card);border:1px solid rgba(36,52,71,.15);border-radius:10px; padding:10px 12px;box-shadow:0 8px 24px rgba(36,52,71,.22); text-transform:none;letter-spacing:0;font-family:'IBM Plex Sans',sans-serif; text-align:left;color:var(--ink); } .newsmenu[hidden]{display:none} .newshead{ display:flex;align-items:center;gap:8px; font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted); } .newshead .newsclose{ margin-left:auto;border:0;background:transparent;cursor:pointer; color:var(--muted);font-size:1rem;line-height:1;padding:2px 4px;border-radius:6px; } .newshead .newsclose:hover{color:var(--ink);background:rgba(36,52,71,.07)} /* Fußnote zur Sprache der Notizen (D58-Nachtrag): klein, grau, ohne Rahmen und ohne Fläche — sie soll gelesen werden können, aber hinter dem Inhalt zurückstehen, zu dem sie gehört. */ .newsnote{margin-top:7px;color:var(--muted);font-size:.73rem;line-height:1.45} /* „Seit deinem letzten Besuch" (D28) — die persönliche Hälfte, deshalb bernstein abgesetzt und oben, vor der allgemeinen Chronik. */ .newssince{ display:flex;align-items:center;gap:8px;flex-wrap:wrap; margin-top:8px;padding:8px 10px;border-radius:8px; background:#FEFCE8;border:1px solid #FACC15;color:#854D0E;font-size:.82rem; } .newssince .newsseen{ margin-left:auto;border:1px solid rgba(133,77,14,.35);border-radius:7px; background:transparent;color:inherit;cursor:pointer; padding:3px 9px;font-family:inherit;font-size:.78rem; } .newssince .newsseen:hover{background:rgba(133,77,14,.1)} .newsday{margin-top:10px;padding-top:9px;border-top:1px solid rgba(36,52,71,.12)} .newsdate{font-weight:600;font-size:.84rem} .newslines{margin:5px 0 0;padding-left:17px;font-size:.82rem;line-height:1.55;color:var(--ink)} .newslines li{margin:1px 0} .newslines code{ font-family:'IBM Plex Mono',monospace;font-size:.92em; background:rgba(36,52,71,.07);border-radius:4px;padding:0 3px; } .newsshow{ margin-top:6px;border:1px solid rgba(36,52,71,.2);border-radius:7px; background:var(--card);color:var(--muted);cursor:pointer; padding:4px 9px;font-family:inherit;font-size:.78rem; } .newsshow:hover{border-color:var(--or);color:var(--or)} .newsshow[aria-pressed="true"]{background:var(--or);color:#fff;border-color:var(--or)} .newsempty{margin-top:8px;color:var(--muted);font-size:.82rem} /* 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 --col/--drow als Inline-Style per Splitter). Werte = die bisherigen var()-Fallbacks; die JS-Erkennung „hat der Nutzer gezogen?" liest app.style (Inline) und bleibt davon unberührt. (--pmin-d/-e hielten auf Mobil die beiden Titelzeilen frei — entfallen mit dem Ein-Bereich-Modell, D17-Nachtrag.) */ --col:360px;--drow:60%; 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
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. */ /* Die Zahl der neuen Knoten sitzt seit D58 am Neuigkeiten-Knopf in der Kopfzeile (die bernsteinfarbene Fassung steht dort bei `.newsbtn.unseen`). */ .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; /* gap:10 statt 14 — dieselbe Rechnung wie in D17-Nachtrag 5, nur für den Schreibtisch: Beim zehnten Bedienelement (D56) fehlten 10 px, und zehn Lücken à 14 px machen 140 px Weißraum für 566 px Knöpfe. Verkleinert wird der Zwischenraum, nicht das Ziel; 4 px weniger je Lücke geben die Breite des neuen Knopfes zurück. */ display:flex;justify-content:space-between;align-items:center;gap:10px; 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} /* `.ic-main` ist die neutrale Form von `.ic-copy` für Knöpfe, die nichts kopieren, aber dieselbe Erfolgs-Rückmeldung geben (Stand sichern, D54). */ .copybtn.done .ic-copy,.copybtn.done .ic-main{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} /* Falt-Durchschalter (SPEC §9/D75): vier Icons, sichtbar ist das des NÄCHSTEN Schritts — `data-next` setzt updateFoldBtn(). Zwei Klassen wie beim Pad-Wähler, damit die Regel `.copybtn svg{display:block}` schlägt. */ .copybtn.foldbtn .fc-small, .copybtn.foldbtn .fc-path, .copybtn.foldbtn .fc-closed, .copybtn.foldbtn .fc-open{display:none} .copybtn.foldbtn[data-next="small"] .fc-small, .copybtn.foldbtn[data-next="path"] .fc-path, .copybtn.foldbtn[data-next="closed"] .fc-closed, .copybtn.foldbtn[data-next="open"] .fc-open{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 */ .panetoggle{display:none} /* ebenso: dort gibt es nur einen Bereich */ /* Zurückschreiben der Faltung (D38-Nachtrag 2): Für die Dauer des SYNCHRONEN Schreibens braucht das Textfeld einen Kasten — ohne ihn tut `execCommand` nichts (gemessen: liefert `false`, obwohl `activeElement` das Feld meldet). Auf kleinem Bildschirm ist der Editor `display:none`, wenn das Diagramm vorn ist. Absolut aus dem Fluss genommen und aus dem Bild geschoben, damit der sichtbare Bereich seine Größe behält; gezeichnet wird davon nichts — derselbe Griff wie `exporting` im Grafikexport. */ body.writing-fold .panel.editor{ display:flex!important; position:absolute!important;left:-10000px!important;top:0!important; width:360px!important;height:400px!important; } /* 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). */ /* Wähler + Snapshot-Knopf als eine Gruppe: Sie trägt das `margin-right:auto` (schiebt die übrigen Knöpfe nach rechts) und ist der Positionsbezug des Snapshot-Menüs — dadurch klappt es unter seinem Knopf auf, ohne dass JS rechnen muss. Am Wähler selbst darf das auto-Margin nicht mehr stehen, sonst schöbe es den Snapshot-Knopf von ihm weg. */ .docgroup{ display:inline-flex;align-items:center;gap:4px; margin-right:auto;max-width:62%;min-width:0; } /* Bezug des Menüs ist der KNOPF, nicht die Gruppe: An der Gruppe ausgerichtet (`right:0`) begänne es 190 px weiter links und liefe aus dem Panel heraus — nachgemessen bei der ersten Fassung. */ .snapwrap{position:relative;display:inline-flex;flex:0 0 auto} .doctrigger{ display:inline-flex;align-items:center;gap:5px;cursor:pointer; min-width:0;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} /* Rückmeldung fürs stille In-Place-Speichern (Strg+S, D74): Petrol-Blitz samt Haken am Dokumentnamen — das Haus-Idiom von flashBtn (D54). */ .doctrigger.done{color:var(--or)} .doctrigger.done::before{content:"✓ ";flex:0 0 auto} .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; } /* Snapshot-Menü (D54): wie das Dokument-Menü, aber am rechten Rand seiner Gruppe ausgerichtet — also unter dem Knopf. */ .snapbtn{flex:0 0 auto} .snapmenu{ position:absolute;top:calc(100% + 6px);left:0;z-index:50;min-width:190px; max-height:280px;overflow:auto; display:flex;flex-direction:column;gap:2px; 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; } .snapmenu[hidden]{display:none} .snapitem{ display:flex;align-items:baseline;gap:8px;width:100%; border:0;background:transparent;cursor:pointer;text-align:left; padding:6px 9px;border-radius:7px;color:var(--ink); font-family:inherit;font-size:.84rem;white-space:nowrap; } .snapitem:hover{background:rgba(15,118,110,.08)} .snapitem .snapsize{margin-left:auto;color:var(--muted);font-size:.74rem} .snapempty{padding:7px 9px;color:var(--muted);font-size:.8rem} .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)} /* „Original wiederherstellen" (D22-Nachtrag): sichtbar, sobald ein mitgeliefertes Dokument aktiv ist; ausgegraut, solange es unverändert ist. */ .docact:disabled{opacity:.45;cursor:default} .docact:disabled:hover{border-color:rgba(36,52,71,.2);color:var(--muted)} .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} /* Kein Zeilenumbruch (D49, `wrap="off"` am Element): Die Einrückung trägt die Hierarchie, und ein weicher Umbruch setzt die Fortsetzung an den linken Rand — die Ebene wäre damit gerade dort nicht mehr abzulesen, wo eine Zeile lang ist. Lange Zeilen bekommen stattdessen einen waagerechten Balken. */ textarea{ display:block;width:100%;min-height:0; border:0;outline:none;resize:none;overflow:auto; 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. Jede Zahl steht auf einer **gemessenen** Höhe (`renderLineNos()` in app.js), nicht auf „Zeilenhöhe × n"; seit D49 bricht nichts mehr um, aber die Schriftgröße unterscheidet sich zwischen Telefon und Schreibtisch, und die Messung stimmt in beiden Fällen von selbst. Der Streifen scrollt **nur senkrecht** mit: Er verschiebt sich gegen `src.scrollTop`, den waagerechten Balken des Textfelds macht er nicht mit — die Zahlen bleiben stehen, wo sie hingehören. 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} /* Die Zahl einer Warn-Zeile trägt die Meldung als Tooltip (D33-Nachtrag). Dafür reicht ihr Kasten über die **ganze** Streifenbreite statt nur über die Ziffern: Ein zwei Zeichen breites Ziel trifft man sonst kaum. Die Ziffern selbst bleiben, wo sie sind (rechtsbündig, 6 px Abstand). */ /* Der Zeiger sagt, WAS hier steht: Pfeil mit **Warndreieck** statt des `?` von `cursor:help` — dasselbe ⚠ wie vor jeder Meldung im Warnungsbereich (D33-Nachtrag 2). Der Pfeil bleibt, sonst zeigte der Zeiger auf nichts; die Spitze liegt auf dem Brennpunkt (1 1). `help` ist der Rückfall, wo ein Bild-Zeiger nicht angenommen wird. */ .lineno-inner span.warn{ opacity:1;color:var(--warn);font-weight:600; left:0;right:0;padding-right:6px;text-align:right; cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cpath d='M1 1V17.6L5.3 13.5 8.3 20.2 11.2 18.9 8.2 12.4 13.8 12.2Z' fill='%23243447' stroke='%23fff' stroke-width='1.4' stroke-linejoin='round'/%3E%3Cpath d='M20.5 14 29.5 30 11.5 30Z' fill='%23B45309' stroke='%23fff' stroke-width='1.6' stroke-linejoin='round'/%3E%3Cpath d='M19.4 19.8h2.2v5.2h-2.2zM19.4 26.6h2.2v2.2h-2.2z' fill='%23fff'/%3E%3C/svg%3E") 1 1, help; } .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; /* Lange Labels brechen um, statt den Baum in die Breite zu treiben (D64). Die Umbrüche setzt der RENDERER (wrapLabel: ~40 Zeichen je Zeile, gleichmäßig verteilt) als `\n` — pre-line macht sie sichtbar, und der Kasten schrumpft auf die längste balancierte Zeile. `max-width` + `overflow-wrap` sind nur der Rückhalt für ein einzelnes Wort über der Grenze. Zentriert (Organigramm-Konvention). Die feste Zeilenhöhe bleibt — der 23-px-Abzweig (§9) trifft bei mehrzeiligen Knoten deterministisch die Mitte der ERSTEN Zeile. */ white-space:pre-line; max-width:32ch; text-align:center; overflow-wrap:break-word; 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)} /* Falt-Zeichen (SPEC §9, D38): ▾ offen / „▸ n" eingeklappt, Klickziel fürs Umklappen. Gedeckt in Grau — es ist Bedienelement, keine Statusaussage; beim Zeigen färbt es sich zur Tinte. */ /* Als gerahmter Chip statt bloßem Glyph (D64): das nackte ▾ war ~10px klein und kaum zu treffen. Die Höhe ist bewusst gedeckelt (feste line-height + kein vertikales Padding): Der Chip darf die Zeilenbox des Knotens NICHT über die festen 18,3px heben, sonst wandert die 23-px-Abzweig-Geometrie (§9) um Halbpixel. */ .node .fold{ cursor:pointer;color:var(--line); font-size:.75em;margin-right:6px; display:inline-block;padding:0 6px;line-height:14px; border:1px solid var(--muted);border-radius:5px; background:#fff; user-select:none;-webkit-user-select:none; } /* Deckendes Weiß plus Tinte-Glyphe: Die halbtransparente Füllung der ersten Fassung verschwand auf den Pastell-Statusfarben („weiß auf hell") — der Chip muss sich auf ALLEN acht Füllungen abheben, und Weiß mit sattem Rand tut das; auf dem weißen Neutral-Knoten trägt der Rand allein. */ .node .fold:hover{color:var(--ink);border-color:var(--line);background:#fff} /* Der dunkle Wurzelknoten braucht die helle Umkehrung. */ .root-node .fold{color:#fff;border-color:rgba(255,255,255,.7);background:rgba(255,255,255,.16)} .root-node .fold:hover{color:#fff;border-color:#fff;background:rgba(255,255,255,.26)} /* ”-Marke (SPEC §9, D40): der Knoten trägt eine Beschreibung — im Tooltip nachlesbar. Spiegelt das "-Zeichen der Notation; gedeckt, kein Klickziel. */ .node .desc-mark{color:var(--muted);margin-left:4px;font-weight:600} .root-node .desc-mark{color:rgba(255,255,255,.75)} /* Knoten-ID vor dem Titel (D56). Zurückgenommen und in der Mono-Schrift des Textfelds: Sie ist die Adresse, nicht der Name — der Titel soll die Zeile weiter anführen. Kein `margin-right`; das trennende Leerzeichen steht im Markup, damit der Grafikexport (er liest den Knotentext) es mitbekommt. */ .node .nid{ font-family:'IBM Plex Mono',monospace;font-size:.86em;font-weight:500; color:var(--muted); } .root-node .nid{color:rgba(255,255,255,.75)} /* Der Knopf trägt das Zeichen selbst statt eines Icons — in der Größe der SVGs daneben, damit die Kopfzeile ihre Höhe behält. */ .copybtn.idsbtn span{ display:block;width:15px;height:15px;line-height:15px;text-align:center; font-family:'IBM Plex Mono',monospace;font-size:14px;font-weight:600; } /* 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. */ /* Der Ring allein war zu leise (D25-Nachtrag): Er benutzt denselben Kanal, den schon jeder Knoten belegt — **jeder Status hat einen Rahmen** (SPEC §4). Ein weiterer Ring in einem Feld aus lauter gerahmten Kästchen ist ein Unterschied im Grad, nicht in der Art. Deshalb kommt **Tiefe** dazu: Schlagschatten plus leichte Vergrößerung. Das ist der einzige Kanal, den im Diagramm noch nichts belegt (nichts anderes wirft Schatten oder hebt sich), kollidiert also mit keiner Farbcodierung und wirkt über allen acht Statusfarben gleich. `transform` ändert **kein Layout** — die Linien bleiben stehen; und weil um die **Mitte** skaliert wird, bleibt auch die Knotenmitte unverändert, auf die `alignStems()` und `drawCheapPath()` messen. `z-index`, weil Schatten und vergrößerter Knoten AUSSERHALB der Box liegen (dasselbe wie bei `.fresh`). */ #out .node.current{ box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink),0 10px 20px -6px rgba(36,52,71,.5); transform:scale(1.04);z-index:3; } /* 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). Die Erhebung der Cursor-Zeile gilt in jeder Kombination — der Schlagschatten wird deshalb hier mit angehängt (das `transform` erbt die Regel oben). */ #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),0 10px 20px -6px rgba(36,52,71,.5);z-index:3} /* 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),0 10px 20px -6px rgba(36,52,71,.5);z-index:3} /* 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} /* Puls beim Zeilenwechsel (D25-Nachtrag): Man verliert den Knoten beim **Bewegen**, nicht im Stillstand — also meldet er sich genau dann einmal. Einmalig, kein Blinken: WCAG 2.2.2 zielt auf Blinkendes, 2.3.1 auf Flackern über 3 Hz; ein einzelner Durchlauf ist keins von beidem (vgl. D28). Ausgelöst wird er in `highlightCurrentNode()` **nur** bei geändertem `caretLine` — sonst pulste er bei jedem Tastendruck. Bewusst NICHT über `box-shadow` animiert: die Kombinationen mit `.fresh` und `.focusmark` haben je eigene Schatten-Listen, eine Animation darauf ließe den gelben bzw. petrolfarbenen Kranz für die Dauer des Pulses verschwinden. Stattdessen zwei Kanäle, die niemand sonst benutzt: ein kurzer Hüpfer im `transform` und ein auslaufender Ring auf dem freien `::after` (`::before` gehört dem Optional-Kreis, D29). */ @keyframes nodeLift{0%{transform:scale(1.04)}40%{transform:scale(1.11)}100%{transform:scale(1.04)}} @keyframes nodeRipple{from{transform:scale(1);opacity:.5}to{transform:scale(1.4);opacity:0}} #out .node.current.pulse{animation:nodeLift .36s ease-out 1} /* `both` ist Pflicht: ohne Fill-Mode schnappt der Ring am Ende auf die Anfangsdeckkraft zurück und bliebe stehen. */ #out .node.current.pulse::after{ content:'';position:absolute;inset:-3px;border-radius:calc(var(--r) + 3px); border:2px solid var(--ink);pointer-events:none; animation:nodeRipple .45s ease-out 1 both; } @media (prefers-reduced-motion: reduce){ #out .node.current.pulse{animation:none} #out .node.current.pulse::after{display:none} } /* Während `diagramToSvg()` misst: Erhebung und Puls abschalten — der Export zeichnet aus der Live-Geometrie nach, und die Vergrößerung ist eine Editierhilfe, keine Aussage über den Plan (D25). */ #out.exporting .node.current{transform:none;animation:none} #out.exporting .node.current::after{display:none} /* 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; } /* Diskrepanz-Marke des effektiven Status (SPEC §9, D39): die eigene Statusbox in den eigenen §4-Farben (nutzt die .chip-Farbregeln der Legende), unten links — die letzte freie Knoten-Ecke. */ .node .chip.ownst{ position:absolute;left:-9px;bottom:-9px; font-size:.58rem;font-weight:500;letter-spacing:.02em; padding:1px 5px;line-height:1.4;border-radius:999px; } .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); } /* Zuständigen-Engpass (SPEC §9/D71): die Pille der Person, die mehr als die Hälfte der offenen Pfad-Arbeit trägt — Warnfarbe wie das Größen-Konflikt-Badge (D62); der Export liest die gemessenen Farben. */ .tag.overload{background:var(--warn);color:#fff;border-color:var(--warn)} .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} /* Und dasselbe für die Cursor-Zeile (D25-Nachtrag) — die Ausnahme fehlte hier als einzige, obwohl der Fall der häufigste ist: Der Umschalter ist voreingestellt an, und jeder optionale Knoten und jede nicht gewählte Alternative ist „nicht auf dem Pfad" (im mitgelieferten Beispiel 7 von 18 Knoten). Gemessen auf `+ [?] Dark mode`: Deckkraft 0,32 — der Ring war dort praktisch unsichtbar. Wo der Cursor steht, muss sichtbar bleiben, egal wie der Pfad entschieden hat; anders als beim optionalen Knoten (D29), wo das Zurücktreten die Aussage IST, ist hier gar keine Aussage über den Plan gemeint — es ist eine Editierhilfe. */ .cheap-on .node:not(.cheap).current{opacity:1;filter:none} /* Erledigtes tritt NIE zurück (D46-Nachtrag). D46 hatte das schon entschieden — „sie auszublassen hieße, sie sähen aus wie eine verworfene Alternative" —, die Regel griff aber nur für erledigte Knoten AUF dem Pfad. Ein fertiger OPTIONALER Knoten (`+`, D29: nie auf dem Pfad) und eine fertige, nicht gewählte Alternative fielen durch das Raster: `#ed.fresh` ist gebaut und deployed, sah aber aus wie etwas, das nie passieren wird. Blassheit heißt jetzt einheitlich „hier ist nichts getan und wird auch nichts getan". */ .cheap-on .node:not(.cheap).done{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)} /* Größen-Konflikt (SPEC §5/D62): die Teilpakete übersteigen zusammen die angegebene Größe — das Badge wechselt auf die Warnfarbe, der weiße Trennrand zur Knotenkante bleibt. */ .size.conflict{background:var(--warn)} /* 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} /* Die VORDERE Ebene muss über JEDEN Knoten — auch über die, die sich selbst eine Stapelposition nehmen: Cursor-Zeile (`z-index:3`, D25), Strahlenkränze (`z-index:2`, D28/D32). Ohne das deckt ausgerechnet der hervorgehobene Knoten seinen eigenen Stationspunkt zu. „Vorn" heißt vorn. (Die Abhängigkeits-Kanten hatten aus demselben Grund einmal `.dep-front` mit `z-index:4`; sie liegen jetzt vollständig hinten — D41-Nachtrag 2.) */ .cheap-overlay.cheap-front{z-index:5} /* Querverbindungen der Abhängigkeiten (SPEC §9, D41): blassgrau GEPUNKTET und gekrümmt — Punktierung und Krümmung unterscheiden sie doppelt von den Baumlinien (durchgezogen/gestrichelt, orthogonal) und halten sie leise (D41-Nachtrag). Hervorgehobene Kanten (fokussierter Knoten/Cursor-Zeile) durchgezogen in Tinte. */ .dep-overlay{position:absolute;left:0;top:0;overflow:visible;pointer-events:none} .dep-edge{fill:none;stroke:var(--muted);stroke-width:1.5;opacity:.4; stroke-linecap:round;stroke-dasharray:.1 3} /* Offene Pfeilspitze — grau und dünn wie die Linie, kein gefülltes Dreieck. */ .dep-arrow{fill:none;stroke:var(--muted);stroke-width:1.5;opacity:.4; stroke-linecap:round;stroke-linejoin:round} .dep-edge.hl{stroke:var(--line);stroke-width:2;opacity:.9;stroke-dasharray:none} .dep-arrow.hl{stroke:var(--line);stroke-width:2;opacity:.9} .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} /* ---------- Knoten-Fenster für Touch (SPEC §6/§9, D52) ---------- Auf einem Zeigergerät zeigt der `title` den Tooltip; ohne Zeiger gibt es keinen. Der einfache Tipp öffnet deshalb dieses Fenster mit demselben Inhalt — und mit dem, was ein `title` nicht kann: Absatz, echte Trennlinie, Knöpfe. `position:fixed` (JS setzt left/top aus `getBoundingClientRect()`, die den `zoom` von #out schon enthält): Ein Kind von #out erbte dessen `zoom` und würde von dessen `overflow` beschnitten (D50). */ .nodetip{ position:fixed;z-index:60;max-width:min(21rem, calc(100vw - 24px)); background:var(--card);color:var(--ink); border:1px solid rgba(36,52,71,.15);border-radius:10px; box-shadow:0 8px 24px rgba(36,52,71,.3); padding:10px 30px 10px 12px;font-size:.78rem;line-height:1.45; } .nodetip[hidden]{display:none} .nodetip-x{ position:absolute;top:2px;right:2px;width:26px;height:26px; display:flex;align-items:center;justify-content:center; background:none;border:0;border-radius:6px;cursor:pointer; color:var(--muted);font-size:1.05rem;line-height:1; } .nodetip-x:hover{color:var(--ink);background:rgba(36,52,71,.06)} /* Absätze kommen aus Leerzeilen der Notation (§1); JS baut sie zu

. */ .nodetip-desc p + p{margin-top:.55em} /* Die echte Linie an der Stelle, an der der Tooltip 24 `─` zeichnen musste. */ .nodetip-facts{ margin-top:8px;padding-top:7px;border-top:1px solid rgba(36,52,71,.15); color:var(--muted);font-size:.72rem; } /* Ohne Beschreibung gibt es nichts zu trennen — der Strich stünde sonst als erste Zeile im Fenster. Dieselbe Regel wie im Tooltip (D40-Nachtrag). */ .nodetip-facts:first-child{margin-top:0;padding-top:0;border-top:0} .nodetip-link{ display:inline-flex;align-items:center;gap:5px;margin-top:9px; padding:5px 10px;border:1px solid var(--or);border-radius:7px; color:var(--or);text-decoration:none;font-size:.74rem;font-weight:500; } /* Die Spitze zeigt auf den Knoten — ohne sie wäre bei dicht stehenden Knoten nicht zu sehen, welcher gemeint ist. Ein gedrehtes Quadrat mit zwei Kanten: so erbt es Rahmen UND Schatten des Fensters. JS setzt --tipx (Spitze) und dreht sie per .above nach unten, wenn das Fenster über dem Knoten sitzt. */ .nodetip-arrow{ position:absolute;top:-6px;left:var(--tipx, 50%);width:11px;height:11px; margin-left:-5.5px;background:var(--card); border-left:1px solid rgba(36,52,71,.15); border-top:1px solid rgba(36,52,71,.15); transform:rotate(45deg); } .nodetip.above .nodetip-arrow{ top:auto;bottom:-6px; border:0;border-right:1px solid rgba(36,52,71,.15); border-bottom:1px solid rgba(36,52,71,.15); } /* Solange das Fenster offen ist, trägt sein Knoten einen Petrol-Ring — wie der „scharfe" Knoten beim langen Druck (D25). #out-Präfix nötig, weil `ul.or .node{box-shadow:none}` spezifischer ist. */ #out .node.tipped{box-shadow:0 0 0 3px rgba(15,118,110,.55)} /* ---------- ID-Vorschläge beim Tippen von Abhängigkeiten (D63) ---------- `position:fixed` auf wie das Knoten-Fenster: In einem Vorfahren mit `overflow` würde die Liste geklippt (D50). Die IDs in der Mono-Schrift des Textfelds, der Titel als gedämpfter Kontext daneben. */ .aclist{ position:fixed;z-index:70;min-width:160px;max-width:min(24rem, calc(100vw - 16px)); max-height:12.5rem;overflow-y:auto; background:var(--card);border:1px solid rgba(36,52,71,.15);border-radius:8px; box-shadow:0 8px 24px rgba(36,52,71,.25);padding:4px; } .aclist[hidden]{display:none} .acitem{ display:flex;align-items:baseline;gap:8px;padding:4px 8px;border-radius:5px; cursor:pointer;white-space:nowrap; } .acitem.sel,.acitem:hover{background:rgba(15,118,110,.12)} .acid{font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--ink)} .aclabel{ font-size:.7rem;color:var(--muted); overflow:hidden;text-overflow:ellipsis;max-width:14rem; } /* Visuell versteckt, für Screenreader da — die Live-Region der Vorschläge. */ .vh{ position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden; clip-path:inset(50%);white-space:nowrap; } /* --- 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: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} /* Einziges Kind: Normalerweise fallen der Stiel aus dem Elternknoten (`ul.and::before`, 50 % der Gruppe) und der Stiel zum Kind (`--stem-x`) zusammen — eine gerade Linie, keine Leiste nötig. Steht der Knoten aber nicht mittig in seiner Zelle (`li.has-or`, Treppe — dort misst `alignStems()` die Knotenmitte, D29-Nachtrag 2), liegen die beiden auseinander, und ohne Geschwister gibt es keine Sammelleiste, die sie verbände: die Linie riss ab (gemessen: 8,5 px). Deshalb hier ein kurzes Leiterstück genau zwischen den beiden — dieselbe L-Form, mit der ein äußeres Kind einer größeren Gruppe angeschlossen ist. Ohne `--stem-x` ist es null Pixel breit und damit unsichtbar. Diese Regeln müssen NACH den first/last-Regeln stehen: `li:only-child` ist beides, und bei gleicher Spezifität gewinnt die spätere. */ ul.and>li:only-child{padding-top:14px} ul.and>li:only-child::before{ border:0 none;border-top:2px solid var(--line);border-radius:0;height:14px; left:min(50%, var(--stem-x, 50%)); right:calc(100% - max(50%, var(--stem-x, 50%))); } ul.and>li:only-child::after{ border:0 none;border-left:2px solid var(--line);border-radius:0;height:14px; left:var(--stem-x, 50%);right: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); } /* XOR (`=`, SPEC §3/§9, D35): Geometrie kommt komplett von `ul.or` (der Renderer gibt `class="or xor"` aus); `.xor` ergänzt nur die „1"-Plakette am Austritt der Sammelleiste — „genau eine". Grau wie die any-of-Führung (Gate-Codierung, keine Signalfarbe, D15); sitzt mittig auf dem 14-px-Einlaufstück (left:9px = Leisten-x in allen Modi). */ ul.xor::after{ content:'1'; position:absolute;left:9px;top:7px;transform:translate(-50%,-50%); width:13px;height:13px;border-radius:50%; background:var(--card);border:1.5px solid var(--muted); box-sizing:border-box; color:var(--muted);font-size:9px;font-weight:600;line-height:10px; text-align:center; } 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; /* 18px statt 50%: Der gestapelte Abzweig sitzt fest bei 23px im
  • (= 5px Listenabstand + halbe EINZEILIGE Knotenhöhe). Seit Knoten umbrechen (D64), ist 50% bei mehrzeiligen nicht mehr diese Höhe — der Kreis muss auf dem Abzweig sitzen, nicht auf der Knotenmitte. Für einzeilige Knoten sind 18px und 50% dasselbe. */ left:-5px;top:18px;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:18px;transform:translateY(-50%); } /* Vertikal zentrierte all-of-Zwischenknoten (unten): dort trifft der Abzweig wirklich die Knotenmitte (50%) — der Kreis geht mit. */ .tree.vertical li.has-and>ul.and>li.has-and>.node.opt::before{top: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{--rail:9px;flex-direction:column;align-items:flex-start;padding-top:14px} .tree.vertical ul.and::before, .tree.kompakt ul.and::before{left:var(--rail);height:14px} .tree.vertical ul.and>li, .tree.kompakt ul.and>li{padding:5px 0 5px calc(var(--rail) + 21px)} .tree.vertical ul.and>li::before, .tree.kompakt ul.and>li::before{ left:var(--rail);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:var(--rail);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} /* Der Stub ist hier länger als die 9 px der gemeinsamen Basis: Das Größen-Badge (oben rechts) und die Tag-Pillen (unten rechts) ragen 8 px über die Knotenkante hinaus und standen mit 1 px Luft an der Sammelleiste — mit ihrem weißen Rand fraßen sie die Linie optisch auf. 20 px lassen 12 px Luft. Nur vertikal: kompakt führt all-of nach unten, dort treffen Badge und Leiste nie aufeinander. */ .tree.vertical li.has-and>ul.and{--rail:20px;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:var(--rail);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)} /* Einziges Kind: KEIN border:0 mehr (D65). Der Eltern-Stub dockt bei 50 % der Gruppenhöhe an, der Kind-Abzweig liegt fest bei 23 px — seit dem 20-px-Zusatzabstand unten (und erst recht bei mehrzeiligen Knoten, D64) fallen die beiden NICHT mehr zusammen. Das Verbindungsstück ist hier rein in CSS ausdrückbar, weil Gruppenhöhe == Kindhöhe (ul hat padding-top:0). */ .tree.vertical li.has-and>ul.and>li:only-child::after{top:23px;height:max(0px, calc(50% - 23px))} /* Letztes Kind mit großem Teilbaum (D65): Die Leiste endet am 23-px-Abzweig, der Eltern-Stub dockt aber bei 50 % der GRUPPE an — liegt der tiefer (großer Teilbaum im letzten Kind), hinge er in der Luft. `alignVRails()` in app.js misst das und verlängert die Leiste per --vrail-ext bis zum Stub. has-and-Kinder brauchen das nie (Abzweig bei 50 % der Zelle, und 50 % der Gruppe liegt beweisbar darüber). */ .tree.vertical li.has-and>ul.and>li:last-child:not(.has-and):not(:only-child)::after{height:var(--vrail-ext, 23px)} .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. Es ist **immer genau ein Bereich** zu sehen (Diagramm ODER Text); oben links schaltet ein Knopf um. Die Min/Normal/Max-Buttons und der Splitter entfallen hier ganz — es gibt nichts zu teilen. Die Legende bekommt einen eigenen Umschalter statt der dauerhaften „AGENDA“-Zeile. Siehe D17-Nachtrag. */ body.mobile .winbtns{display:none} /* Ein Bereich füllt die Fläche; der andere ist `display:none` (nicht bloß zusammengeschoben) — nur so bekommt der sichtbare die ganze Höhe, ohne dass eine zweite Titelzeile sie anknabbert. Die Kehrseite steht in app.js: Ein verborgenes Diagramm misst sich zu 0, deshalb wird die Geometrie beim Zurückschalten neu gezeichnet. */ body.mobile .app, body.mobile .app.side{ grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,1fr); grid-template-areas:"pane"; } body.mobile .gutter{display:none} body.mobile .panel.editor, body.mobile .panel.right{grid-area:pane} body.pane-diagram.mobile .panel.editor{display:none} body.pane-text.mobile .panel.right{display:none} /* Der Bereichstitel „DIAGRAMM" entfällt — welcher Bereich vorn ist, sagt der Inhalt; die Zeile wird für den Umschalter und die Aktionen gebraucht. */ body.mobile .panel.right .panel-head>span[data-i18n="diagramTitle"]{display:none} /* Umschalter oben links; er übernimmt vom entfallenen Titel das `margin-right:auto`, damit die Aktions-Knöpfe rechts stehen bleiben. Im Editor-Kopf tut das weiterhin der Dokumenten-Wähler daneben. */ body.mobile .panetoggle{display:inline-flex} body.mobile .panel.right .panetoggle{margin-right:auto} /* Die Titelzeile muss EINZEILIG bleiben (D17): Bricht sie um, wächst der Kopf auf das Doppelte und frisst die Fläche, für die es den Ein-Bereich-Modus überhaupt gibt. Die Knöpfe passen längst — es waren die Abstände: 14 px Lücke mal sieben plus 2×16 px Innenabstand sind mehr als ein Drittel von 375 px. Deshalb hier enger und `nowrap` als Riegel; die Knöpfe selbst bleiben unangetastet (Fingergröße). Mit dem neunten Bedienelement (Stationssprung, D47) noch einmal enger: 6 px Lücke und 8 px Innenabstand. Dazu `flex:0 0 auto` — ohne das schrumpft ausgerechnet der Modus-Wähler (das einzige Element ohne feste Größe) bei Platzmangel auf einen 2-px-Strich zusammen, während sein Icon darüber hinausragt. Ein zerdrücktes Bedienelement ist der schlechtere Fehler als ein sichtbarer Überlauf (D17-Nachtrag 5). */ body.mobile .panel-head{gap:6px;flex-wrap:nowrap;padding-inline:8px} body.mobile .panel-head>*{flex:0 0 auto} /* Unterhalb von rund 440 px ist es reine Arithmetik: **zehn** Bedienelemente à 29 px (D56 hat den ID-Umschalter hinzugefügt) plus Lücken brauchen 386 px, bei 375 px stehen 359 zur Verfügung. Kleiner als 29 px darf ein Fingerziel nicht werden (D17-Nachtrag 5) — dort darf die Zeile deshalb umbrechen. Sie tut es aber nur, wenn wirklich alle zehn da sind: Der „Was ist neu?"-Knopf (44 px) erscheint nur bei Neuigkeiten, ohne ihn bleiben 336 px und damit eine Reihe. Die Schwelle stand vorher bei 360 px und ist mit dem zehnten Element mitgewandert — dieselbe Rechnung, ein Element mehr. **Kein `overflow` an dieser Stelle** (D50): Die Kopfzeile trägt die beiden Aufklapp-Menüs (Dokumente, Download) als absolut positionierte Kinder. Ein Scroll-Container klippt sie — und weil `overflow-x` das `visible` der anderen Achse auf `auto` hebt, klippt er auch nach unten, wo die Menüs stehen. Sie waren damit auf **jeder** Mobilbreite unerreichbar, nicht nur auf den schmalen. */ @media (max-width:440px){ body.mobile .panel-head{flex-wrap:wrap} } /* Inhalte rund 25 % kleiner, damit mehr Plan auf die Fläche passt (D17-Nachtrag 2). Das Diagramm macht das über den CSS-`zoom` (Faktor auf den Nutzer-Zoom, siehe MOBILE_ZOOM in app.js); der Text über die Schriftgröße — `zoom` verbietet sich hier, weil Zeilennummern-Streifen und Spiegel am Textfeld messen (D33) und der Streifen seine Breite in `ch` rechnet. Beide müssen dieselbe Typografie tragen, sonst wandern die Zahlen weg; `line-height` ist einheitenlos und skaliert von selbst mit. */ body.mobile textarea, body.mobile .lineno{font-size:.64rem} body.mobile textarea{padding:10px 12px} /* 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} /* Das Neuigkeiten-Popup ist breiter als sein Knopf. Am Knopf ausgerichtet (`.newswrap{position:relative}`) begänne es auf dem Telefon bei −116 px, also außerhalb des Bildes — nachgemessen bei 375 px. Hier zählt deshalb die **Werkzeugleiste** als Bezug: Ihr rechter Rand ist auch der der Seite. Kein `overflow` an der Kopfzeile als Ausweg — das klippt die Menüs (D50). */ body.mobile .newswrap{position:static} 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,.nodetip,.aclist{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). Bei der Cursor-Zeile gehören Erhebung und Puls mit dazu — sonst stünde ein Knoten im Druck grundlos vergrößert und schief zu seiner Anschlusslinie. */ .node.current,.node.focusmark{box-shadow:none!important} .node.current{transform:none!important;animation:none!important} .node.current::after{display:none!important} .node.fresh{box-shadow:none!important} /* „neu seit dem letzten Besuch" ist persönlich (D28) */ .node.tipped{box-shadow:none!important} /* offenes Knoten-Fenster ist Bedienung (D52) */ /* Faltung (D38): das ▾ offener Knoten ist Bedienelement — nicht drucken; das „▸ n" eingeklappter Knoten ist Kennzeichnung und bleibt. */ .node:not(.folded) .fold{display:none!important} @page{margin:12mm} }