diff --git a/backend/guide.py b/backend/guide.py
index b618ec1..05c2fc2 100644
--- a/backend/guide.py
+++ b/backend/guide.py
@@ -169,6 +169,11 @@ def _mathe_auftraege(text: str, wo: str) -> list[str]:
if re.search(r"\\n(?![a-zA-Z])", text): # \neq etc. bleiben unberührt
auftraege.append(f"{wo}: literales „\\n“ im Text — durch echten"
f" Zeilenumbruch bzw. Leerzeile ersetzen.")
+ lange = [m for m in re.findall(r"\$([^$\n]+?)\$", text) if len(m) > 60]
+ if lange: # Inline-Formeln brechen nie um — auf schmalen Screens Überlauf
+ auftraege.append(f"{wo}: {len(lange)} überlange Inline-Formel(n)"
+ f" (>60 Zeichen, z. B. „{lange[0][:40]}…“): abgesetzt"
+ f" als $$…$$ setzen.")
formeln = ([{"tex": m, "display": True} for m in re.findall(r"\$\$([\s\S]+?)\$\$", text)]
+ [{"tex": m, "display": False} for m in re.findall(r"\$([^$\n]+?)\$", text)])
for f in _katex_fehler(formeln):
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index b0a331f..53c51ca 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -14,7 +14,10 @@ const neuName = ref('')
const neuArt = ref('thema')
const fehler = ref('')
const theme = ref(localStorage.getItem('theme') || 'auto')
-const navAuf = ref(localStorage.getItem('navAuf') !== '0')
+// mobil (≤768px) startet die Navigation eingeklappt, sofern nie manuell gesetzt
+const navAuf = ref(localStorage.getItem('navAuf') !== null
+ ? localStorage.getItem('navAuf') !== '0'
+ : window.innerWidth > 768)
function navToggle() {
navAuf.value = !navAuf.value
@@ -176,9 +179,11 @@ function runBadge(run) {
+
-
+
+
diff --git a/frontend/src/api.js b/frontend/src/api.js
index aa8d158..3cf902c 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -34,10 +34,18 @@ export const api = {
}
export function wsVerbinden(onEvent) {
- const url = (import.meta.env.DEV ? 'ws://localhost:8000' : `ws://${location.host}`) + '/ws'
+ // Protokoll an die Seite koppeln: HTTPS erlaubt nur wss:// (Mixed Content)
+ const wsProto = location.protocol === 'https:' ? 'wss' : 'ws'
+ const url = (import.meta.env.DEV ? 'ws://localhost:8000' : `${wsProto}://${location.host}`) + '/ws'
let ws
const verbinden = () => {
- ws = new WebSocket(url)
+ try {
+ ws = new WebSocket(url)
+ } catch (e) {
+ // nie den Aufrufer mitreißen (der SecurityError killte sonst den Poll)
+ console.error('WebSocket nicht verfügbar:', e)
+ return
+ }
ws.onopen = () => onEvent({ typ: 'verbunden' }) // nach Reconnect: Stand neu laden
ws.onmessage = (e) => onEvent(JSON.parse(e.data))
ws.onclose = () => setTimeout(verbinden, 2000)
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 1624902..4cd58bf 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -186,11 +186,12 @@ input, select {
}
body.vollbild .sidebar, body.vollbild .topbar { display: none; }
body.vollbild .nav-toggle { display: none; }
-/* Eingeklappt: Themen-Sidebar + Topbar weg — die Kapitelübersicht bleibt */
+/* Eingeklappt: Themen-Sidebar + Topbar weg — die Kapitelübersicht bleibt.
+ Der Inhalt rückt nach unten, damit das fixe ☰ nichts überlappt. */
body.nav-zu .sidebar, body.nav-zu .topbar { display: none; }
-body.nav-zu .guide-nav { padding-top: 44px; }
+body.nav-zu .hauptbereich { padding-top: 38px; }
.nav-toggle {
- position: fixed; top: 6px; left: 8px; z-index: 20;
+ position: fixed; top: 6px; left: 8px; z-index: 40; /* über den mobilen Drawern */
background: none; color: var(--dim); border: 0; padding: 4px 8px; font-size: 16px;
}
.nav-toggle:hover { color: var(--text); }
@@ -241,3 +242,32 @@ table.kennzahlen th, table.kennzahlen td {
}
table.kennzahlen th:first-child, table.kennzahlen td:first-child { text-align: left; }
table.kennzahlen th { color: var(--dim); }
+
+/* ── Mobil (≤768px): Navigationen als Drawer, Board mit Spalten-Snap ────────── */
+.markdown .katex { max-width: 100%; overflow-x: auto; overflow-y: hidden; }
+@media (max-width: 768px) {
+ .sidebar, .guide-nav {
+ position: fixed; top: 0; bottom: 0; left: 0; z-index: 30;
+ width: min(85vw, 320px); min-width: 0;
+ border-right: 1px solid var(--rand); box-shadow: 4px 0 24px rgba(0, 0, 0, 0.35);
+ background: var(--panel);
+ }
+ /* Guide-Tab: der Drawer zeigt die Kapitelliste, sonst die Themenliste */
+ body:has(.guide-nav) .sidebar { display: none; }
+ /* mobil versteckt ☰ nur die Drawer — die Topbar (Tabs!) bleibt sichtbar */
+ body.nav-zu .topbar { display: flex; }
+ body.nav-zu .hauptbereich { padding-top: 0; }
+ /* mobil heißt ☰-zu: ALLE Drawer weg — auch die Kapitelnav (Desktop behält sie) */
+ body.nav-zu .guide-nav { display: none; }
+ .sidebar .topics { padding-top: 44px; }
+ .guide-nav { padding-top: 44px; }
+ .topbar { padding-left: 40px; gap: 6px; }
+ .topbar .badge { display: none; }
+ .board { scroll-snap-type: x mandatory; padding: 8px; gap: 8px; }
+ .spalte { min-width: 86vw; max-width: 86vw; scroll-snap-align: start; }
+ .seite { padding: 12px; }
+ .seite.guide-text { max-width: 100%; padding: 14px 16px; }
+ .guide-section { padding: 12px 14px; }
+ .guide-text .kapitel-titel { margin-top: 20px; }
+ .flash { max-width: 100%; padding: 18px; margin: 16px auto; }
+}
diff --git a/templates/Guide-Writer.md b/templates/Guide-Writer.md
index 52f8657..2bed6ed 100644
--- a/templates/Guide-Writer.md
+++ b/templates/Guide-Writer.md
@@ -45,6 +45,8 @@ Regeln (hart):
Nur KaTeX-STANDARDBEFEHLE — kein LaTeX-Textsatz wie `\textsc`, `\usepackage`.
Ein Parse-Gate prüft jede Formel mit dem echten Renderer.
Hervorhebung im Text NUR als `**fett**` — nie einzelne `*…*`.
+ Formeln über ~50 Zeichen ABGESETZT als `$$…$$` — lange Inline-Formeln laufen
+ auf schmalen Bildschirmen aus dem Text.
PDF-Artefakte aus den Belegen in saubere Notation übersetzen: „u 6= v" ist
$u \neq v$, „V 0" ist $V'$ — du lehrst, du zitierst nicht.
5. LÄNGE: „lang" hat {min_woerter}–{max_woerter} Wörter. Die Obergrenze ist ein HARTES
diff --git a/tests/test_bausteine.py b/tests/test_bausteine.py
index 7faa9f9..839fbf8 100644
--- a/tests/test_bausteine.py
+++ b/tests/test_bausteine.py
@@ -203,3 +203,5 @@ def test_katex_gate_und_markdown_hygiene(monkeypatch):
assert "Beträge" in auftraege
assert "einzelnes „*“" in auftraege
assert "literales" in auftraege
+ lang_formel = "Es gilt $" + "x + ".join(["y"] * 30) + "$ hier."
+ assert "überlange Inline-Formel" in " ".join(guide._mathe_auftraege(lang_formel, "T"))