feat: Debug-Kasten minimierbar; Mobil-Inhalte ~25 % kleiner

Debug-Panel: Ein Klick entfernte es bisher ganz — was nichts half, weil der
15-Sekunden-Takt es sofort wieder aufbaute; auf dem Telefon verdeckte es damit
dauerhaft die untere rechte Ecke. Jetzt schaltet der Klick zwischen Kasten und
einem 26-px-Icon (⟳) unten rechts um. Der Zustand liegt im localStorage, nicht
am Element: Das Panel wird bei jedem Takt neu bespielt, ein Zustand am DOM-Knoten
wäre beim nächsten Tick weg. Über einen echten Intervall-Durchlauf geprüft.
Der aufgeklappte Kasten bekommt white-space:pre-wrap — die mit \n gefügten
Zeilen liefen bisher zu einem Absatz zusammen.

Mobil ~25 % kleiner: MOBILE_ZOOM=0.75 als Faktor AUF den Nutzer-Zoom, nicht als
neuer Anfangswert — sonst wäre die Verkleinerung nach dem ersten Zoomen weg und
„Zurücksetzen" führte auf eine Größe, die auf dem Telefon zu groß ist. Die
Anzeige nennt den effektiven Wert (75 %), sie soll beschreiben was man sieht.
Die drei Messstellen rechnen jetzt effZoom() zurück; nachgemessen bei 0,75
liegen alle fünf Stationspunkte exakt auf ihren Blattknoten (0 px).

Der Text kann kein zoom bekommen — Zeilennummern-Streifen und Spiegel messen am
Textfeld (D33) und die Streifenbreite rechnet in ch. Also font-size .64rem an
Textfeld und Streifen gemeinsam, Innenabstand im selben Verhältnis mit.
27 statt 19 Zeilen im Bild.

Dabei aufgefallen: Der Grafikexport war schon immer zoom-abhängig falsch — die
SVG-Schriftgrößen sind feste Zahlen, die Kästen gemessen. Fiel nie auf, weil
100 % der Normalfall war; mit dem Mobil-Faktor wäre es der Regelfall geworden.
diagramToSvg() stellt den Zoom für die Messung jetzt kurz auf 1 (Knoten auf dem
Schirm 143 px, im SVG 189,7 px — die unskalierte Größe).

Desktop unverändert nachgeprüft (Zoom 1, 100 %, 13,6 px). Prod-Build enthält das
Debug-Panel weiterhin gar nicht. SPEC §9, D17 (Nachtrag 2) und
frontend/CLAUDE.md nachgezogen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-23 07:50:34 +02:00
co-authored by Claude Fable 5
parent ec086eb526
commit 037db30d96
5 changed files with 135 additions and 29 deletions
+48
View File
@@ -243,6 +243,54 @@ Mobil, die Grid-Minima `--pmin-d`/`--pmin-e`, `syncPanelMins()`,
`setMobileDrow()` und das Titelzeilen-Tippen; Desktop behält Splitter, `setMobileDrow()` und das Titelzeilen-Tippen; Desktop behält Splitter,
Presets und Fenster-Buttons unverändert. Presets und Fenster-Buttons unverändert.
**Nachtrag 2 — auf Mobil sind die Inhalte grundsätzlich ~25 % kleiner, und der
Debug-Kasten minimiert sich statt zu verschwinden.**
**Verkleinerung als Faktor, nicht als neuer Anfangswert.** `MOBILE_ZOOM = 0.75`
multipliziert den Nutzer-Zoom (`effZoom()`), statt ihn beim ersten Start auf
0,75 zu setzen. Der Unterschied zählt: Ein Anfangswert wäre nach dem ersten
Zoomen weg, und „Zurücksetzen" führte zurück auf eine Größe, die auf dem
Telefon zu groß ist. Als Faktor bleibt die Verkleinerung eine Eigenschaft des
Geräts, und der Regler arbeitet relativ dazu weiter. Die Anzeige nennt den
**effektiven** Wert (75 % statt 100 %) — sie soll beschreiben, was man sieht,
nicht was man eingestellt hat.
**Die drei Messstellen mussten mit.** `alignStems()`, `drawCheapPath()` und
`drawDepLinks()` rechnen gemessene Pixel durch den CSS-`zoom` zurück; sie lesen
jetzt `effZoom()` statt `zoom`. Nachgemessen bei 0,75: alle fünf
Stationspunkte liegen exakt (0 px Abweichung) auf ihren Blattknoten.
**Der Text kann kein `zoom` bekommen.** Zeilennummern-Streifen und Spiegel
messen am Textfeld (D33), und der Streifen rechnet seine Breite in `ch` — beide
folgen der **Schriftgröße**, nicht einem Zoom auf einem Vorfahren. Also
`font-size: .64rem` (= 0,85 × 0,75) an Textfeld **und** Streifen gemeinsam;
`line-height` ist einheitenlos und skaliert mit. Der Innenabstand des
Textfelds schrumpft im selben Verhältnis mit (14/16 → 10/12 px), sonst wäre der
Rand auf 375 px unverhältnismäßig breit. Ergebnis: 27 statt 19 Zeilen im Bild.
**Dabei aufgefallen: der Grafikexport war schon immer zoom-abhängig falsch.**
Die Schriftgrößen im Ausgabe-SVG sind feste Zahlen (14 für Labels, 911 für
Badges), die Kästen kommen aus der Live-Messung — bei jedem Zoom ≠ 1 passten
Text und Kasten nicht zueinander. Das fiel nie auf, weil 100 % der Normalfall
war; mit der Mobil-Verkleinerung wäre es der Regelfall geworden. `diagramToSvg()`
stellt den Zoom für die Messung deshalb kurz auf 1 und danach zurück —
derselbe Griff wie bei der `exporting`-Klasse (D25-Nachtrag), und die Funktion
läuft synchron, es wird nichts davon gezeichnet. Nachgemessen: Knoten auf dem
Schirm 143 px, im SVG 189,7 px — also die unskalierte Größe, passend zur festen
Schrift.
**Das Debug-Panel minimiert sich jetzt, statt sich zu schließen.** Ein Klick
entfernte es bisher ganz — was nichts half, weil der 15-Sekunden-Takt es sofort
wieder aufbaute; auf dem Telefon verdeckte es damit dauerhaft die untere rechte
Ecke. Jetzt schaltet der Klick zwischen Kasten und einem 26-px-Icon (⟳) unten
rechts um. Der Zustand liegt im **localStorage**, nicht am Element: Das Panel
wird bei jedem Takt neu bespielt, ein Zustand am DOM-Knoten wäre also beim
nächsten Tick weg. Nachgeprüft über einen echten Intervall-Durchlauf und über
den `visibilitychange`-Pfad — es bleibt minimiert. (Test-Hilfe, im Prod-Build
ohnehin unterdrückt; die Gelegenheit genutzt, dem aufgeklappten Kasten
`white-space: pre-wrap` zu geben — die mit `\n` gefügten Zeilen liefen bisher
zu einem Absatz zusammen.)
## D18 — Günstigsten Pfad per Inversion zeigen, fehlende Größe = M ## D18 — Günstigsten Pfad per Inversion zeigen, fehlende Größe = M
Der kostengünstigste Weg durch den Baum wird hervorgehoben (Umschalter im Der kostengünstigste Weg durch den Baum wird hervorgehoben (Umschalter im
Diagramm-Kopf, Default an, Zustand persistiert). Nötig sind bei **all of** alle Diagramm-Kopf, Default an, Zustand persistiert). Nötig sind bei **all of** alle
+6 -1
View File
@@ -403,7 +403,12 @@ damit genau dann zu sehen, wenn der Text vorn ist. Die übrigen
umgeschaltet — jeder Bereich zeigt seine eigenen, in der unten beschriebenen umgeschaltet — jeder Bereich zeigt seine eigenen, in der unten beschriebenen
komprimierten Form. Der sichtbare Bereich wird gemerkt; ein Sprung zwischen komprimierten Form. Der sichtbare Bereich wird gemerkt; ein Sprung zwischen
Diagramm und Text (§9) holt den nötigen Bereich selbst nach vorn. Diagramm und Text (§9) holt den nötigen Bereich selbst nach vorn.
Voreinstellung ist das Diagramm. Die **Legende** bekommt hier einen eigenen Voreinstellung ist das Diagramm. Beide Inhalte werden hier zudem rund **25 %
kleiner** dargestellt, damit mehr Plan auf die Fläche passt; das ist ein Faktor
**auf** den Zoom, der Zoom-Regler arbeitet unverändert relativ dazu und zeigt
den effektiven Wert (auf dem Telefon also 75 %). Der **Grafikexport** ist davon
unberührt — er zeichnet immer die unskalierte Geometrie nach. Die **Legende**
bekommt hier einen eigenen
Umschalter im Editor-Kopf (statt der dauerhaften „AGENDA“-Zeile), damit sie Umschalter im Editor-Kopf (statt der dauerhaften „AGENDA“-Zeile), damit sie
zugeklappt keinen Platz kostet. Der **Modus-Wähler** zeigt nur das aktive Icon zugeklappt keinen Platz kostet. Der **Modus-Wähler** zeigt nur das aktive Icon
und schaltet bei jedem Tippen reihum weiter (horizontal → kompakt → vertikal → und schaltet bei jedem Tippen reihum weiter (horizontal → kompakt → vertikal →
+13 -1
View File
@@ -395,7 +395,19 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Bereich selbst um: `revealEditor()` auf Text, `focusNodeOfCaret()` aufs Bereich selbst um: `revealEditor()` auf Text, `focusNodeOfCaret()` aufs
Diagramm — **vor** dem Zentrieren. Zustand in `werkbaum-ui` (`mobilePane`), Diagramm — **vor** dem Zentrieren. Zustand in `werkbaum-ui` (`mobilePane`),
Variable oben bei `padView` deklariert (saveUI liest sie). Variable oben bei `padView` deklariert (saveUI liest sie).
`applyLayout` ruft auf Mobil **kein** `applySplit`. Dazu eigener Legenden-Umschalter `applyLayout` ruft auf Mobil **kein** `applySplit`.
**Inhalte ~25 % kleiner** (D17-Nachtrag 2): `MOBILE_ZOOM = 0.75` als **Faktor**
auf den Nutzer-Zoom — `effZoom()` ist die Wahrheit, die `applyZoom()` setzt und
die die drei Messstellen (`alignStems`, `drawCheapPath`, `drawDepLinks`)
zurückrechnen; wer dort wieder `zoom` einsetzt, verschiebt Linien und
Stationspunkte. `applyMobile()` ruft `applyZoom()` in **beiden** Zweigen, sonst
bliebe der Faktor beim Wechsel auf Desktop stehen. Der **Text** bekommt kein
`zoom`, sondern `font-size` (Streifen und Textfeld gemeinsam) — Spiegel und
`ch`-Breite hängen an der Schriftgröße (D33). **`diagramToSvg()` stellt den
Zoom für die Messung auf 1**: Die Schriftgrößen im Ausgabe-SVG sind feste
Zahlen, die Kästen gemessen — bei Zoom ≠ 1 passt sonst beides nicht zusammen
(galt schon vorher, wurde durch den Mobil-Faktor nur zum Regelfall).
Dazu eigener Legenden-Umschalter
(`#legendBtn`), schlanke Sprachwahl, Download-Overlay; Default Vollbild + (`#legendBtn`), schlanke Sprachwahl, Download-Overlay; Default Vollbild +
Diagramm maximiert. Layout-CSS hängt an `body.mobile`, nicht an einer eigenen Diagramm maximiert. Layout-CSS hängt an `body.mobile`, nicht an einer eigenen
`@media`-Regel — beide Seiten müssen denselben 640-px-Schwellwert nutzen `@media`-Regel — beide Seiten müssen denselben 640-px-Schwellwert nutzen
+58 -27
View File
@@ -259,7 +259,7 @@ function applyOptStairs(){
function alignStems(){ function alignStems(){
out.querySelectorAll('ul.and>li').forEach(li => li.style.removeProperty('--stem-x')); out.querySelectorAll('ul.and>li').forEach(li => li.style.removeProperty('--stem-x'));
if(out.classList.contains('vertical') || out.classList.contains('kompakt')) return; if(out.classList.contains('vertical') || out.classList.contains('kompakt')) return;
const z = zoom || 1; const z = effZoom() || 1;
/* `li.opt-group` (Treppe) hat keinen eigenen Knoten der Stiel zielt auf den /* `li.opt-group` (Treppe) hat keinen eigenen Knoten der Stiel zielt auf den
ERSTEN Knoten der Kaskade. */ ERSTEN Knoten der Kaskade. */
out.querySelectorAll('ul.and>li.has-or, ul.and>li.opt-group').forEach(li => { out.querySelectorAll('ul.and>li.has-or, ul.and>li.opt-group').forEach(li => {
@@ -277,7 +277,7 @@ function drawCheapPath(){
const leaves = [...out.querySelectorAll('.node.cheap-leaf')]; /* Dokument-Reihenfolge = Lese-Reihenfolge */ const leaves = [...out.querySelectorAll('.node.cheap-leaf')]; /* Dokument-Reihenfolge = Lese-Reihenfolge */
if(leaves.length < 2) return; if(leaves.length < 2) return;
const outRect = out.getBoundingClientRect(); const outRect = out.getBoundingClientRect();
const z = zoom || 1; const z = effZoom() || 1;
if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */ if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */
const pts = leaves.map(el => { const pts = leaves.map(el => {
const r = el.getBoundingClientRect(); const r = el.getBoundingClientRect();
@@ -366,7 +366,7 @@ function drawDepLinks(){
if(!edges.length) return; if(!edges.length) return;
const outRect = out.getBoundingClientRect(); const outRect = out.getBoundingClientRect();
if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */ if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */
const z = zoom || 1; const z = effZoom() || 1;
const rect = el => { const r = el.getBoundingClientRect(); const rect = el => { const r = el.getBoundingClientRect();
return {x:(r.left - outRect.left)/z, y:(r.top - outRect.top)/z, return {x:(r.left - outRect.left)/z, y:(r.top - outRect.top)/z,
w:r.width/z, h:r.height/z, w:r.width/z, h:r.height/z,
@@ -405,6 +405,15 @@ function diagramToSvg(){
Klasse statt durch Abnehmen von `.current`/`.pulse` so reißt der Export Klasse statt durch Abnehmen von `.current`/`.pulse` so reißt der Export
keine laufende Puls-Animation ab und startet sie hinterher nicht neu. */ keine laufende Puls-Animation ab und startet sie hinterher nicht neu. */
out.classList.add('exporting'); out.classList.add('exporting');
/* Zoom für die Messung auf 1 stellen. Die Schriftgrößen im Ausgabe-SVG sind
feste Zahlen (14 für Labels, 911 für Badges), die Kästen kommen dagegen
aus der Messung bei jedem Zoom 1 passten Text und Kasten also nicht
zueinander. Das fiel bisher kaum auf, weil 100 % der Normalfall war; mit
der Mobil-Verkleinerung (D17-Nachtrag 2) wäre es der Regelfall geworden.
Derselbe Griff wie bei `exporting`: kurz neutralisieren, hinterher zurück
die Funktion läuft synchron, es wird nichts davon gezeichnet. */
const zoomBefore = out.style.zoom;
out.style.zoom = 1;
const treeRect = out.getBoundingClientRect(); const treeRect = out.getBoundingClientRect();
const PAD = 24; const PAD = 24;
const W = Math.ceil(treeRect.width) + PAD*2; const W = Math.ceil(treeRect.width) + PAD*2;
@@ -581,6 +590,7 @@ function diagramToSvg(){
}); });
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="'IBM Plex Sans',system-ui,sans-serif">${parts.join('')}</svg>`; const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="'IBM Plex Sans',system-ui,sans-serif">${parts.join('')}</svg>`;
out.style.zoom = zoomBefore;
out.classList.remove('exporting'); out.classList.remove('exporting');
return {svg, W, H}; return {svg, W, H};
} }
@@ -1166,6 +1176,14 @@ const ZMIN = 0.3, ZMAX = 3, ZSTEP = 0.1;
const ZOOM_COLLAPSE_DELAY = 3000; /* 3 Sekunden */ const ZOOM_COLLAPSE_DELAY = 3000; /* 3 Sekunden */
let zoom = 1; let zoom = 1;
let zoomCollapseTimeout; let zoomCollapseTimeout;
/* Auf kleinem Bildschirm wird der Inhalt grundsätzlich verkleinert (~25 %),
damit mehr Plan auf die Fläche passt (D17-Nachtrag 2). Das ist ein Faktor
AUF den Nutzer-Zoom, kein neuer Anfangswert: Wer hineinzoomt, tut das
weiterhin relativ hierzu. Der Text bekommt dieselbe Verkleinerung über die
Schriftgröße (style.css) CSS-`zoom` verbietet sich dort, weil der
Zeilennummern-Streifen und der Spiegel am Textfeld messen (D33). */
const MOBILE_ZOOM = 0.75;
function effZoom(){ return zoom * (isMobile() ? MOBILE_ZOOM : 1); }
function resetZoomCollapseTimeout(){ function resetZoomCollapseTimeout(){
const zoomctl = document.querySelector('.zoomctl'); const zoomctl = document.querySelector('.zoomctl');
@@ -1179,8 +1197,11 @@ function resetZoomCollapseTimeout(){
function applyZoom(){ function applyZoom(){
zoom = Math.min(ZMAX, Math.max(ZMIN, Math.round(zoom * 100) / 100)); zoom = Math.min(ZMAX, Math.max(ZMIN, Math.round(zoom * 100) / 100));
out.style.zoom = zoom; out.style.zoom = effZoom();
document.getElementById('zoomReset').textContent = Math.round(zoom * 100) + ' %'; /* Angezeigt wird der EFFEKTIVE Wert die Anzeige soll beschreiben, was man
sieht. Auf Mobil steht dort also 75 %, und das erklärt die Verkleinerung,
statt sie als 100 %" zu behaupten. */
document.getElementById('zoomReset').textContent = Math.round(effZoom() * 100) + ' %';
resetZoomCollapseTimeout(); resetZoomCollapseTimeout();
saveUI(); saveUI();
} }
@@ -2959,6 +2980,7 @@ function applyMobile(){
ausgeblendet die Regeln hängen zwar an `body.mobile`, aber ein ausgeblendet die Regeln hängen zwar an `body.mobile`, aber ein
stehengebliebener Zustand ist beim nächsten Verkleinern verwirrend. */ stehengebliebener Zustand ist beim nächsten Verkleinern verwirrend. */
document.body.classList.remove('pane-diagram', 'pane-text'); document.body.classList.remove('pane-diagram', 'pane-text');
applyZoom(); /* Mobil-Faktor wieder herausrechnen (D17-Nachtrag 2) */
return; return;
} }
/* Genau ein Bereich (D17-Nachtrag): kein Desktop-Collapse, keine Aufteilung. /* Genau ein Bereich (D17-Nachtrag): kein Desktop-Collapse, keine Aufteilung.
@@ -2967,6 +2989,7 @@ function applyMobile(){
clearCollapse(); clearCollapse();
app.style.removeProperty('--drow'); app.style.removeProperty('--drow');
applyMobilePane(); applyMobilePane();
applyZoom(); /* Mobil-Faktor an-/abschalten (D17-Nachtrag 2) */
/* Default Vollbild auf kleinem Bildschirm — nur ohne gespeicherte Wahl. */ /* Default Vollbild auf kleinem Bildschirm — nur ohne gespeicherte Wahl. */
if(!hadStoredUI && !document.body.classList.contains('fullscreen')){ if(!hadStoredUI && !document.body.classList.contains('fullscreen')){
document.body.classList.add('fullscreen'); document.body.classList.add('fullscreen');
@@ -3127,36 +3150,44 @@ function logUpdate(msg){
localStorage.setItem('werkbaum-update-log', log.join('\n')); localStorage.setItem('werkbaum-update-log', log.join('\n'));
} }
/* Debug-Panel anzeigen (Test-Hilfe; im Prod-Build unterdrückt) */ /* Debug-Panel anzeigen (Test-Hilfe; im Prod-Build unterdrückt).
Ein Klick **minimiert** es auf ein Icon unten rechts, ein weiterer holt es
zurück. Vorher entfernte der Klick es ganz das half nichts, weil der
15-Sekunden-Takt es gleich wieder aufbaute; auf dem Telefon verdeckte es so
dauerhaft die untere rechte Ecke. Der Zustand liegt im localStorage, nicht am
Element: Das Panel wird bei jedem Takt neu bespielt und überlebt so auch ein
Neuladen. */
const LS_DEBUG_MIN = 'werkbaum-update-debug-min';
function paintUpdateDebug(panel){
const min = localStorage.getItem(LS_DEBUG_MIN) === '1';
panel.style.cssText =
'position:fixed;bottom:10px;right:10px;z-index:999;' +
'background:rgba(0,0,0,0.9);color:#0F766E;border:1px solid #0F766E;' +
'font-family:monospace;cursor:pointer;' +
(min
? 'width:26px;height:26px;padding:0;border-radius:50%;font-size:14px;' +
'display:flex;align-items:center;justify-content:center;overflow:hidden;'
: 'padding:12px;border-radius:4px;font-size:11px;max-width:240px;' +
'max-height:120px;overflow-y:auto;white-space:pre-wrap;');
panel.title = 'Update Debug Panel Klick zum ' + (min ? 'Aufklappen' : 'Minimieren');
const log = localStorage.getItem('werkbaum-update-log') || '';
panel.textContent = min ? '⟳'
: (log ? log.split('\n').slice(-6).join('\n') : 'Keine Einträge');
}
function showUpdateDebug(){ function showUpdateDebug(){
if(isProdBuild) return; if(isProdBuild) return;
let panel = document.getElementById('updateDebugPanel'); let panel = document.getElementById('updateDebugPanel');
if(!panel){ if(!panel){
panel = document.createElement('div'); panel = document.createElement('div');
panel.id = 'updateDebugPanel'; panel.id = 'updateDebugPanel';
panel.style.cssText = `
position: fixed;
bottom: 10px;
right: 10px;
background: rgba(0,0,0,0.9);
color: #0F766E;
padding: 12px;
border-radius: 4px;
font-size: 11px;
font-family: monospace;
max-width: 240px;
max-height: 120px;
overflow-y: auto;
z-index: 999;
border: 1px solid #0F766E;
cursor: pointer;
`;
panel.title = 'Update Debug Panel Klick zum Schließen';
document.body.appendChild(panel); document.body.appendChild(panel);
panel.addEventListener('click', () => panel.remove()); panel.addEventListener('click', () => {
const min = localStorage.getItem(LS_DEBUG_MIN) !== '1';
try{ localStorage.setItem(LS_DEBUG_MIN, min ? '1' : '0'); }catch(_){}
paintUpdateDebug(panel);
});
} }
const log = localStorage.getItem('werkbaum-update-log') || ''; paintUpdateDebug(panel);
panel.textContent = log ? log.split('\n').slice(-6).join('\n') : 'Keine Einträge';
} }
/* Reset-Button im Header neben Fullscreen (Test-Hilfe; im Prod-Build weggelassen) */ /* Reset-Button im Header neben Fullscreen (Test-Hilfe; im Prod-Build weggelassen) */
+10
View File
@@ -1078,6 +1078,16 @@
Editor-Kopf tut das weiterhin der Dokumenten-Wähler daneben. */ Editor-Kopf tut das weiterhin der Dokumenten-Wähler daneben. */
body.mobile .panetoggle{display:inline-flex} body.mobile .panetoggle{display:inline-flex}
body.mobile .panel.right .panetoggle{margin-right:auto} body.mobile .panel.right .panetoggle{margin-right:auto}
/* 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) */ /* Untertitel: Kurzfassung, garantiert einzeilig (Ellipsis als Sicherung) */
body.mobile .sub-full{display:none} body.mobile .sub-full{display:none}
body.mobile .sub-short{display:inline} body.mobile .sub-short{display:inline}