Branch3 #2

Merged
florian merged 8 commits from branch3 into main 2026-10-03 10:12:13 +00:00
17 changed files with 1355 additions and 0 deletions

No files matched your search

Vendored
BIN
View File
Binary file not shown.
+28
View File
@@ -0,0 +1,28 @@
&&&& &&&&&
+xX&&XX+++x
&&$x+xX++x+:;
&;;:$&;:;X&&
::::::::::;
&&$$$$$$$$$$$;::::x::+:;
&&XXX;::::::::::::::::::xxx
&&$x+x;::::::::::::::::::X&
&&X+++x;::::::::::::::::::X&
&&Xxxxx;::::::::::::::::::X&
&&&&&&+;::++;::;++;::;++:x&&
&$::&&X:;&&&&;:X&&::X&
&&xX&&$:;&&&&Xx&&&:;X&
&&&&&&&&& &&&& &&&&
██╗ ██╗███████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗██╗██╗ ██████╗ ██╗ ██╗███╗ ███╗
██║ ██║██╔════╝██╔════╝ ██╔═══██╗██╔════╝ ██╔══██╗██║ ██║██║██║ ██╔══██╗ ██║ ██║████╗ ████║
███████║█████╗ ██║ ███╗ ██║ ██║███████╗ ██████╔╝██║ ██║██║██║ ██║ ██║ ██║ ██║██╔████╔██║
██╔══██║██╔══╝ ██║ ██║ ██║ ██║╚════██║ ██╔══██╗██║ ██║██║██║ ██║ ██║ ╚██╗ ██╔╝██║╚██╔╝██║
██║ ██║██║ ╚██████╔╝███████╗╚██████╔╝███████║ ██████╔╝╚██████╔╝██║███████╗██████╔╝ ╚████╔╝ ██║ ╚═╝ ██║
╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═══╝ ╚═╝ ╚═╝
DEBIAN 13 VM auf w4d-gpu unterm Schreibtisch
Executable
+569
View File
@@ -0,0 +1,569 @@
#!/usr/bin/env python3
"""Mini-CMS: MD-Dateien in md/ per Browseroberflaeche bearbeiten
und als statische Seite (HTML + CSS) nach output/ exportieren.
Nur Python-Standardbibliothek, kein Internet noetig.
Aufruf:
python3 cms.py -> Server + UI starten (http://localhost:8500)
python3 cms.py export -> nur exportieren, kein Server
python3 cms.py port 9000 -> Server auf anderem Port
"""
import json
import os
import re
import shutil
import sys
import threading
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse
ROOT = os.path.dirname(os.path.abspath(__file__))
MD_DIR = os.path.join(ROOT, "md")
TPL_DIR = os.path.join(ROOT, "templates")
CSS_DIR = os.path.join(ROOT, "css")
OUT_DIR = os.path.join(ROOT, "output")
PORT = 8500
for d in (MD_DIR, TPL_DIR, CSS_DIR, OUT_DIR):
os.makedirs(d, exist_ok=True)
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$", re.IGNORECASE)
START_PAGE = "startseite"
HEADER_PAGE = "header" # Baustein: wird ueber den Inhalt jeder Seite gesetzt
FOOTER_PAGE = "footer" # Baustein: wird unter den Inhalt jeder Seite gesetzt
# ---------------------------------------------------------------- markdown
def md_to_html(text, headings=None):
"""Minimaler Markdown->HTML-Konverter:
Ueberschriften, **fett**, *kursiv*, `code`, [Links](url),
Listen, Code-Bloecke, > Zitate, --- Trennlinien, Absaetze.
Ueberschriften bekommen id-Anker; in headings (Liste) werden
optional (anker, text) aller Ueberschriften gesammelt.
"""
lines = text.replace("\r\n", "\n").split("\n")
out = []
i = 0
in_code = False
in_list = False
list_tag = None
used_ids = {}
def inline(s):
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
s = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", s)
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
return s
def close_list():
nonlocal in_list, list_tag
if in_list:
out.append("</%s>" % list_tag)
in_list = False
list_tag = None
while i < len(lines):
line = lines[i]
if line.strip().startswith("```"):
close_list()
if not in_code:
out.append("<pre><code>")
in_code = True
else:
out.append("</code></pre>")
in_code = False
i += 1
continue
if in_code:
out.append(_escape(line))
i += 1
continue
stripped = line.strip()
if not stripped:
close_list()
i += 1
continue
# Rohe HTML-Zeilen (z.B. <div>, <svg>) unveraendert durchreichen
if stripped.startswith("<"):
close_list()
out.append(stripped)
i += 1
continue
m = re.match(r"^(#{1,6})\s+(.*)$", stripped)
if m:
close_list()
level = len(m.group(1))
text_h = m.group(2)
hid = _slugify(text_h)
if hid in used_ids:
used_ids[hid] += 1
hid = "%s-%d" % (hid, used_ids[hid])
else:
used_ids[hid] = 0
if headings is not None:
headings.append((level, hid, text_h))
out.append('<h%d id="%s">%s</h%d>'
% (level, hid, inline(_escape(text_h)), level))
i += 1
continue
if re.match(r"^-{3,}$", stripped):
close_list()
out.append("<hr>")
i += 1
continue
m = re.match(r"^([-*+])\s+(.*)$", stripped)
if m:
if not in_list or list_tag != "ul":
close_list()
out.append("<ul>")
in_list, list_tag = True, "ul"
out.append("<li>%s</li>" % inline(_escape(m.group(2))))
i += 1
continue
m = re.match(r"^\d+\.\s+(.*)$", stripped)
if m:
if not in_list or list_tag != "ol":
close_list()
out.append("<ol>")
in_list, list_tag = True, "ol"
out.append("<li>%s</li>" % inline(_escape(m.group(1))))
i += 1
continue
if stripped.startswith(">"):
close_list()
quote = [stripped.lstrip("> ")]
while i + 1 < len(lines) and lines[i + 1].strip().startswith(">"):
i += 1
quote.append(lines[i].strip().lstrip("> "))
out.append("<blockquote><p>%s</p></blockquote>" % inline(_escape(" ".join(quote))))
i += 1
continue
close_list()
para = [stripped]
while (i + 1 < len(lines) and lines[i + 1].strip()
and not re.match(r"^(#{1,6}\s|[-*+]\s|\d+\.\s|>|```|-{3,}$|<)",
lines[i + 1].strip())):
i += 1
para.append(lines[i].strip())
out.append("<p>%s</p>" % inline(_escape(" ".join(para))))
i += 1
close_list()
return "\n".join(out)
def _escape(s):
return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
def _slugify(s):
"""Ueberschriftentext -> HTML-id (ankertauglich, klein, ohne Sonderzeichen)."""
s = s.lower()
for k, v in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
s = s.replace(k, v)
s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")
return s or "abschnitt"
def heading_nav(headings):
"""Baut die Anker-Navigation als verschachtelte ul/li-Liste:
Eine Ueberschrift mit einem # mehr als die vorherige wird als
Unterpunkt eingerueckt.
headings: Liste von (level, anker, text).
"""
if not headings:
return ""
parts = []
levels = []
for level, hid, text in headings:
if not levels:
# erste Ueberschrift: aeusserste Liste oeffnen
parts.append("<ul>")
levels.append(level)
elif level > levels[-1]:
# tiefer -> neue verschachtelte Liste im letzten <li>
parts.append("<ul>")
levels.append(level)
else:
# gleiche oder hoehere Ebene -> Listen schliessen bis Ebene passt
while len(levels) > 1 and levels[-1] > level:
parts.append("</li></ul>")
levels.pop()
parts.append("</li>")
parts.append('<li><a href="#%s">%s</a>' % (hid, _escape(text)))
parts.append("</li>")
while len(levels) > 1:
parts.append("</ul></li>")
levels.pop()
parts.append("</ul>")
return "".join(parts)
# ------------------------------------------------------------------- seiten
def parse_md(raw):
"""Trennt Front-Matter (titel: ...) vom Inhalt."""
meta = {}
body = raw
if raw.startswith("---"):
parts = raw.split("---", 2)
if len(parts) >= 3:
for line in parts[1].strip().splitlines():
if ":" in line:
k, v = line.split(":", 1)
meta[k.strip().lower()] = v.strip()
body = parts[2].lstrip("\n")
return meta, body
def md_path(slug):
return os.path.join(MD_DIR, slug + ".md")
def list_pages():
pages = []
for f in sorted(os.listdir(MD_DIR)):
if f.endswith(".md") and f[:-3] not in (HEADER_PAGE, FOOTER_PAGE):
pages.append(f[:-3])
return pages
def load_page(slug):
with open(md_path(slug), encoding="utf-8") as fh:
return fh.read()
def save_page(slug, raw):
with open(md_path(slug), "w", encoding="utf-8") as fh:
fh.write(raw)
def delete_page(slug):
p = md_path(slug)
if os.path.exists(p):
os.remove(p)
# ------------------------------------------------------------------ export
def block_html(slug):
"""Rendert einen Baustein (header/footer) als HTML, leer wenn nicht vorhanden."""
if os.path.exists(md_path(slug)):
_, body = parse_md(load_page(slug))
return md_to_html(body)
return ""
def render_page(raw, template, header_html="", footer_html=""):
meta, body = parse_md(raw)
headings = []
html = md_to_html(body, headings)
anker_html = heading_nav(headings)
page = (template
.replace("{{titel}}", _escape(meta.get("titel", "")))
.replace("{{inhalt}}", html))
if "{{anker}}" in template:
page = page.replace("{{anker}}", anker_html)
elif anker_html and "{{header}}" in template:
# Template ohne {{anker}}-Platzhalter: Anker ans Ende des Headers
page = page.replace("{{header}}",
header_html + '\n<nav class="anker-nav">' + anker_html + "</nav>")
if "{{header}}" in template:
page = page.replace("{{header}}", header_html)
elif header_html and "<body>" in page:
# Template ohne {{header}}-Platzhalter: Baustein automatisch setzen
idx = page.index("<body>") + len("<body>")
page = page[:idx] + "\n<header>\n" + header_html + "\n</header>\n" + page[idx:]
if "{{footer}}" in template:
page = page.replace("{{footer}}", footer_html)
elif footer_html and "</body>" in page:
# Template ohne {{footer}}-Platzhalter: Baustein automatisch anhaengen
idx = page.rindex("</body>")
page = page[:idx] + "<footer>\n" + footer_html + "\n</footer>\n" + page[idx:]
return page, meta.get("titel", "")
def load_template():
files = [f for f in os.listdir(TPL_DIR) if f.endswith(".html")]
if not files:
raise SystemExit("Kein Template in templates/ gefunden (z.B. default.html).")
with open(os.path.join(TPL_DIR, files[0]), encoding="utf-8") as fh:
return fh.read()
def export_site():
"""Baut output/: pro MD-Datei eine HTML-Seite, CSS wird kopiert."""
template = load_template()
# Output leeren und neu aufbauen
if os.path.isdir(OUT_DIR):
shutil.rmtree(OUT_DIR)
os.makedirs(os.path.join(OUT_DIR, "css"), exist_ok=True)
for f in os.listdir(CSS_DIR):
if f.endswith(".css"):
shutil.copy2(os.path.join(CSS_DIR, f), os.path.join(OUT_DIR, "css", f))
footer_html = block_html(FOOTER_PAGE)
header_html = block_html(HEADER_PAGE)
exported = []
for slug in list_pages():
page, _ = render_page(load_page(slug), template, header_html, footer_html)
# startseite.md wird zur index.html
out_name = "index.html" if slug == START_PAGE else slug + ".html"
with open(os.path.join(OUT_DIR, out_name), "w", encoding="utf-8") as fh:
fh.write(page)
exported.append(out_name)
return exported
# ------------------------------------------------------------------- server
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def _send(self, code, body, ctype="application/json; charset=utf-8"):
data = body.encode("utf-8") if isinstance(body, str) else body
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _json(self, obj, code=200):
self._send(code, json.dumps(obj))
def _body(self):
n = int(self.headers.get("Content-Length", 0))
return json.loads(self.rfile.read(n).decode("utf-8")) if n else {}
def do_GET(self):
path = urlparse(self.path).path
if path in ("/", "/admin"):
self._send(200, ADMIN_HTML, "text/html; charset=utf-8")
elif path == "/api/pages":
self._json({"pages": list_pages(), "start": START_PAGE,
"header": os.path.exists(md_path(HEADER_PAGE)),
"footer": os.path.exists(md_path(FOOTER_PAGE))})
elif path.startswith("/api/page/"):
slug = unquote(path[len("/api/page/"):])
if slug in (HEADER_PAGE, FOOTER_PAGE) or slug in list_pages():
self._json({"slug": slug, "raw": load_page(slug)})
else:
self._json({"error": "Seite nicht gefunden"}, 404)
elif path == "/api/export":
self._json({"exported": export_site()})
else:
self._json({"error": "unbekannter Pfad"}, 404)
def do_POST(self):
path = urlparse(self.path).path
try:
data = self._body()
except (ValueError, json.JSONDecodeError):
return self._json({"error": "ungueltige Anfrage"}, 400)
if path == "/api/page":
slug = (data.get("slug") or "").strip()
raw = data.get("raw") or ""
if not SLUG_RE.match(slug):
return self._json({"error": "Ungueltiger Name (a-z, 0-9, -)."}, 400)
save_page(slug, raw)
self._json({"ok": True, "slug": slug})
elif path == "/api/delete":
slug = data.get("slug", "")
if slug not in (HEADER_PAGE, FOOTER_PAGE) and slug not in list_pages():
return self._json({"error": "Seite nicht gefunden"}, 404)
if slug in (START_PAGE, HEADER_PAGE, FOOTER_PAGE):
return self._json({"error": "Startseite und Bausteine koennen nicht geloescht werden."}, 400)
delete_page(slug)
self._json({"ok": True})
elif path == "/api/preview":
meta, body = parse_md(data.get("raw") or "")
self._json({"html": md_to_html(body)})
else:
self._json({"error": "unbekannter Pfad"}, 404)
ADMIN_HTML = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Mini-CMS</title>
<style>
body { font-family: -apple-system, "Segoe UI", sans-serif; margin: 0; display: flex; height: 100vh; }
#side { width: 220px; background: #f2f2f2; padding: 1rem; box-sizing: border-box; }
#side h1 { font-size: 1rem; }
#pages a { display: block; padding: .35rem .5rem; border-radius: 4px; cursor: pointer; text-decoration: none; color: #000; }
#pages a.active, #pages a:hover { background: #ddd; }
#main { flex: 1; display: flex; flex-direction: column; padding: 1rem; box-sizing: border-box; gap: .5rem; }
#toolbar { display: flex; gap: .5rem; align-items: center; }
button { padding: .45rem .9rem; border: 1px solid #999; border-radius: 4px; background: #fff; cursor: pointer; }
button.primary { background: #0066cc; color: #fff; border-color: #0066cc; }
input[type=text] { padding: .45rem; border: 1px solid #999; border-radius: 4px; }
#editor { flex: 1; width: 100%; font-family: monospace; font-size: .9rem; padding: .5rem; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; resize: none; }
#preview { flex: 1; overflow: auto; border: 1px solid #ccc; border-radius: 4px; padding: 1rem; display: none; }
#msg { min-height: 1.2rem; font-size: .85rem; color: #060; }
</style>
</head>
<body>
<div id="side">
<h1>Seiten</h1>
<div id="pages"></div>
<hr>
<p style="font-size:.8rem">Neue Seite:</p>
<input type="text" id="newslug" placeholder="z.B. ueber-uns" style="width:100%;box-sizing:border-box;padding:.35rem">
<button id="btn-new" style="width:100%;margin-top:.4rem">Neu</button>
</div>
<div id="main">
<div id="toolbar">
<b id="current">-</b>
<button id="btn-save" class="primary">Speichern</button>
<button id="btn-preview">Vorschau</button>
<button id="btn-export" class="primary">Nach output/ exportieren</button>
<button id="btn-delete" style="margin-left:auto">Seite l&ouml;schen</button>
</div>
<div id="msg"></div>
<textarea id="editor" spellcheck="false">---
titel: Neue Seite
---
Hier den Inhalt schreiben. **fett**, *kursiv*, [Link](seite.html), # Ueberschrift, - Liste.</textarea>
<div id="preview"></div>
</div>
<script>
let pages = [], current = null;
const $ = id => document.getElementById(id);
const msg = t => { $("msg").textContent = t; setTimeout(() => $("msg").textContent = "", 4000); };
async function api(path, opts) {
const r = await fetch(path, opts);
return r.json();
}
async function loadPages() {
const d = await api("/api/pages");
pages = d.pages;
$("pages").innerHTML = "";
for (const p of pages) {
const a = document.createElement("a");
a.textContent = p + (p === d.start ? " (Start)" : "");
a.onclick = () => openPage(p);
if (p === current) a.className = "active";
$("pages").appendChild(a);
}
for (const b of [["header", d.header], ["footer", d.footer]]) {
if (b[1]) {
const a = document.createElement("a");
a.textContent = b[0] + " (Baustein)";
a.onclick = () => openPage(b[0]);
if (current === b[0]) a.className = "active";
$("pages").appendChild(a);
}
}
}
async function openPage(slug) {
const d = await api("/api/page/" + encodeURIComponent(slug));
current = slug;
$("current").textContent = slug;
$("editor").value = d.raw;
$("editor").disabled = false;
loadPages();
}
async function save() {
if (!current) return;
const d = await api("/api/page", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({slug: current, raw: $("editor").value})
});
msg(d.ok ? "Gespeichert." : ("Fehler: " + d.error));
if (d.ok) loadPages();
}
$("btn-save").onclick = save;
$("btn-delete").onclick = async () => {
if (!confirm("Seite '" + current + "' loeschen?")) return;
const d = await api("/api/delete", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({slug: current})
});
msg(d.ok ? "Geloescht." : ("Fehler: " + d.error));
current = null; $("current").textContent = "-"; $("editor").disabled = true;
loadPages();
};
$("btn-new").onclick = async () => {
const slug = $("newslug").value.trim().toLowerCase();
if (!slug) return msg("Bitte Namen eingeben.");
const d = await api("/api/page", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({slug, raw: "---\\ntitel: " + slug + "\\n---\\n\\nNeue Seite.\\n"})
});
if (d.ok) { $("newslug").value = ""; openPage(slug); }
else msg("Fehler: " + d.error);
};
$("btn-preview").onclick = async () => {
const ed = $("editor"), pv = $("preview");
if (pv.style.display === "block") {
pv.style.display = "none"; ed.style.display = "block"; return;
}
const d = await api("/api/preview", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({raw: ed.value})
});
pv.innerHTML = d.html; pv.style.display = "block"; ed.style.display = "none";
};
$("btn-export").onclick = async () => {
const d = await api("/api/export");
msg(d.exported ? "Exportiert: " + d.exported.join(", ") : ("Fehler: " + d.error));
};
loadPages();
</script>
</body>
</html>"""
def main():
global PORT
if len(sys.argv) > 1 and sys.argv[1] == "export":
files = export_site()
print("Exportiert nach output/:")
for f in files:
print(" -", f)
return
if len(sys.argv) > 2 and sys.argv[1] == "port":
PORT = int(sys.argv[2])
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
url = "http://localhost:%d" % PORT
print("Mini-CMS laeuft unter", url)
print("Beenden mit Strg+C.")
threading.Timer(1.0, lambda: webbrowser.open(url)).start()
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nServer gestoppt.")
if __name__ == "__main__":
main()
+244
View File
@@ -0,0 +1,244 @@
/* ---- Pixel-Pferd im Header ---- */
header {
text-align: left;
}
.pixel-horse-container {
width: 100px;
height: 100px;
position: relative;
margin: 0 auto; /* zentriert im Header */
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
.pixel-horse {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
opacity: 0;
animation: run 0.5s steps(1) infinite;
}
.pixel-horse:nth-child(1) { animation-delay: 0s; opacity: 1; }
.pixel-horse:nth-child(2) { animation-delay: 0.125s; }
.pixel-horse:nth-child(3) { animation-delay: 0.25s; }
.pixel-horse:nth-child(4) { animation-delay: 0.375s; }
@keyframes run {
0%, 24.9% { opacity: 1; }
25%, 49.9% { opacity: 0; }
50%, 74.9% { opacity: 1; }
75%, 99.9% { opacity: 0; }
100% { opacity: 1; }
}
/* ================================================================
Linux-Terminal Dark Theme
Nur System-Fonts (lokal verfuegbar, keine Web-Fonts, DSGVO-konform)
================================================================ */
:root {
--bg: #1a1a1a; /* sehr dunkles Grau */
--text: #ffffff; /* normaler Text weiss */
--dim: #909090; /* gedimmter Text (Footer, Zitate) */
--heading: #ff9500; /* Ueberschriften orange */
--link: #66aaff; /* Links, gut lesbar auf schwarz */
--green: #00ff00; /* Prompt, Code */
--border: #333333;
}
html {
font-size: 14px; /* Basis: alle Schriften 14 px (auch 1rem) */
background: var(--bg);
}
body {
/* Terminal-/Monospace-Fonts des Systems, kein Web-Font, DSGVO-konform */
font-family: "Ubuntu Mono", "DejaVu Sans Mono", Menlo, "Liberation Mono",
"Noto Sans Mono", Inconsolata, Monaco, "Courier New", monospace;
background: var(--bg);
color: var(--text);
line-height: 1.6;
margin: 0;
padding: 2rem;
box-sizing: border-box;
}
/* ---- Header = Terminal-Titelleiste ---- */
header {
max-width: 1200px; /* gleiche Spalte wie main/footer */
margin: 0 auto 1.5rem; /* in der Seitenmitte */
text-align: left;
}
/* ---- Hauptbereich als Terminal-Fenster ---- */
main {
max-width: 1200px;
margin: 0 auto; /* in der Seitenmitte */
text-align: left;
padding: 0 1.5rem 1.5rem 0;
}
/* Shell-Prompt ganz oben auf der Seite */
.prompt {
max-width: 1200px;
margin: 0 auto 1.5rem;
color: var(--green);
}
/* Blinkender Terminal-Cursor hinter dem Prompt */
.cursor {
display: inline-block;
width: .6em;
height: 1.1em;
background: var(--green);
vertical-align: text-bottom;
animation: blink 1s steps(1) infinite;
}
@keyframes blink {
0%, 49.9% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* ---- Typografie ---- */
h1, h2, h3, h4, h5, h6 {
color: var(--heading);
text-shadow: 0 0 8px rgba(255, 149, 0, 0.35);
letter-spacing: 1px;
font-size: 1rem; /* 14 px, ueberall gleiche Schriftgroesse */
font-weight: normal;
margin: 1.5rem 0 .8rem;
}
main > h1:first-of-type {
margin-top: 0;
}
p {
margin: .8rem 0;
}
/* ---- Anker-Navigation im Header (Links auf die Ueberschriften) ---- */
.anker-nav {
margin-top: 1rem;
}
.anker-nav ul {
list-style: none;
margin: 0;
padding-left: 0;
}
/* Unterpunkte einruecken (Ueberschrift mit einem # mehr) */
.anker-nav ul ul {
padding-left: 1.5rem;
}
.anker-nav li {
margin: .25rem 0;
}
.anker-nav a {
color: var(--link);
}
.anker-nav a:hover {
color: #ffffff;
}
/* ---- Links ---- */
a {
color: var(--link);
text-decoration: none;
}
a:hover {
color: #ffffff;
text-decoration: underline;
text-shadow: 0 0 8px rgba(102, 170, 255, 0.6);
}
/* ---- Kein Bold: alle Schrift normal ---- */
strong, b, th {
font-weight: normal;
}
/* ---- Listen, Zitate, Trennlinien ---- */
ul, ol {
margin: .8rem 0;
padding-left: 1.5rem;
}
li {
margin: .25rem 0;
}
li::marker {
color: var(--green);
}
blockquote {
border-left: 3px solid var(--green);
margin: 1rem 0;
padding: .5rem 1rem;
color: var(--dim);
background: rgba(0, 255, 0, 0.05);
}
hr {
border: none;
border-top: 1px dashed var(--border);
margin: 1.5rem 0;
}
/* ---- Code ---- */
code {
color: var(--green);
background: #242424;
padding: .1rem .3rem;
border-radius: 3px;
}
pre {
background: #141414;
border-radius: 4px;
padding: 1rem;
overflow-x: auto;
}
pre code {
background: none;
padding: 0;
}
/* ---- Footer ---- */
footer {
max-width: 1600px;
margin: 1.5rem auto 0; /* in der Seitenmitte, bündig zu main */
text-align: left;
border-top: 1px solid var(--border);
padding-top: 1rem;
color: var(--dim);
}
/* ---- Textauswahl invertiert, wie im Terminal ---- */
::selection {
background: var(--link);
color: #000000;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+148
View File
@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="212mm"
height="185mm"
viewBox="0 0 212 185"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="logo.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.67528301"
inkscape:cx="300.6147"
inkscape:cy="567.16961"
inkscape:window-width="2560"
inkscape:window-height="1283"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g1" /><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><g
id="g1"
inkscape:label="logo"><rect
style="fill:#bc6ec5;fill-opacity:1;stroke-width:0.436961"
id="rect1-6"
width="27.207493"
height="48.685009"
x="17.110464"
y="83.338158" /><rect
style="fill:#bc6ec5;fill-opacity:1;stroke-width:0.254324"
id="rect1-6-6"
width="17.513844"
height="25.62081"
x="7.3483195"
y="106.4026" /><rect
style="fill:#6cacb6;fill-opacity:1;stroke-width:0.744538"
id="rect1-2-3-2-2-1"
width="73.685966"
height="52.191757"
x="128.47047"
y="31.434856" /><rect
style="fill:#6cacb6;fill-opacity:1;stroke-width:0.303737"
id="rect1-2-3-2-2-1-4"
width="17.332495"
height="36.927048"
x="174.61983"
y="140.21355" /><rect
style="fill:#6cacb6;fill-opacity:1;stroke-width:0.335087"
id="rect1-2-3-2-2-1-4-6"
width="21.09514"
height="36.927048"
x="138.87154"
y="140.21284" /><rect
style="fill:#6cacb6;fill-opacity:1;stroke-width:0.325207"
id="rect1-2-3-2-2-1-4-6-4"
width="19.869507"
height="36.927048"
x="92.092537"
y="140.21284" /><rect
style="fill:#6cacb6;fill-opacity:1;stroke-width:0.340355"
id="rect1-2-3-2-2-1-4-6-4-4"
width="21.763668"
height="36.927048"
x="56.148205"
y="140.21284" /><rect
style="fill:#6cacb6;fill-opacity:1;stroke-width:1.22605"
id="rect1-2-3-2-2-1-3"
width="145.06674"
height="71.888435"
x="43.290218"
y="69.704735" /><rect
style="fill:#0d0d0b;fill-opacity:1;stroke-width:0.13182"
id="rect1-2-9"
width="10.990082"
height="10.968916"
x="152.58284"
y="35.083874" /><rect
style="fill:#0d0d0b;fill-opacity:1;stroke-width:0.0979278"
id="rect1-2-9-1"
width="9.4858952"
height="7.0134616"
x="164.40558"
y="71.279991" /><rect
style="fill:#0d0d0b;fill-opacity:1;stroke-width:0.0809229"
id="rect1-2-9-1-8"
width="6.4775219"
height="7.0134616"
x="182.83105"
y="71.279831" /><rect
style="fill:#0d0d0b;fill-opacity:1;stroke-width:0.15631"
id="rect1-2-9-9"
width="12.952992"
height="13.08592"
x="189.18622"
y="32.104492" /><rect
style="fill:#bc6ec5;fill-opacity:1;stroke-width:0.257236"
id="rect1"
width="35.229824"
height="13.030209"
x="128.47047"
y="22.358643" /><rect
style="fill:#6cadb6;fill-opacity:1;stroke-width:0.161179"
id="rect1-2"
width="13.831325"
height="13.030209"
x="162.7973"
y="22.358643" /><rect
style="fill:#bc6ec5;fill-opacity:1;stroke-width:0.161177"
id="rect1-2-3-2"
width="13.831325"
height="13.030209"
x="162.79794"
y="9.3847332" /><rect
style="fill:#6cacb6;fill-opacity:1;stroke-width:0.210201"
id="rect1-2-3-2-2"
width="23.524973"
height="13.030209"
x="128.4705"
y="9.3847332" /><rect
style="fill:#6babb5;fill-opacity:1;stroke-width:0.294245"
id="rect1-2-3-2-7"
width="25.738684"
height="23.336674"
x="176.41765"
y="9.3847332" /><rect
style="fill:#bc6ec5;fill-opacity:1;stroke-width:0.153906"
id="rect1-2-3"
width="12.611309"
height="13.030209"
x="176.41748"
y="22.358643" /></g></g></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File
Whitespace-only changes.
+6
View File
@@ -0,0 +1,6 @@
---
titel: Footer
---
Dieser Footer-Baustein wird automatisch an das Ende **aller** Seiten gesetzt.
Zum Beispiel mit Links: [Startseite](index.html)
+10
View File
@@ -0,0 +1,10 @@
---
titel: Header
---
<div class="pixel-horse-container">
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="70" width="8" height="20" fill="#00ffff" /><rect x="40" y="75" width="8" height="15" fill="#00ffff" /><rect x="60" y="70" width="8" height="20" fill="#00ffff" /><rect x="75" y="75" width="8" height="15" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="25" y="85" width="8" height="5" fill="#008888" /><rect x="60" y="85" width="8" height="5" fill="#008888" /></svg>
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="75" width="8" height="15" fill="#00ffff" /><rect x="40" y="70" width="8" height="20" fill="#00ffff" /><rect x="60" y="75" width="8" height="15" fill="#00ffff" /><rect x="75" y="70" width="8" height="20" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="40" y="85" width="8" height="5" fill="#008888" /><rect x="75" y="85" width="8" height="5" fill="#008888" /></svg>
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="70" width="8" height="20" fill="#00ffff" /><rect x="40" y="75" width="8" height="15" fill="#00ffff" /><rect x="60" y="70" width="8" height="20" fill="#00ffff" /><rect x="75" y="75" width="8" height="15" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="25" y="85" width="8" height="5" fill="#008888" /><rect x="60" y="85" width="8" height="5" fill="#008888" /></svg>
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="75" width="8" height="15" fill="#00ffff" /><rect x="40" y="70" width="8" height="20" fill="#00ffff" /><rect x="60" y="75" width="8" height="15" fill="#00ffff" /><rect x="75" y="70" width="8" height="20" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="40" y="85" width="8" height="5" fill="#008888" /><rect x="75" y="85" width="8" height="5" fill="#008888" /></svg>
</div>
+31
View File
@@ -0,0 +1,31 @@
---
titel: Willkommen
---
# Was ist die hfg_linux_usergroup
Die hfg_linux_usergroup ist eine diverse Gemeinschaft der HFG Offenbach, die sich für freie Software, Open Source und digitale Souveränität einsetzt.
Hintergrund ist die zentrale Bedeutung von Open Source für Kunst und Gestaltung: Es ermöglicht Demokratisierung, Transparenz, Chancengleichheit und Weiterentwicklung. Durch offene Lizenzen und frei zugängliche Software werden Wissen und kreative Prozesse entmonopolisiert. Künstler:innen und Designer:innen sind so nicht länger abhängig von proprietärer Software – die oft mit hohen Kosten, geschlossenen Systemen, Abhängigkeiten oder Abos einhergeht. Hinzu kommen ethische Bedenken wie Datenschutz oder Überwachungskapitalismus.
Open Source fördert nicht nur gestalterische Freiheit, sondern auch eine kritische Reflexion über Technologie selbst.
# Nächste Treffen
Die nächsten Termine für den Herbst/Winter 2026 werden aktuell geplant:
## hfg_linux_usergroup_date_1_music
## hfg_linux_usergroup_date_2_image-editing
## hfg_linux_usergroup_date_3_coding-development
## hfg_linux_usergroup_date_4_3d-film-fotogrammetrie
# Wer ist die hfg_linux_usergroup
Alle, die sich für Linux, Open Source und die Förderung von Wissen in Kunst und Design interessieren, können Teil der Gemeinschaft werden oder an den Treffen teilnehmen. Die hfg_linux_usergroup wurde von den beiden HFG-Lehrenden Sebastian Kujas und Florian Albrecht-Schoeck gegründet und wird aktuell von ihnen moderiert.
# Was sind die Ziele der hfg_linux_usergroup
Ziel ist es, den Einsatz freier Software in Kunst und Design zu fördern. Durch Workshops, den Aufbau von Wissensdatenbanken, Austausch und Debatten sollen Eigenermächtigung und Verantwortung für Technologie wiederhergestellt werden. Dazu entwickeln wir gemeinsam ein eigenes Linux-basiertes Betriebssystem und unterstützen bei allen Schritten des Wechsels zu freier Software.
# Das hfg_OS_
Das hfg_OS ist ein auf Debian 13 (Trixie) basierendes, Linux-basiertes Betriebssystem. Es wurde speziell für die Bedürfnisse von Künstler:innen und Designer:innen angepasst und wird kontinuierlich weiterentwickelt.
Bald verfügbar: hfg_OS_Falbella-2026-10
+244
View File
@@ -0,0 +1,244 @@
/* ---- Pixel-Pferd im Header ---- */
header {
text-align: left;
}
.pixel-horse-container {
width: 100px;
height: 100px;
position: relative;
margin: 0 auto; /* zentriert im Header */
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
.pixel-horse {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
opacity: 0;
animation: run 0.5s steps(1) infinite;
}
.pixel-horse:nth-child(1) { animation-delay: 0s; opacity: 1; }
.pixel-horse:nth-child(2) { animation-delay: 0.125s; }
.pixel-horse:nth-child(3) { animation-delay: 0.25s; }
.pixel-horse:nth-child(4) { animation-delay: 0.375s; }
@keyframes run {
0%, 24.9% { opacity: 1; }
25%, 49.9% { opacity: 0; }
50%, 74.9% { opacity: 1; }
75%, 99.9% { opacity: 0; }
100% { opacity: 1; }
}
/* ================================================================
Linux-Terminal Dark Theme
Nur System-Fonts (lokal verfuegbar, keine Web-Fonts, DSGVO-konform)
================================================================ */
:root {
--bg: #1a1a1a; /* sehr dunkles Grau */
--text: #ffffff; /* normaler Text weiss */
--dim: #909090; /* gedimmter Text (Footer, Zitate) */
--heading: #ff9500; /* Ueberschriften orange */
--link: #66aaff; /* Links, gut lesbar auf schwarz */
--green: #00ff00; /* Prompt, Code */
--border: #333333;
}
html {
font-size: 14px; /* Basis: alle Schriften 14 px (auch 1rem) */
background: var(--bg);
}
body {
/* Terminal-/Monospace-Fonts des Systems, kein Web-Font, DSGVO-konform */
font-family: "Ubuntu Mono", "DejaVu Sans Mono", Menlo, "Liberation Mono",
"Noto Sans Mono", Inconsolata, Monaco, "Courier New", monospace;
background: var(--bg);
color: var(--text);
line-height: 1.6;
margin: 0;
padding: 2rem;
box-sizing: border-box;
}
/* ---- Header = Terminal-Titelleiste ---- */
header {
max-width: 1200px; /* gleiche Spalte wie main/footer */
margin: 0 auto 1.5rem; /* in der Seitenmitte */
text-align: left;
}
/* ---- Hauptbereich als Terminal-Fenster ---- */
main {
max-width: 1200px;
margin: 0 auto; /* in der Seitenmitte */
text-align: left;
padding: 0 1.5rem 1.5rem 0;
}
/* Shell-Prompt ganz oben auf der Seite */
.prompt {
max-width: 1200px;
margin: 0 auto 1.5rem;
color: var(--green);
}
/* Blinkender Terminal-Cursor hinter dem Prompt */
.cursor {
display: inline-block;
width: .6em;
height: 1.1em;
background: var(--green);
vertical-align: text-bottom;
animation: blink 1s steps(1) infinite;
}
@keyframes blink {
0%, 49.9% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* ---- Typografie ---- */
h1, h2, h3, h4, h5, h6 {
color: var(--heading);
text-shadow: 0 0 8px rgba(255, 149, 0, 0.35);
letter-spacing: 1px;
font-size: 1rem; /* 14 px, ueberall gleiche Schriftgroesse */
font-weight: normal;
margin: 1.5rem 0 .8rem;
}
main > h1:first-of-type {
margin-top: 0;
}
p {
margin: .8rem 0;
}
/* ---- Anker-Navigation im Header (Links auf die Ueberschriften) ---- */
.anker-nav {
margin-top: 1rem;
}
.anker-nav ul {
list-style: none;
margin: 0;
padding-left: 0;
}
/* Unterpunkte einruecken (Ueberschrift mit einem # mehr) */
.anker-nav ul ul {
padding-left: 1.5rem;
}
.anker-nav li {
margin: .25rem 0;
}
.anker-nav a {
color: var(--link);
}
.anker-nav a:hover {
color: #ffffff;
}
/* ---- Links ---- */
a {
color: var(--link);
text-decoration: none;
}
a:hover {
color: #ffffff;
text-decoration: underline;
text-shadow: 0 0 8px rgba(102, 170, 255, 0.6);
}
/* ---- Kein Bold: alle Schrift normal ---- */
strong, b, th {
font-weight: normal;
}
/* ---- Listen, Zitate, Trennlinien ---- */
ul, ol {
margin: .8rem 0;
padding-left: 1.5rem;
}
li {
margin: .25rem 0;
}
li::marker {
color: var(--green);
}
blockquote {
border-left: 3px solid var(--green);
margin: 1rem 0;
padding: .5rem 1rem;
color: var(--dim);
background: rgba(0, 255, 0, 0.05);
}
hr {
border: none;
border-top: 1px dashed var(--border);
margin: 1.5rem 0;
}
/* ---- Code ---- */
code {
color: var(--green);
background: #242424;
padding: .1rem .3rem;
border-radius: 3px;
}
pre {
background: #141414;
border-radius: 4px;
padding: 1rem;
overflow-x: auto;
}
pre code {
background: none;
padding: 0;
}
/* ---- Footer ---- */
footer {
max-width: 1600px;
margin: 1.5rem auto 0; /* in der Seitenmitte, bündig zu main */
text-align: left;
border-top: 1px solid var(--border);
padding-top: 1rem;
color: var(--dim);
}
/* ---- Textauswahl invertiert, wie im Terminal ---- */
::selection {
background: var(--link);
color: #000000;
}
+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Willkommen</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="prompt">user@hfg_linux_group:~$ free-software-for-art-and-design<span class="cursor"></span></div>
<header>
<div class="pixel-horse-container">
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="70" width="8" height="20" fill="#00ffff" /><rect x="40" y="75" width="8" height="15" fill="#00ffff" /><rect x="60" y="70" width="8" height="20" fill="#00ffff" /><rect x="75" y="75" width="8" height="15" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="25" y="85" width="8" height="5" fill="#008888" /><rect x="60" y="85" width="8" height="5" fill="#008888" /></svg>
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="75" width="8" height="15" fill="#00ffff" /><rect x="40" y="70" width="8" height="20" fill="#00ffff" /><rect x="60" y="75" width="8" height="15" fill="#00ffff" /><rect x="75" y="70" width="8" height="20" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="40" y="85" width="8" height="5" fill="#008888" /><rect x="75" y="85" width="8" height="5" fill="#008888" /></svg>
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="70" width="8" height="20" fill="#00ffff" /><rect x="40" y="75" width="8" height="15" fill="#00ffff" /><rect x="60" y="70" width="8" height="20" fill="#00ffff" /><rect x="75" y="75" width="8" height="15" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="25" y="85" width="8" height="5" fill="#008888" /><rect x="60" y="85" width="8" height="5" fill="#008888" /></svg>
<svg class="pixel-horse" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="45" width="60" height="30" fill="#00ffff" /><rect x="55" y="25" width="30" height="25" fill="#00ffff" /><rect x="55" y="20" width="10" height="10" fill="#00ffff" /><rect x="75" y="20" width="10" height="10" fill="#00ffff" /><rect x="65" y="30" width="5" height="5" fill="#000000" /><rect x="80" y="30" width="5" height="5" fill="#000000" /><rect x="70" y="45" width="3" height="3" fill="#000000" /><rect x="78" y="45" width="3" height="3" fill="#000000" /><rect x="55" y="25" width="5" height="5" fill="#ff00ff" /><rect x="60" y="20" width="5" height="5" fill="#ff00ff" /><rect x="65" y="25" width="5" height="5" fill="#ff00ff" /><rect x="70" y="20" width="5" height="5" fill="#ff00ff" /><rect x="75" y="25" width="5" height="5" fill="#ff00ff" /><rect x="25" y="75" width="8" height="15" fill="#00ffff" /><rect x="40" y="70" width="8" height="20" fill="#00ffff" /><rect x="60" y="75" width="8" height="15" fill="#00ffff" /><rect x="75" y="70" width="8" height="20" fill="#00ffff" /><rect x="10" y="50" width="10" height="20" fill="#ff00ff" /><rect x="5" y="60" width="10" height="10" fill="#ff00ff" /><rect x="40" y="85" width="8" height="5" fill="#008888" /><rect x="75" y="85" width="8" height="5" fill="#008888" /></svg>
</div>
<nav class="anker-nav"><ul><li><a href="#was-ist-die-hfg-linux-usergroup">Was ist die hfg_linux_usergroup</a></li><li><a href="#naechste-treffen">Nächste Treffen</a><ul><li><a href="#hfg-linux-usergroup-date-1-music">hfg_linux_usergroup_date_1_music</a></li><li><a href="#hfg-linux-usergroup-date-2-image-editing">hfg_linux_usergroup_date_2_image-editing</a></li><li><a href="#hfg-linux-usergroup-date-3-coding-development">hfg_linux_usergroup_date_3_coding-development</a></li><li><a href="#hfg-linux-usergroup-date-4-3d-film-fotogrammetrie">hfg_linux_usergroup_date_4_3d-film-fotogrammetrie</a></li></ul></li><li><a href="#wer-ist-die-hfg-linux-usergroup">Wer ist die hfg_linux_usergroup</a></li><li><a href="#was-sind-die-ziele-der-hfg-linux-usergroup">Was sind die Ziele der hfg_linux_usergroup</a></li><li><a href="#das-hfg-os">Das hfg_OS_</a></li></ul></nav>
</header>
<main>
<h1>Willkommen</h1>
<h1 id="was-ist-die-hfg-linux-usergroup">Was ist die hfg_linux_usergroup</h1>
<p>Die hfg_linux_usergroup ist eine diverse Gemeinschaft der HFG Offenbach, die sich für freie Software, Open Source und digitale Souveränität einsetzt.</p>
<p>Hintergrund ist die zentrale Bedeutung von Open Source für Kunst und Gestaltung: Es ermöglicht Demokratisierung, Transparenz, Chancengleichheit und Weiterentwicklung. Durch offene Lizenzen und frei zugängliche Software werden Wissen und kreative Prozesse entmonopolisiert. Künstler:innen und Designer:innen sind so nicht länger abhängig von proprietärer Software – die oft mit hohen Kosten, geschlossenen Systemen, Abhängigkeiten oder Abos einhergeht. Hinzu kommen ethische Bedenken wie Datenschutz oder Überwachungskapitalismus. Open Source fördert nicht nur gestalterische Freiheit, sondern auch eine kritische Reflexion über Technologie selbst.</p>
<h1 id="naechste-treffen">Nächste Treffen</h1>
<p>Die nächsten Termine für den Herbst/Winter 2026 werden aktuell geplant:</p>
<h2 id="hfg-linux-usergroup-date-1-music">hfg_linux_usergroup_date_1_music</h2>
<h2 id="hfg-linux-usergroup-date-2-image-editing">hfg_linux_usergroup_date_2_image-editing</h2>
<h2 id="hfg-linux-usergroup-date-3-coding-development">hfg_linux_usergroup_date_3_coding-development</h2>
<h2 id="hfg-linux-usergroup-date-4-3d-film-fotogrammetrie">hfg_linux_usergroup_date_4_3d-film-fotogrammetrie</h2>
<h1 id="wer-ist-die-hfg-linux-usergroup">Wer ist die hfg_linux_usergroup</h1>
<p>Alle, die sich für Linux, Open Source und die Förderung von Wissen in Kunst und Design interessieren, können Teil der Gemeinschaft werden oder an den Treffen teilnehmen. Die hfg_linux_usergroup wurde von den beiden HFG-Lehrenden Sebastian Kujas und Florian Albrecht-Schoeck gegründet und wird aktuell von ihnen moderiert.</p>
<h1 id="was-sind-die-ziele-der-hfg-linux-usergroup">Was sind die Ziele der hfg_linux_usergroup</h1>
<p>Ziel ist es, den Einsatz freier Software in Kunst und Design zu fördern. Durch Workshops, den Aufbau von Wissensdatenbanken, Austausch und Debatten sollen Eigenermächtigung und Verantwortung für Technologie wiederhergestellt werden. Dazu entwickeln wir gemeinsam ein eigenes Linux-basiertes Betriebssystem und unterstützen bei allen Schritten des Wechsels zu freier Software.</p>
<h1 id="das-hfg-os">Das hfg_OS_</h1>
<p>Das hfg_OS ist ein auf Debian 13 (Trixie) basierendes, Linux-basiertes Betriebssystem. Es wurde speziell für die Bedürfnisse von Künstler:innen und Designer:innen angepasst und wird kontinuierlich weiterentwickelt.</p>
<p>Bald verfügbar: hfg_OS_Falbella-2026-10</p>
</main>
<footer>
<p>Dieser Footer-Baustein wird automatisch an das Ende <strong>aller</strong> Seiten gesetzt. Zum Beispiel mit Links: <a href="index.html">Startseite</a></p>
</footer>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
ich baue ein cms.
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Mini-CMS per Doppelklick starten (macOS)
cd "$(dirname "$0")" || exit 1
python3 cms.py "$@"
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Mini-CMS starten (macOS und Linux)
cd "$(dirname "$0")" || exit 1
exec python3 cms.py "$@"
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{titel}}</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="prompt">user@hfg_linux_group:~$ free-software-for-art-and-design<span class="cursor"></span></div>
<header>
{{header}}
<nav class="anker-nav">{{anker}}</nav>
</header>
<main>
<h1>{{titel}}</h1>
{{inhalt}}
</main>
<footer>
{{footer}}
</footer>
</body>
</html>