operations_research/web_04/assets/site.css
dschlueter 5d5bdb6508 Folium-Karten: die Blockade war eine falsche Alternative
Der einzige Punkt, der in diesem Projekt je als "blockiert" gefuehrt wurde,
ist erledigt. Die Begruendung lautete: Die VRP-Instanz sei ein synthetisches
100 x 100-km-Raster ohne Geokoordinaten, eine echte Karte verlange also
ENTWEDER eine neue Instanz (und damit zwei Dutzend neue abgedruckte Zahlen)
ODER eine willkuerliche Verankerung.

Es gibt einen dritten Weg, und er ist exakt. Eine azimutal-aequidistante
Projektion um den Schwerpunkt des Rasters bildet die 17 Punkte so auf Laenge
und Breite ab, dass die Grosskreisdistanzen mit den Rasterdistanzen
uebereinstimmen:

  festes cos(Breite)        Median 0,360 %   Maximum 2,003 %
  cos der eigenen Breite    Median 0,351 %   Maximum 0,908 %
  azimutal-aequidistant     Median 0,000 %   Maximum 0,001 %

Ein Meter auf hundert Kilometer. Kein abgedruckter Wert aendert sich, die
Gesamtstrecke bleibt bei 619 km, das SVG ist byte-identisch geblieben.

Zwei Befunde stuetzten die Entscheidung: "100 x 100 km Raster" steht nur in
zwei Codekommentaren, nirgends im Fliesstext; und der didaktische Punkt der
Bildunterschrift - die sich kreuzenden Touren, die KEIN Fehler sind - wird auf
einer Karte deutlicher.

erzeuge_vrp_touren.py schreibt jetzt SVG UND Karte aus einem Lauf. Ein zweites
Skript haette erneut geloest, und die Routing-Bibliothek arbeitet heuristisch.
pruefe_projektion() bricht ab, wenn die Verzerrung 0,01 % ueberschreitet - die
Zahl im Buch wird bei jedem Lauf bewiesen.

Neu im Build: die Marke {karte:name}, aufgeloest zu einem <iframe> (Leaflets
Stylesheet soll nicht in die Buchseite greifen), im PDF zum Verweissatz - wie
bei {plotly:}. --check bewacht jetzt beide Markenarten auf Existenz der Datei;
gegengeprueft mit einer absichtlich falschen Marke.

Zwei Vorkehrungen, die ohne Messung nicht aufgefallen waeren:

* Folium vergibt jedem Element eine uuid4 - jeder Lauf erzeugte eine andere
  Datei. normalisiere() ersetzt sie durch fortlaufende Nummern; zwei Laeufe
  liefern jetzt dieselbe Pruefsumme.
* Folium bindet ZEHN CDN-Ressourcen ein (jQuery, Bootstrap, Glyphicons,
  FontAwesome, awesome-markers ...). Gebraucht werden ZWEI. leaflet.js und
  leaflet.css liegen jetzt in web_04/assets/, die uebrigen acht entfallen.
  Nachgeprueft: Die Karte benutzt ausschliesslich L.*-Aufrufe. Ohne Netz
  fehlen nur die Kacheln, Marker und Touren bleiben sichtbar.

Gegenrechnung an vier Strecken: Depot -> K1 misst auf der Karte 27,20 km, die
Distanzmatrix sagt 27 - die Differenz stammt aus deren int(), nicht aus der
Geographie.

Veroeffentlicht: 8 Dateien uebertragen, alle dreizehn HTTP-Proben richtig, die
Karte auf dem Server unter 200 erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 20:38:16 +02:00

354 lines
18 KiB
CSS

/* site.css — Design-System der Mehrseiten-Website "Operations Research mit
Python". Struktur (CSS-Variablen, .dark-Umschaltung, kuehle Graustufen)
nach dem Vorbild https://jamulix.de/sorting/, Farben aus der bestehenden
Buchpalette (Titelseite/PDF: Indigo/Cyan/Violett/Gruen/Amber). */
:root {
--bg: #f5f6fa;
--surface: #ffffff;
--surface2: #eef0f5;
--border: #d9dce6;
--text: #1a1a2e;
--muted: #5c6380;
--accent: #4338ca;
--accent-text: #ffffff;
--cyan: #0891b2;
--violett: #7c3aed;
--emerald: #059669;
--amber: #b45309;
--rose: #e11d48;
--tint-indigo: #eef2ff;
--tint-violett: #f5f3ff;
--tint-cyan: #ecfeff;
--tint-emerald: #ecfdf5;
--tint-amber: #fffbeb;
--code-bg: #1e293b;
--code-text: #e6edf3;
--shadow: 0 2px 10px rgba(26, 26, 46, .07);
--shadow-lg: 0 8px 30px rgba(26, 26, 46, .12);
--radius: 12px;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0e0f1a;
--surface: #161826;
--surface2: #1e2133;
--border: #2b2f45;
--text: #e7e9f5;
--muted: #9aa1c4;
--accent: #818cf8;
--accent-text: #0e0f1a;
--cyan: #22d3ee;
--violett: #c4b5fd;
--emerald: #34d399;
--amber: #fbbf24;
--rose: #fb7185;
--tint-indigo: #1b1f3a;
--tint-violett: #241f3d;
--tint-cyan: #132833;
--tint-emerald: #10291f;
--tint-amber: #2c2410;
--code-bg: #0b0d14;
--code-text: #dbe4f0;
--shadow: 0 2px 10px rgba(0, 0, 0, .35);
--shadow-lg: 0 8px 30px rgba(0, 0, 0, .5);
}
}
:root[data-theme="dark"] {
--bg: #0e0f1a;
--surface: #161826;
--surface2: #1e2133;
--border: #2b2f45;
--text: #e7e9f5;
--muted: #9aa1c4;
--accent: #818cf8;
--accent-text: #0e0f1a;
--cyan: #22d3ee;
--violett: #c4b5fd;
--emerald: #34d399;
--amber: #fbbf24;
--rose: #fb7185;
--tint-indigo: #1b1f3a;
--tint-violett: #241f3d;
--tint-cyan: #132833;
--tint-emerald: #10291f;
--tint-amber: #2c2410;
--code-bg: #0b0d14;
--code-text: #dbe4f0;
--shadow: 0 2px 10px rgba(0, 0, 0, .35);
--shadow-lg: 0 8px 30px rgba(0, 0, 0, .5);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0; background: var(--bg); color: var(--text);
font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "DejaVu Sans", Arial, sans-serif;
line-height: 1.7; font-size: 16px; -webkit-text-size-adjust: 100%;
}
.icon { width: 1.15em; height: 1.15em; flex: none; vertical-align: -.2em; }
/* --- Kopfzeile --- */
.site-header {
position: sticky; top: 0; z-index: 30; display: flex; align-items: center;
gap: .75rem; padding: .6rem 1rem; background: var(--surface);
border-bottom: 1px solid var(--border); box-shadow: var(--shadow);
}
.icon-btn {
display: flex; align-items: center; justify-content: center;
width: 2.2rem; height: 2.2rem; border-radius: 8px; border: 1px solid transparent;
background: transparent; color: var(--text); cursor: pointer;
}
.icon-btn:hover { background: var(--surface2); border-color: var(--border); }
.brand {
display: flex; align-items: center; gap: .5rem; color: var(--accent);
font-weight: 700; text-decoration: none; white-space: nowrap;
}
.brand span { display: none; }
@media (min-width: 900px) { .brand span { display: inline; } }
.site-search { position: relative; flex: 1; max-width: 420px; margin: 0 auto; }
.site-search input {
width: 100%; padding: .5rem .8rem .5rem 2.2rem; border-radius: 20px;
border: 1px solid var(--border); background: var(--surface2); color: var(--text);
font-size: .92rem;
}
.site-search input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.such-icon { position: absolute; left: .7rem; top: 50%; transform: translateY(-50%); color: var(--muted); }
.suche-ergebnisse {
position: absolute; top: calc(100% + .4rem); left: 0; right: 0; max-height: 60vh;
overflow-y: auto; background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); box-shadow: var(--shadow-lg); z-index: 40;
}
.suche-ergebnisse a {
display: block; padding: .55rem .9rem; color: var(--text); text-decoration: none;
border-bottom: 1px solid var(--border);
}
.suche-ergebnisse a:last-child { border-bottom: none; }
.suche-ergebnisse a:hover, .suche-ergebnisse a.aktiv { background: var(--tint-indigo); }
.suche-ergebnisse small { display: block; color: var(--muted); font-weight: 400; }
.suche-leer { padding: .8rem .9rem; color: var(--muted); }
#theme-toggle .icon-moon { display: none; }
:root[data-theme="dark"] #theme-toggle .icon-sun { display: none; }
:root[data-theme="dark"] #theme-toggle .icon-moon { display: inline; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) #theme-toggle .icon-sun { display: none; }
:root:not([data-theme="light"]) #theme-toggle .icon-moon { display: inline; }
}
/* --- Layout: Sidebar + Inhalt --- */
.site-body { display: flex; align-items: flex-start; max-width: 1400px; margin: 0 auto; }
.sidebar {
position: sticky; top: 3.6rem; width: 290px; flex: none;
height: calc(100vh - 3.6rem); overflow-y: auto; border-right: 1px solid var(--border);
background: var(--surface); padding: 1rem 0;
}
.sidebar-gruppe { border-bottom: 1px solid var(--border); }
.sidebar-gruppe summary {
cursor: pointer; padding: .6rem 1rem; font-weight: 700; color: var(--accent);
list-style: none; display: flex; align-items: center; justify-content: space-between;
}
.sidebar-gruppe summary::-webkit-details-marker { display: none; }
.sidebar-gruppe summary::after { content: "▾"; color: var(--muted); font-size: .8em; }
.sidebar-gruppe[open] summary::after { content: "▴"; }
.sidebar ul { list-style: none; margin: 0; padding: 0 .5rem .5rem; }
.sidebar li a {
display: flex; align-items: center; gap: .5rem; padding: .4rem .6rem;
border-radius: 8px; color: var(--text); text-decoration: none; font-size: .92rem;
}
.sidebar li a:hover { background: var(--tint-indigo); }
.sidebar li.aktiv a { background: var(--accent); color: var(--accent-text); font-weight: 600; }
.fortschritt-haken { width: 1em; height: 1em; color: var(--emerald); visibility: hidden; }
.fortschritt-haken .icon { width: 100%; height: 100%; }
li[data-erledigt="1"] .fortschritt-haken { visibility: visible; }
.sidebar-extra { border-top: 2px solid var(--border); margin-top: .5rem; padding-top: .8rem !important; }
.sidebar-overlay { display: none; }
.content { flex: 1; min-width: 0; padding: 1.5rem 2rem 5rem; max-width: 900px; margin: 0 auto; }
/* --- Breadcrumb, Prev/Next --- */
.breadcrumb { font-size: .85rem; color: var(--muted); margin-bottom: .8rem; }
.breadcrumb a { color: var(--muted); }
.prev-next { display: flex; justify-content: space-between; gap: 1rem; margin: 1.2rem 0; }
.prev-next-knopf {
display: flex; align-items: center; gap: .6rem; flex: 1; max-width: 48%;
padding: .7rem 1rem; border: 1px solid var(--border); border-radius: var(--radius);
background: var(--surface); color: var(--text); text-decoration: none; font-size: .9rem;
box-shadow: var(--shadow); transition: border-color .15s, transform .15s;
}
.prev-next-knopf:hover { border-color: var(--accent); transform: translateY(-1px); }
.prev-next-next { margin-left: auto; text-align: right; flex-direction: row-reverse; }
.prev-next-knopf small { display: block; color: var(--muted); font-weight: 400; }
.prev-next-leer { flex: 1; max-width: 48%; }
.fortschritt-knopf {
display: inline-flex; align-items: center; gap: .5rem; margin: 1rem 0;
padding: .55rem 1rem; border-radius: 20px; border: 1px solid var(--border);
background: var(--surface2); color: var(--text); cursor: pointer; font: inherit; font-size: .88rem;
}
.fortschritt-knopf .icon { color: var(--muted); }
.fortschritt-knopf.erledigt { background: var(--tint-emerald); border-color: var(--emerald); color: var(--emerald); }
.fortschritt-knopf.erledigt .icon { color: var(--emerald); }
.site-footer { text-align: center; padding: 1.5rem; color: var(--muted); font-size: .85rem; border-top: 1px solid var(--border); }
.site-footer a { color: var(--muted); }
/* --- Typografie im Artikel (angelehnt an bilder/or_html_style.html) --- */
article h1, article h2, article h3, article h4, article h5 {
line-height: 1.25; font-weight: 700; margin-top: 2.1em; margin-bottom: .6em; scroll-margin-top: 4.5rem;
}
article h1 { color: var(--accent); font-size: 1.9rem; border-bottom: 3px solid var(--accent); padding-bottom: .25em; }
article h2 { color: var(--accent); font-size: 1.45rem; border-bottom: 2px solid var(--border); padding-bottom: .2em; }
article h3 { color: var(--cyan); font-size: 1.18rem; }
article h4 { color: var(--violett); font-size: 1.05rem; }
article h2[id$="-lernziele"]::before { content: "🎯 "; }
article h2[id$="-uebungsaufgaben"]::before { content: "📝 "; }
article h2[id$="-selbsttest"]::before { content: "✅ "; }
article h2[id$="-zusammenfassung"]::before { content: "📌 "; }
article a { color: var(--accent); text-decoration: none; }
article a:hover { text-decoration: underline; }
article a[target="_blank"]::after { content: " \2197"; font-size: .82em; color: var(--cyan); text-decoration: none; }
article p { margin: .75em 0; }
article hr { border: 0; border-top: 1px solid var(--border); margin: 2.2em 0; }
article figure { margin: 2em 0; text-align: center; }
article img { max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 10px; }
article figure img { box-shadow: var(--shadow-lg); }
article figcaption { color: var(--muted); font-size: .9rem; font-style: italic; margin-top: .6em; }
article table { border-collapse: collapse; width: 100%; margin: 1.4em 0; font-size: .94rem; box-shadow: var(--shadow); border-radius: 8px; overflow: hidden; }
article th { background: var(--accent); color: var(--accent-text); font-weight: 700; text-align: left; }
article th, article td { padding: .55em .75em; border: 1px solid var(--border); vertical-align: top; }
article tbody tr:nth-child(even) { background: var(--tint-indigo); }
article code { font-family: "DejaVu Sans Mono", Consolas, Menlo, monospace; font-size: .9em; background: var(--tint-violett); color: var(--violett); padding: .12em .4em; border-radius: 5px; }
article pre { background: var(--code-bg); color: var(--code-text); padding: 1.1em 1.2em; border-radius: 10px; overflow-x: auto; line-height: 1.5; font-size: .85rem; box-shadow: var(--shadow-lg); }
article pre code { background: none; color: inherit; padding: 0; font-size: inherit; }
article div.sourceCode { position: relative; }
article div.sourceCode pre.sourceCode { padding-top: 2.6em; }
article blockquote { border-left: 4px solid var(--violett); background: var(--tint-violett); margin: 1.4em 0; padding: .6em 1.2em; border-radius: 0 8px 8px 0; }
article ul, article ol { padding-left: 1.4em; }
article li { margin: .3em 0; }
.katex-display { overflow-x: auto; overflow-y: hidden; padding: .4em 0; }
/* --- Code-Buttons (Kopieren / Herunterladen) --- */
.code-btn-leiste { position: absolute; top: .55em; right: .55em; z-index: 2; display: flex; gap: .4em; }
.code-btn, .download-link {
display: inline-flex; align-items: center; gap: .35em; background: rgba(255,255,255,.08);
color: #cbd5e1; border: 1px solid rgba(255,255,255,.18); border-radius: 6px;
padding: .3em .65em; font: inherit; font-size: .78rem; cursor: pointer;
text-decoration: none !important; transition: background .15s, color .15s;
}
.code-btn:hover, .download-link:hover { background: rgba(255,255,255,.18); color: #fff; }
.code-btn.copied { background: var(--emerald); color: #fff; border-color: var(--emerald); }
.download-link { position: static; margin-left: .6em; }
/* --- Karten (Kapitel auf einen Blick, Lernpfade, Kapitelraster) --- */
.card { border-radius: var(--radius); padding: 1.1em 1.4em; margin: 1.6em 0; box-shadow: var(--shadow); }
.card p, .card blockquote { margin: .5em 0 !important; border: none !important; background: none !important; padding: 0 !important; }
.card-blick { background: var(--tint-indigo); border: 1px solid var(--accent); }
.card-blick blockquote { border-left: 3px solid var(--accent); padding-left: .8em !important; }
/* --- Didaktische Karten der Version 04 ---------------------------------
Jede Kartenart traegt eine eigene Leitfarbe aus der Buchpalette, damit
Kursteilnehmer beim Blaettern sofort erkennen, was sie vor sich haben:
gruen = sofort ausprobieren, cyan = Formel im Klartext, violett = Excel,
amber = Selbstkontrolle, rose = hier steckt ein Fehler drin. Die
Leitfarbe liegt links als 5px-Balken, damit sie auch im Ausdruck und in
der Druckvorschau erkennbar bleibt. */
.card-schnellstart,
.card-formel,
.card-excel,
.card-quiz,
.card-denkfehler { border: 1px solid var(--border); border-left-width: 5px; }
.card-schnellstart { background: var(--tint-emerald); border-left-color: var(--emerald); }
.card-formel { background: var(--tint-cyan); border-left-color: var(--cyan); }
.card-excel { background: var(--tint-violett); border-left-color: var(--violett); }
.card-quiz { background: var(--tint-amber); border-left-color: var(--amber); }
.card-denkfehler { background: var(--tint-amber); border-left-color: var(--rose); }
/* Die Karten enthalten haeufig Tabellen (Formel-Uebersetzer) und
Codebloecke (Schnellstart, Denkfehler). Beide sollen sich klar vom
getoenten Kartengrund abheben statt darin zu verschwimmen. */
.card-schnellstart pre,
.card-denkfehler pre { margin: .8em 0 !important; }
.card-formel table { margin: .6em 0; background: var(--surface); }
.card-formel td:first-child { white-space: nowrap; }
/* --- Interaktive Plotly-Figuren ---------------------------------------
Stehen im Text unmittelbar hinter der statischen Fassung (die im PDF
gedruckt wird). Auf schmalen Displays darf die Figur horizontal
scrollen, statt die Seite selbst breit zu machen. */
.plotly-figur { margin: 1.4em 0; background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: .6em; overflow-x: auto; }
.plotly-figur .plotly-graph-div { min-width: 320px; }
/* Leaflet-Karte im iframe ({karte:...}). Der Rahmen kommt von hier, nicht vom
iframe selbst - dessen Standardrahmen passt zu keinem der beiden Themen. */
.karte-figur { display: block; width: 100%; height: 30rem; margin: 1.4em 0;
border: 1px solid var(--border); border-radius: var(--radius);
background: var(--surface); }
@media (max-width: 640px) { .karte-figur { height: 22rem; } }
/* Der Formel-Uebersetzer ist zweispaltig gedacht: links die Mathematik,
rechts der Klartext. Auf schmalen Displays wuerde die Formelspalte die
Tabelle sonst aus dem Viewport druecken. */
@media (max-width: 640px) {
.card-formel td:first-child { white-space: normal; }
}
.kartenraster { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 1rem; margin: 1rem 0 2rem; }
.karte { display: block; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.1rem; box-shadow: var(--shadow); text-decoration: none; color: var(--text); }
.karte:hover { border-color: var(--accent); transform: translateY(-2px); transition: transform .15s, border-color .15s; }
.karte h3 { margin: .3em 0; color: var(--text); font-size: 1.05rem; }
.karte p { color: var(--muted); font-size: .88rem; margin: .4em 0; }
.karte-nummer { font-size: .78rem; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: .03em; }
.karte-zeit { font-size: .8rem; color: var(--muted); }
.karte-pfad { background: var(--tint-violett); border-color: var(--violett); }
.karte-einstieg { background: var(--tint-cyan); border-color: var(--cyan); }
.kapitelgruppe { margin: 2.5rem 0; }
.kapitelgruppe h2 { color: var(--accent); border-bottom: 2px solid var(--border); padding-bottom: .3em; }
.lernpfade h2 { color: var(--violett); }
/* --- Landing-Page Hero --- */
.hero { margin: 1rem 0 2.5rem; }
.hero-kicker { color: var(--cyan); letter-spacing: .12em; font-weight: 700; font-size: .85rem; }
.hero-text h1 { font-size: 2.1rem; color: var(--accent); margin: .2em 0; }
.hero-untertitel { color: var(--muted); font-size: 1.05rem; }
.hero-badges { display: flex; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
.hero-kompendium { color: var(--muted); margin-top: 1.4rem; max-width: 640px; }
.hero-anwendungen { color: var(--muted); margin-top: .8rem; max-width: 640px; }
.hero-autor { color: var(--muted); font-size: .92rem; line-height: 1.5; }
.badge { padding: .35em .9em; border-radius: 20px; color: #fff; font-weight: 600; font-size: .85rem; }
.badge-indigo { background: var(--accent); }
.badge-cyan { background: var(--cyan); }
.badge-violett { background: var(--violett); }
.badge-emerald { background: var(--emerald); }
/* --- Programme-Seite --- */
.programmgruppe { margin: 2rem 0; }
.programmliste { list-style: none; padding: 0; }
.programmliste li { display: flex; align-items: center; justify-content: space-between; padding: .5em .8em; border: 1px solid var(--border); border-radius: 8px; margin: .4em 0; background: var(--surface); }
/* --- Stichwortverzeichnis --- */
.register-sprungleiste { display: flex; flex-wrap: wrap; gap: .3rem; margin: 1.2rem 0 1.8rem; position: sticky; top: 3.6rem; background: var(--bg); padding: .5rem 0; z-index: 5; }
.register-sprungleiste a { display: inline-flex; align-items: center; justify-content: center; width: 1.8rem; height: 1.8rem; border-radius: 6px; background: var(--surface2); color: var(--accent); text-decoration: none; font-weight: 700; font-size: .85rem; }
.register-sprungleiste a:hover { background: var(--tint-indigo); }
.register-buchstabe { color: var(--accent); border-bottom: 2px solid var(--border); padding-bottom: .15em; margin-top: 2em; font-size: 1.3rem; }
.register-eintrag { padding: .55em 0; border-bottom: 1px solid var(--border); }
.register-begriff { font-weight: 700; }
.register-eintrag a { font-size: .92rem; }
.register-unter { list-style: none; margin: .4em 0 0; padding-left: 1.4em; }
.register-unter li { margin: .3em 0; color: var(--muted); font-size: .92rem; }
.register-unter a { font-size: inherit; }
/* --- Mobile --- */
@media (max-width: 899px) {
.sidebar {
position: fixed; top: 0; left: 0; height: 100vh; z-index: 50; transform: translateX(-105%);
transition: transform .2s ease; box-shadow: var(--shadow-lg); padding-top: 3.6rem;
}
.sidebar.offen { transform: translateX(0); }
.sidebar-overlay.offen { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 40; }
.content { padding: 1.2rem 1rem 4rem; }
.site-search { max-width: none; }
}