Compare commits

..
25 Commits
Author SHA1 Message Date
raffaelandClaude Opus 5.5 204e11b966 chore(infra): Gitea-Host auf git.hg-int.dev umstellen
Gitea läuft seit 2026-11-11 auf hivegrid-control (git.hg-int.dev);
git.tool-ctrl.de wird abgeschaltet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:54:23 +01:00
ToolCTRLandClaude Opus 4.8 41664c3d0d SEO: Meta-Description auf neue Copy (Bedienung & Gestaltung)
Deploy / build-and-deploy (push) Successful in 22s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:42:27 +02:00
ToolCTRLandClaude Opus 4.8 d44a33e63b SEO: Favicon 96x96, GameJam-Uploads noindex, og:image-Metadaten
Deploy / build-and-deploy (push) Successful in 24s
- Favicon aus Emblem neu in 96x96 generiert (Google-SERP braucht min. 48x48),
  Favicon-Links versioniert
- /gamejam-games/: X-Robots-Tag noindex + robots.txt Disallow (Fremdcontent)
- og:image:width/height/alt auf Start, TechDay und GameJam

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:01:21 +02:00
ToolCTRLandClaude Opus 4.8 9f832bcec7 SEO: Canonicals/OG auf allen Seiten, WebP, Caching, www-Redirect
Deploy / build-and-deploy (push) Successful in 24s
- alle Unterseiten: Canonical-Tags; techday/gamejam zusaetzlich Open Graph
- techday: Event-JSON-LD (27. Mai 2026, HubWerk01 Bruchsal)
- mITapp-Screenshots als WebP (1,6 MB -> 65 KB) mit width/height/lazy
- dediziertes OG-Bild 1200x630 (Logo + Tagline) fuer Start & GameJam
- server.js: 301 www -> non-www, /index.html -> /, statische Assets
  1 Jahr immutable cachen (HTML weiter no-cache), .webp MIME
- CSS/JS ueberall versioniert (?v=20260610)
- sitemap: lastmod ergaenzt, noindex-Seiten (Impressum/Datenschutz) entfernt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:56:50 +02:00
ToolCTRLandClaude Opus 4.8 41bdd52be8 SEO: SERP-Titel/Description fixen, OG/JSON-LD, robots & sitemap
Deploy / build-and-deploy (push) Successful in 28s
- index.html: beschreibender Titel ohne Em-Dash, gekuerzte Meta-Description,
  Open-Graph/Twitter-Tags, Canonical, Organization-JSON-LD
- Em-Dashes aus allen Seitentiteln entfernt (techday, gamejam, impressum, datenschutz)
- gamejam.html: fehlende Meta-Description ergaenzt
- robots.txt + sitemap.xml neu, .txt/.xml MIME-Typen im Server

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:43:39 +02:00
ToolCTRLandClaude Opus 4.8 bfdd37ac40 Landing: Standort-Section (EU-Hosting) mit Paragraf-Wasserzeichen
Deploy / build-and-deploy (push) Successful in 21s
Neue Section zwischen Organisation und Events: positioniert ToolCTRL
als deutsch gehostete, dem deutschen Recht unterstellte Alternative zu
US-Software. Grau hinterlegt, hält den Grau/Weiss-Wechsel intakt.
Grosses, blasses Paragraf-Zeichen als Wasserzeichen im Freiraum oben
rechts (analog zum CTRL-Schriftzug im Hero).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:43:35 +02:00
ToolCTRLandClaude Opus 4.7 9ecfc015b1 Form: Vorname/Nachname + Branche, Hero ohne Leistungen-Button
Deploy / build-and-deploy (push) Successful in 21s
Form
- Name in Vorname + Nachname gesplittet (beide Pflicht), 2-Spalten-Grid
- Neues Feld Branche neben Firmenname (optional, 2-Spalten-Grid)
- Telefon-Label auf 'Telefonnummer' vereinheitlicht
- Placeholder ohne Em-Dash

script.js
- Validierung jetzt auf vorname/nachname/email/nachricht
- Payload sendet neue Einzelfelder (vorname, nachname, firmenname,
  branche) PLUS zusammengesetzte Backward-Compat-Felder (name, firma
  mit Branche-Suffix in Klammern), damit server.js/Inbox-Schema
  unverändert bleibt

Hero
- 'Leistungen ansehen' Ghost-Button entfernt, nur noch der schwarze
  'Kontakt aufnehmen' CTA

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 12:21:39 +02:00
ToolCTRLandClaude Opus 4.7 9d75831130 Impressum/Datenschutz mit echten Daten, Hero-Trennlinie raus
Deploy / build-and-deploy (push) Successful in 22s
Impressum
- Geschäftsführer: Raffael Sheikh
- Telefon +49 1523 7750939 (tel:-Link)
- Registereintrag: Amtsgericht Mannheim, HRB 756189
- USt-IdNr.: DE458565520
- Verantwortlich §18 Abs. 2 MStV: Raffael Sheikh

Datenschutz
- Hosting-Block: Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen
- Zusatzabsatz: Kontaktanfragen werden auf eigenen Servern/Services
  verarbeitet, keine Weitergabe an externe CRM-/Marketing-Dienste

UI
- border-bottom auf .hero entfernt, damit Hero nahtlos in Leistungen
  übergeht (Grid-Hintergrund läuft jetzt durch)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 20:12:30 +02:00
ToolCTRLandClaude Opus 4.7 6597f6b303 Landing: Events-Sektion, Grid-Hintergrund, scharfe Form, Konzept-Texte, Telefon im Footer
Deploy / build-and-deploy (push) Successful in 22s
UI / Layout
- Subtiler 72px-Grid auf html (rgba(10,10,10,.032)), body transparent
- Neue Events-Sektion vor #kontakt: TechDay-Card + AI-GameJam-Card mit
  3px-Border + 7px-Offset-Shadow, ganze Card klickbar via stretched-link
- Form mit scharfen Ecken und 12px-Offset-Shadow (raffael-Stil),
  Section-Dark-Button-Schatten, monochromer Form-Status

Navigation
- Nur noch Start / TechDay / GameJam + Kontakt-Button (Anchor-Links
  zur Landingpage raus, da sie ohnehin direkt darunter erscheinen)
- Größere Hover-Fläche pro Nav-Link via padding, Unterstreichung
  via ::after fest positioniert
- Mobile-Nav synchron, TechDay-Page synchron

Copy
- Hero: CTA 'Projekt besprechen' -> 'Kontakt aufnehmen'
- Leistungen: neuer Intro + alle sechs Card-Texte (Hands-Off, Anzug,
  Kommandozentrale, Tools-aneinander-vorbei, zerstueckelte Teil-
  loesungen, waechst-mit-Ihrem-Betrieb)
- Vorgehen -> 'Konzept' / 'Das ToolCTRL Konzept.' mit vier Schritten
  (Situationsanalyse, Grundkonzepte, Umsetzung, Betrieb & Wartung)
- Kontakt-Intro umgeschrieben, Form-Label 'Worum geht es Ihnen?'

Kontakt
- E-Mail global admin@ -> info@ (5 Dateien inkl. script.js Fallback)
- Telefon +49 1523 7750939 in Footer-Kontaktspalte (index + techday),
  klickbar via tel:

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 19:59:42 +02:00
raffaelandClaude Opus 4.8 0a9983b6d9 feat: forward leads to central lead-inbox (ADR 012)
Deploy / build-and-deploy (push) Successful in 22s
handleLead forwards to LEAD_INBOX_URL/v1/leads (DE field aliases map
server-side); local ndjson/csv stays as fallback. No-op until
LEAD_INBOX_URL/LEAD_INBOX_KEY are set, so safe to deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:27:55 +02:00
ToolCTRLandClaude Opus 4.7 d9fd1621ff GameJam: Preise-Section entfernt (durch Gewinner-Section ersetzt)
Deploy / build-and-deploy (push) Successful in 21s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:41:31 +02:00
ToolCTRLandClaude Opus 4.7 e94e0a36ec GameJam: Gewinner korrigiert + Section nach unten verschoben
Deploy / build-and-deploy (push) Successful in 21s
Richtige Reihenfolge: Platz 1 Banana Bandit, Platz 2 Ship IT,
Platz 3 Firestarter. Gewinner-Section von oben (unter Hero) ans
Seitenende verschoben (zwischen Galerie und Footer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:38:03 +02:00
ToolCTRLandClaude Opus 4.7 ba7ee347e6 GameJam: Gewinner-Section ergänzt (Top 3)
Deploy / build-and-deploy (push) Successful in 21s
Nach dem Event: Podium direkt unter dem Hero im Preise-Stil.
Platz 1 Firestarter (hervorgehoben, Pokal), Platz 2 Ship IT,
Platz 3 Banana Bandit, jeweils mit Gutschein-Betrag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:32:46 +02:00
ToolCTRLandClaude Opus 4.7 92f113f89b GameJam: Galerie-iframes laden wieder sofort, aber ohne Auto-Ton
Deploy / build-and-deploy (push) Successful in 21s
Klick-Platzhalter zurückgenommen. Stattdessen am iframe
allow="autoplay 'none'" gesetzt: das Spiel lädt sichtbar, der Browser
blockiert automatischen Ton (auch via AudioContext). Erst eine
User-Geste im Spiel (Reinklicken) erlaubt den Ton.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:21:26 +02:00
ToolCTRLandClaude Opus 4.7 49c24c1833 GameJam: Galerie-Spiele erst auf Klick laden (kein Auto-Ton)
Deploy / build-and-deploy (push) Successful in 20s
Statt jedes eingereichte Spiel sofort in ein iframe zu laden (wodurch
mehrere Spiele gleichzeitig Ton abspielten), zeigt die Galerie jetzt
einen "Spiel starten"-Platzhalter. Das iframe (und damit Ton) wird
erst beim Klick erzeugt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:13:28 +02:00
ToolCTRLandClaude Opus 4.7 bede371528 GameJam: Mitbringen-Section spezifiziert Mac/Linux-Laptop
Deploy / build-and-deploy (push) Successful in 37s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:24:22 +02:00
ToolCTRLandClaude Opus 4.7 0cfc264c4b GameJam: Hart-Deadline für Einreichungen am 27.05.2026 22:00 Berlin
Deploy / build-and-deploy (push) Successful in 21s
server.js: handleGamejamUpload lehnt nach UPLOAD_DEADLINE mit HTTP 410
und Klartext-Hinweis ab. gamejam.html: Formular wird nach Ablauf
clientseitig gesperrt und ein roter Status zeigt den abgelaufenen
Termin. Doppelschicht (Server + UI), Server ist verbindlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:36:59 +02:00
ToolCTRLandClaude Opus 4.7 63be60b88a GameJam: kleine "Mitbringen"-Section ergänzt
Deploy / build-and-deploy (push) Successful in 20s
Zwischen "So läuft's" und "Bewertung": ein Satz, dass Teilnehmer ihr
eigenes Gerät (am besten Laptop) mitbringen und sonst nur Lust auf
ein KI-Spiel brauchen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:22:32 +02:00
ToolCTRLandClaude Opus 4.7 616cd60c6d Produktion: ClamAV-Scan deaktiviert (CLAMAV_ENABLED=false)
Deploy / build-and-deploy (push) Successful in 20s
Solange in der Coolify-Umgebung kein clamd erreichbar ist, würden
GameJam-Uploads sonst mit 503 (fail-closed) abgelehnt. Für den
TechDay morgen schalten wir den Scan im Container-ENV ab; Uploads
laufen unverändert, nur ohne Virenprüfung. Lokal bleibt der Default
true (Scan aktiv, wenn clamd läuft).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:16:20 +02:00
ToolCTRL 730aac65f8 Revert "Produktion: ClamAV in den App-Container integriert"
Deploy / build-and-deploy (push) Successful in 20s
This reverts commit bec5dc7363.
2026-05-26 14:14:20 +02:00
ToolCTRLandClaude Opus 4.7 8e05f323e2 GameJam: Logos verlinken zu LinkedIn
Deploy / build-and-deploy (push) Successful in 32s
- Topbar mITup-Logo: linkedin.com/company/it-meetup-bruchsal/
- Footer ToolCTRL-Logo: linkedin.com/company/toolctrl/
- Footer mITup-Logo: linkedin.com/company/it-meetup-bruchsal/

Externe Links öffnen in neuem Tab. Das Topbar-ToolCTRL-Logo bleibt
der Zurück-zur-Startseite-Link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:11:39 +02:00
ToolCTRLandClaude Opus 4.7 bec5dc7363 Produktion: ClamAV in den App-Container integriert
Deploy / build-and-deploy (push) Successful in 33s
Dockerfile installiert clamav-daemon + clamav-freshclam und konfiguriert
clamd auf TCP 127.0.0.1:3310 mit DB-Verzeichnis auf der persistenten
Coolify-Volume (/app/data/clamav-db).

entrypoint.sh startet clamd und freshclam vor dem Node-Server, wartet
auf clamd-PING und exec't dann node als unprivilegierter Nutzer. tini
übernimmt PID 1 für Signal-Forwarding.

Folge: GameJam-Uploads werden in Produktion automatisch per ClamAV
geprüft, ohne dass Coolify manuell angepasst werden muss. Erster
Deploy lädt einmalig ~300 MB Virendefinitionen auf die Volume,
Folge-Starts in ~30 s. RAM-Bedarf des Containers ~1.5 GB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:40:19 +02:00
ToolCTRLandClaude Opus 4.7 9639e2d72b GameJam-Preise: Amazon-Gutscheine statt Claude-Accounts
Deploy / build-and-deploy (push) Successful in 21s
Platz 1: 50 EUR Amazon-Gutschein
Platz 2: 25 EUR Amazon-Gutschein
Platz 3: 25 EUR Amazon-Gutschein

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:55:27 +02:00
Raffael Sheikh 95d0e21bbf Fix deploy workflow syntax check
Deploy / build-and-deploy (push) Successful in 22s
2026-05-24 22:18:51 +02:00
Raffael Sheikh a9dbc23d7e Add Coolify deployment workflow
Deploy / build-and-deploy (push) Failing after 11m57s
2026-05-24 22:13:55 +02:00
37 changed files with 1114 additions and 3044 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
.git
.gitea
.DS_Store
node_modules
*.log
leads
gamejam
public/gamejam-games
+62
View File
@@ -0,0 +1,62 @@
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: toolctrl-ci
steps:
- name: Setup hosts override and Docker CLI
run: |
echo "195.201.133.26 git.hg-int.dev" >> /etc/hosts
apt-get update -qq
apt-get install -qq -y --no-install-recommends ca-certificates curl git
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | tee /etc/apt/keyrings/docker.asc >/dev/null
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null
apt-get update -qq
apt-get install -qq -y --no-install-recommends docker-ce-cli docker-buildx-plugin
docker --version
docker buildx version
- name: Checkout
run: |
mkdir -p /workspace/repo
cd /workspace/repo
git clone "https://${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PAT }}@git.hg-int.dev/${{ github.repository }}.git" .
git checkout "${{ github.sha }}"
- name: Static checks
run: |
cd /workspace/repo
node --check server.js
git diff --check
- name: Login to Gitea Registry
run: echo "${{ secrets.REGISTRY_PAT }}" | docker login git.hg-int.dev --username "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build and push image
env:
IMAGE: git.hg-int.dev/${{ github.repository }}
SHA: ${{ github.sha }}
run: |
cd /workspace/repo
SHORT_SHA=${SHA:0:7}
IMAGE_LC=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')
docker build \
--build-arg APP_REVISION="$SHA" \
-t "$IMAGE_LC:$SHORT_SHA" \
-t "$IMAGE_LC:main" \
.
docker push "$IMAGE_LC:$SHORT_SHA"
docker push "$IMAGE_LC:main"
- name: Trigger Coolify deploy
run: |
curl -fsS -X POST \
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}" \
"https://saas-ctrl.com/api/v1/applications/ocwtt7rd2zijtlszgua005k9/start"
+35
View File
@@ -0,0 +1,35 @@
FROM node:22-bookworm-slim
LABEL org.opencontainers.image.description="ToolCTRL Website"
ARG APP_REVISION=local
ENV NODE_ENV=production \
PORT=3000 \
HOST=0.0.0.0 \
DATA_DIR=/app/data \
CLAMAV_ENABLED=false \
APP_REVISION=${APP_REVISION}
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid 1001 nodejs \
&& useradd --uid 1001 --gid nodejs --home-dir /app --shell /usr/sbin/nologin nodejs
WORKDIR /app
COPY --chown=nodejs:nodejs public ./public
COPY --chown=nodejs:nodejs server.js ./server.js
RUN mkdir -p /app/data \
&& chown -R nodejs:nodejs /app/data
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS http://127.0.0.1:3000/healthz >/dev/null || exit 1
CMD ["node", "server.js"]
+30
View File
@@ -83,6 +83,22 @@ Host/Port und An/Aus stehen in `server.js` im Objekt `CLAMAV`. Mit
`enabled: false` läuft der Upload ohne Virenscan (nur für lokale Tests `enabled: false` läuft der Upload ohne Virenscan (nur für lokale Tests
ohne ClamAV gedacht). ohne ClamAV gedacht).
Per Environment kann der Runtime-Betrieb konfiguriert werden:
```bash
PORT=3000
HOST=0.0.0.0
DATA_DIR=/app/data
CLAMAV_HOST=127.0.0.1
CLAMAV_PORT=3310
CLAMAV_ENABLED=true
APP_REVISION=<git-sha>
```
`/healthz` ist die leichte Liveness-Probe für Container und Coolify.
`/readyz` prüft zusätzlich Schreibzugriff auf die Datenverzeichnisse und,
falls aktiv, die Erreichbarkeit von ClamAV.
### Funktion testen ### Funktion testen
Eine Datei mit der EICAR-Testsignatur hochladen — `clamd` meldet dann Eine Datei mit der EICAR-Testsignatur hochladen — `clamd` meldet dann
@@ -93,3 +109,17 @@ Eine Datei mit der EICAR-Testsignatur hochladen — `clamd` meldet dann
Für den QR-Code am Stand muss der Rechner im selben WLAN erreichbar sein. Für den QR-Code am Stand muss der Rechner im selben WLAN erreichbar sein.
Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy mit HTTPS setzen Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy mit HTTPS setzen
oder bei einem Hoster bereitstellen — `tool-ctrl.de`. oder bei einem Hoster bereitstellen — `tool-ctrl.de`.
## Deployment
Deployment folgt dem Gitea-Infra-Standard aus `toolctrl-labs/infra`:
1. Push auf `main` triggert `.gitea/workflows/deploy.yml`.
2. Gitea Actions baut das Docker-Image und pusht es nach
`git.hg-int.dev/toolctrl-labs/toolctrl-website`.
3. Tags: kurzer Commit-SHA und `main`.
4. Coolify-App `toolctrl-website` (`ocwtt7rd2zijtlszgua005k9`) zieht
`:main` auf der Marketing-VM und startet neu.
Die Coolify-App läuft im Projekt `Marketing`, Environment `production`, Server
`marketing-vm`, Port `3000`, mit persistentem Storage auf `/app/data`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2300" height="2000" version="1.0">
<path style="fill:#666" d="M1157.3928 199.37139c-.1561-.351-3.243-2.137-6.86-3.969-9.658-4.894-20.13-13.0491-32.228-25.098-10.972-10.9291-12.004-12.329-31.6471-42.912-3.1689-4.934-6.796-9.645-8.059-10.468-2.087-1.36-4.58-1.469-27.4779-1.206-27.382.315-37.043-.412-49.9371-3.755-18.12509-4.699-31.09801-12.288001-48.08206-28.123001-12.75503-11.893101-19.10594-18.966001-21.23697-23.651002-2.06203-4.532-1.98498-5.791.53398-8.781 2.6-3.086 6.57501-4.736 15.86602-6.585101 13.339-2.655 25.82102-10.881 42.01594-27.690001 27.24019-28.273002 49.92309-66.238004 62.40219-104.442006 22.544-69.020899 40.4279-213.453909 37.524-303.043919-2.466-76.05-9.554-108.479-24.358-111.436-9.0331-1.804-18.294 13.5529-25.24 41.853-4.2181 17.189-5.106 25.236-4.5591 41.309.538 15.8 2.3111 27.0349 7.9171 50.1769 2.126 8.7731 3.91 17.91601 3.964 20.31811.0869 3.815-.208 4.63-2.344 6.465-2.194 1.885-2.952 2.046-7.4181 1.58-15.831-1.652-31.538-10.826-59.80409-34.93001-15.40394-13.135-19.29891-15.761-23.38693-15.761-2.95903 0-3.14901.199-6.29802 6.588-8.40105 17.0499-30.92794 55.05191-47.98398 80.94701-13.42402 20.38-22.71602 33.032-32.47906 44.2229-3.87397 4.4421-7.98798 9.9231-9.14197 12.1802-6.07798 11.8959-1.32096 20.9719 10.99301 20.9719 6.33098 0 8.096-.788 21.70402-9.696 22.85602-14.963 39.85203-22.2741 53.95693-23.209 8.87509-.588 12.904.298 23.27309 5.118l7.66091 3.561 19.15103.04c17.53206.037 19.57116.1981 24.12316 1.894 6.138 2.287 14.5129 8.167 19.3109 13.5561 8.6371 9.7009 12.969 25.2479 11.197 40.18-1.724 14.5259-6.863 30.14591-13.6879 41.60681-3.3271 5.586-8.8301 11.9549-10.3311 11.9549-.4389 0-1.3849-1.4029-2.1029-3.1169-4.116-9.839-18.2071-26.49491-25.32818-29.93691-6.63397-3.208-9.25091-2.355-13.80897 4.50291-4.31795 6.4961-11.66698 13.4991-17.31103 16.494-10.31098 5.471-20.75796 6.758-43.13297 5.3169-18.58193-1.1979-30.32704-2.6529-49.36797-6.116-16.37707-2.9788-27.53798-3.3508-35.65998-1.1878-18.03399 4.8009-36.86601 22.6599-44.82301 42.5049-2.58505 6.447-2.691 7.1669-2.30905 15.711999.33105 7.418.85698 10.122 3.06498 15.755001 4.01603 10.251 8.26199 16.805001 17.01502 26.272001 22.32096 24.140101 33.88898 45.9380026 32.06408 60.4190034-.82907 6.5740006-3.50912 15.9820006-7.87506 27.6411016-4.90099 13.088001-5.04206 16.205001-1.07203 23.644001 4.27402 8.008001 10.37102 12.179001 22.72202 15.544001 3.66306.998 7.727 2.361 9.03202 3.029l2.37399 1.213-1.94803 3.472c-3.04497 5.426901-13.34996 14.474001-21.03296 18.466001-16.38598 8.514001-36.79802 9.508001-50.62102 2.464901-5.55703-2.830901-12.35897-10.466901-21.73599-24.397902-4.84702-7.202-10.50901-14.717001-12.581-16.700001-2.072-1.9821-8.01201-5.844-13.20101-8.5821-15.26999-8.056001-20.24599-13.750001-25.33299-28.990002-4.44-13.3020004-3.735-22.24700093 3.432-43.580902l3.551-10.571101-.361-11.07c-.299-9.210001-.692-11.825001-2.334-15.553001-2.315-5.259-5.55-8.2601-9.749-9.047001-9.155-1.715-14.093 9.350001-20.453 45.831003-5.554 31.858002-10.86401 47.075002-19.73301 56.545003-5.493 5.864-10.652 7.8711-21.374 8.315-7.90001.327-8.901.557-10.83201 2.485001-2.55899 2.5561-3.392 6.394-2.8 12.9101.544 5.993901 3.427 11.837901 9.095 18.435001 4.89802 5.701 8.06201 12.948001 7.25501 16.616001-2.156 9.807001-25.854 10.220001-45.97801.802-12.186-5.703-21.496-13.202001-27.711-22.322001-6.911-10.140001-8.373-17.960001-5.508-29.443102 1.931-7.735 1.489-13.448-1.373-17.782001-10.632-16.0949005-7.949-38.661002 6.365-53.549002 4.343-4.515901 6.944-6.245001 16.70701-11.105001 14.61-7.274 20.08099-10.911001 25.97599-17.270001 7.886-8.507 14.34-24.869101 12.879-32.649002-.793-4.219-5.715-9.252-10.288-10.5201-5.23-1.449999-13.107-1.133999-17.121.686-2.113.9591-7.73099 5.8041-14.044 12.1121-11.247 11.239001-15.697 14.228001-24.549 16.493001-4.987 1.276-5.901 1.263-13.955-.198-4.74-.861-11.361-1.564-14.713-1.564-5.524 0-6.399.244-9.362 2.61-3.855 3.078-8.229 10.837001-10.054 17.832901-.794 3.0461-1.302 8.714101-1.302 14.548101 0 10.604101-.879 13.648001-4.181 14.476001-4.816 1.207-20.202-4.755-29.09501-11.274001-5.61099-4.114-15.18499-14.536001-18.52999-20.172001-3.869-6.519-7.51701-17.451001-8.50501-25.482001-2.018-16.408001 5.87201-31.347 31.75101-60.1181 8.25799-9.1809 10.45099-12.8749 12.993-21.8908 3.563-12.63401 7.642-18.76101 16.86801-25.33611 6.08499-4.3359 11.93499-6.6639 20.05399-7.98 11.144-1.8059 22.516-.5989 55.681 5.9061 26.121 5.1231 38.328 6.448 47.582 5.163 8.37101-1.163 11.32702-2.588 23.51201-11.331 5.398-3.873 20.82001-13.675 34.27001-21.7809 27.81699-16.7641 29.583-18.2981 38.826-33.7191 2.99999-5.007 14.814-27.512 26.25297-50.0111 29.19398-57.42191 39.50902-76.45991 55.78305-102.96191 8.03497-13.084 9.79202-16.917 10.49498-22.8941.39199-3.3289.15103-5.2619-.943-7.564-2.68702-5.6559-8.75595-7.942-27.34401-10.2989-5.66996-.7191-13.71299-2.2851-17.87399-3.4791-9.99703-2.8689-22.15399-8.8389-25.86504-12.702-2.69697-2.806-2.83697-3.1799-1.47496-3.9459 12.07896-6.7901 43.73999-36.978 71.21204-67.899 50.11601-56.40801 88.41093-118.72001 116.41995-189.43401 13.96399-35.25401 17Line truncated
</svg>

Before

Width:  |  Height:  |  Size: 19 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 120" fill="none">
<path d="M48 92h68c17 0 30-12 30-29 0-16-12-29-28-29h-4C107 18 93 8 75 8 50 8 31 27 31 52v3C18 58 10 68 10 81c0 7 3 11 8 11h12" stroke="#666" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 108 136 12" stroke="#666" stroke-width="13" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 377 B

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 89">
<path fill="#666" d="m18.4003 8.5c-6.2 4.2-7.029 12.8268-1.7007 18.3999l2.2002 2.3013-3.7002 3.5991c-3.7898 3.6863-4.9641 6.3366-4.6991 10.8505.265 4.5138 4.0514 9.3178 11.7773 14.9176 11.8224 8.5692 12.5212 9.3018 12.6006 12.2007.0667 2.4333-1.8696 5.0283-3.7002 6.0996-4.0361 2.362-9.6607.1086-8.6001-4.8999.7395-3.4919-1.0001-6-4.1001-6-4.2 0-6.0102 4.6048-3.6006 9.0996 2.7904 5.2052 15.9001 6.8001 22.1001 2.6001s7.0308-12.8285 1.7007-18.3999l-2.2002-2.2998 3.7002-3.6006c3.4302-3.3378 4.6235-5.9098 4.8299-10.2809.2064-4.3713-3.6116-9.4751-11.9081-15.4872-11.8234-8.5678-12.5448-9.2998-12.6006-12.1992-.0471-2.4515 1.5375-4.8693 3.7002-6.0996 3.9056-2.2218 9.6607-.1086 8.6001 4.8999-.7395 3.4919 1.0001 6 4.1001 6 4.2 0 6.0066-4.6051 3.5991-9.1011-1.7425-3.254-7.523-5.0933-12.0011-5.0136-4.4781.0797-7.7725.8385-10.0975 2.4135zM21.1 31l6.3999 4.8003c3.4879 2.6162 7.2523 5.6546 8.2002 6.7998 2.5504 3.0815 2.7671 7.4482.3779 10.3682l-1.8003 2.2002-6.3999-4.8003c-3.4879-2.6162-7.2493-5.6571-8.2002-6.7998-2.9396-3.5328-2.5681-7.6916-.3779-10.3682z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

-12
View File
@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 573.11 136.81">
<g transform="translate(0 -45.51)">
<path d="M40.05,168.37c-4.3,2.14-10,3.92-14.3,3.92-12.86,0-15.73-8.03-15.73-19.64v-51.8h29.48v-9.1H10.02v-26.42l-9.51,2.09c-.29.07-.51.34-.51.63v9.22h.07v49.8h-.07v25.6c0,18.4,6.61,28.95,25.19,28.95,6.08,0,12.33-1.8,18.22-4.65l-3.4-8.57.03-.02Z"/>
<path d="M94.52,89.96c-26.99,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.77-45.91-45.57-45.91ZM94.52,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.38,15.72,35.38,36.27-14.66,36.25-35.38,36.25Z"/>
<path d="M197.39,89.96c-26.97,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.75-45.91-45.57-45.91ZM197.39,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.4,15.72,35.4,36.27-14.66,36.25-35.4,36.25Z"/>
<path d="M259.33,57.7v122.28h10.19V54.95l-9.68,2.12c-.29.07-.49.34-.49.63h-.02Z"/>
<path d="M439.9,162.7c-8.85,0-13.3-5.4-13.3-15.03v-44.14h27.38v-18.3h-27.38l-.03-27.17-20.82,4.57c-.32.07-.54.36-.54.68v84.37c0,22.73,11.57,34.11,31.22,34.11,7.9,0,14.06-1.36,21.4-4.64l-5.98-17.35c-3.65,1.73-8.1,2.9-11.96,2.9h.02Z"/>
<path d="M516.47,82.49c-9.83,0-17.48,1.27-26.17,6.35-13.35,7.79-20.33,22.31-20.33,37.51v54.08h21.59v-46.44c0-18.69,9.83-30.07,25.25-30.07,5.79,0,10.6,1.55,16,5.21l9.63-17.54c-1.41-1.27-3.02-2.43-4.79-3.43-5.45-3.11-14.21-5.67-21.2-5.67h.02Z"/>
<path d="M552.06,50.13c-.32.07-.54.36-.54.68v129.6h21.59V45.51l-21.06,4.62h.02Z"/>
<path d="M303.47,132.12c0-29.44,20.4-49.65,50.63-49.65,13.86,0,25.8,4.8,35.99,14.62l-13.09,14.04c-5.78-5.77-14.05-9.05-22.52-9.05-17.32,0-29.45,12.31-29.45,30.02s11.94,30.02,29.45,30.02c9.24,0,17.71-3.46,23.49-9.81l13.28,14.43c-10.58,10.2-22.33,15.01-36.96,15.01-30.79,0-50.82-19.63-50.82-49.65v.02Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-12
View File
@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 573.11 136.81">
<g transform="translate(0 -45.51)" fill="#fff">
<path d="M40.05,168.37c-4.3,2.14-10,3.92-14.3,3.92-12.86,0-15.73-8.03-15.73-19.64v-51.8h29.48v-9.1H10.02v-26.42l-9.51,2.09c-.29.07-.51.34-.51.63v9.22h.07v49.8h-.07v25.6c0,18.4,6.61,28.95,25.19,28.95,6.08,0,12.33-1.8,18.22-4.65l-3.4-8.57.03-.02Z"/>
<path d="M94.52,89.96c-26.99,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.77-45.91-45.57-45.91ZM94.52,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.38,15.72,35.38,36.27-14.66,36.25-35.38,36.25Z"/>
<path d="M197.39,89.96c-26.97,0-45.57,19.47-45.57,45.91s18.58,45.89,45.57,45.89,45.57-19.47,45.57-45.89-18.75-45.91-45.57-45.91ZM197.39,172.12c-20.91,0-35.38-15.72-35.38-36.25s14.47-36.27,35.38-36.27,35.4,15.72,35.4,36.27-14.66,36.25-35.4,36.25Z"/>
<path d="M259.33,57.7v122.28h10.19V54.95l-9.68,2.12c-.29.07-.49.34-.49.63h-.02Z"/>
<path d="M439.9,162.7c-8.85,0-13.3-5.4-13.3-15.03v-44.14h27.38v-18.3h-27.38l-.03-27.17-20.82,4.57c-.32.07-.54.36-.54.68v84.37c0,22.73,11.57,34.11,31.22,34.11,7.9,0,14.06-1.36,21.4-4.64l-5.98-17.35c-3.65,1.73-8.1,2.9-11.96,2.9h.02Z"/>
<path d="M516.47,82.49c-9.83,0-17.48,1.27-26.17,6.35-13.35,7.79-20.33,22.31-20.33,37.51v54.08h21.59v-46.44c0-18.69,9.83-30.07,25.25-30.07,5.79,0,10.6,1.55,16,5.21l9.63-17.54c-1.41-1.27-3.02-2.43-4.79-3.43-5.45-3.11-14.21-5.67-21.2-5.67h.02Z"/>
<path d="M552.06,50.13c-.32.07-.54.36-.54.68v129.6h21.59V45.51l-21.06,4.62h.02Z"/>
<path d="M303.47,132.12c0-29.44,20.4-49.65,50.63-49.65,13.86,0,25.8,4.8,35.99,14.62l-13.09,14.04c-5.78-5.77-14.05-9.05-22.52-9.05-17.32,0-29.45,12.31-29.45,30.02s11.94,30.02,29.45,30.02c9.24,0,17.71-3.46,23.49-9.81l13.28,14.43c-10.58,10.2-22.33,15.01-36.96,15.01-30.79,0-50.82-19.63-50.82-49.65v.02Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

+20 -48
View File
@@ -3,42 +3,21 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Datenschutzerklärung — ToolCTRL</title> <title>Datenschutzerklärung: ToolCTRL</title>
<meta name="description" content="Datenschutzerklärung von ToolCTRL — Informationen zur Verarbeitung personenbezogener Daten auf dieser Website." /> <meta name="description" content="Datenschutzerklärung von ToolCTRL: Informationen zur Verarbeitung personenbezogener Daten auf dieser Website." />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<link rel="canonical" href="https://tool-ctrl.de/datenschutz" /> <link rel="canonical" href="https://tool-ctrl.de/datenschutz.html" />
<link rel="icon" type="image/png" href="/assets/favicon.png" /> <link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin /> <link rel="stylesheet" href="/styles.css?v=20260610" />
<link rel="stylesheet" href="/styles.css?v=20260564" />
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
<div class="wrap header-inner"> <div class="wrap header-inner">
<a class="brand" href="/" aria-label="ToolCTRL Startseite"> <a class="brand" href="/" aria-label="ToolCTRL Startseite">
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<nav class="nav" aria-label="Hauptnavigation"> <a class="legal-back" href="/">Zurück zur Startseite</a>
<a href="/#leistungen">Leistungen</a>
<a href="/#branchen">Branchen</a>
<a href="/#vorgehen">Vorgehen</a>
<a href="/#arbeit">Organisation</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid btn-sm" href="/#kontakt">Ablauf prüfen</a>
</nav>
<button class="nav-toggle" aria-label="Menü" aria-expanded="false">
<span></span><span></span>
</button>
</div>
<div class="mobile-nav" hidden>
<a href="/#leistungen">Leistungen</a>
<a href="/#branchen">Branchen</a>
<a href="/#vorgehen">Vorgehen</a>
<a href="/#arbeit">Organisation</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid" href="/#kontakt">Ablauf prüfen</a>
</div> </div>
</header> </header>
@@ -57,18 +36,19 @@
</p> </p>
<p> <p>
R&amp;D² UG (haftungsbeschränkt)<br /> R&amp;D² UG (haftungsbeschränkt)<br />
Orbinstr. 1<br /> Orbinstraße 1<br />
76646 Bruchsal<br /> 76646 Bruchsal<br />
E-Mail: <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a> E-Mail: <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
</p> </p>
<p>Die vollständigen Anbieterangaben finden Sie im <a href="/impressum">Impressum</a>.</p> <p>Die vollständigen Anbieterangaben finden Sie im <a href="/impressum.html">Impressum</a>.</p>
<h2>2. Hosting und Server-Logfiles</h2> <h2>2. Hosting und Server-Logfiles</h2>
<p> <p>
Diese Website wird bei einem externen Dienstleister gehostet: Diese Website wird bei einem externen Dienstleister gehostet:
Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen, Deutschland. Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen, Deutschland.
Beim Aufruf der Website verarbeitet der Hosting-Anbieter automatisch technische Beim Aufruf der Website verarbeitet der Hosting-Anbieter automatisch
Informationen, die Ihr Browser übermittelt (sogenannte Server-Logfiles): technische Informationen, die Ihr Browser übermittelt (sogenannte
Server-Logfiles):
</p> </p>
<ul> <ul>
<li>IP-Adresse des anfragenden Geräts</li> <li>IP-Adresse des anfragenden Geräts</li>
@@ -175,24 +155,16 @@
</p> </p>
</main> </main>
<footer class="site-footer"> <footer class="legal-footer">
<div class="wrap footer-bar"> <div class="wrap legal-footer-inner">
<div class="footer-brand"> <span>© 2026 ToolCTRL</span>
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" /> <span>
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p> <a href="/">Startseite</a> ·
</div> <a href="/impressum.html">Impressum</a> ·
<div class="footer-links"> <a href="/datenschutz.html">Datenschutz</a>
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a> </span>
<i></i>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
<i></i>
<a href="/impressum">Impressum</a>
·
<a href="/datenschutz">Datenschutz</a>
</div>
</div> </div>
</footer> </footer>
<script src="/script.js?v=20260554"></script>
</body> </body>
</html> </html>
+89 -253
View File
@@ -3,43 +3,25 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI GameJam — Human in the Loop · TechDay Bruchsal 2026</title> <title>AI GameJam: Human in the Loop, TechDay Bruchsal 2026</title>
<meta name="description" content="AI GameJam beim TechDay Bruchsal 2026: In zwei Stunden mit KI ein spielbares Game bauen. Human in the Loop." /> <meta name="description" content="AI GameJam von ToolCTRL beim TechDay Bruchsal 2026: In wenigen Stunden mit KI ein spielbares Game bauen. Human in the Loop, im HubWerk01 Bruchsal.">
<link rel="canonical" href="https://tool-ctrl.de/gamejam"> <link rel="canonical" href="https://tool-ctrl.de/gamejam.html">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:locale" content="de_DE">
<meta property="og:site_name" content="ToolCTRL"> <meta property="og:site_name" content="ToolCTRL">
<meta property="og:title" content="AI GameJam — Human in the Loop"> <meta property="og:locale" content="de_DE">
<meta property="og:description" content="Zwei Stunden, KI, ein spielbares Ergebnis: die AI GameJam beim TechDay Bruchsal 2026."> <meta property="og:url" content="https://tool-ctrl.de/gamejam.html">
<meta property="og:url" content="https://tool-ctrl.de/gamejam"> <meta property="og:title" content="AI GameJam: Human in the Loop, TechDay Bruchsal 2026">
<meta property="og:image" content="https://tool-ctrl.de/assets/logo-a.webp"> <meta property="og:description" content="AI GameJam von ToolCTRL beim TechDay Bruchsal 2026: In wenigen Stunden mit KI ein spielbares Game bauen. Human in the Loop, im HubWerk01 Bruchsal.">
<meta property="og:image" content="https://tool-ctrl.de/assets/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="ToolCTRL Logo mit Tagline: Die Software, die eure Branche längst verdient hat.">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="theme-color" content="#0c0c0e"> <link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610">
<link rel="icon" type="image/png" href="/assets/favicon.png"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style> <style>
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url(/assets/fonts/space-grotesk-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url(/assets/fonts/space-grotesk-latin-ext.woff2) format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
:root { :root {
--bg: #ffffff; --bg: #ffffff;
--bg2: #f5f5f6; --bg2: #f5f5f6;
@@ -127,18 +109,15 @@
.hero .kicker { .hero .kicker {
font-family: 'Space Mono', monospace; font-size: 13px; letter-spacing: 4px; font-family: 'Space Mono', monospace; font-size: 13px; letter-spacing: 4px;
text-transform: uppercase; color: var(--ac); margin-bottom: 26px; text-transform: uppercase; color: var(--ac); margin-bottom: 26px;
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) both;
} }
.hero h1 { .hero h1 {
font-size: clamp(52px, 11vw, 142px); font-weight: 700; line-height: 0.93; font-size: clamp(52px, 11vw, 142px); font-weight: 700; line-height: 0.93;
letter-spacing: -0.04em; text-transform: uppercase; margin-bottom: 24px; letter-spacing: -0.04em; text-transform: uppercase; margin-bottom: 24px;
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) .04s both;
} }
.hero h1 .ln2 { color: var(--ac); } .hero h1 .ln2 { color: var(--ac); }
.hero .hsub { .hero .hsub {
font-size: clamp(15px, 2vw, 19px); color: var(--muted); font-size: clamp(15px, 2vw, 19px); color: var(--muted);
max-width: 540px; margin-bottom: 56px; max-width: 540px; margin-bottom: 56px;
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) .08s both;
} }
.hero .hsub b { color: var(--text); font-weight: 600; } .hero .hsub b { color: var(--text); font-weight: 600; }
@@ -147,7 +126,6 @@
position: relative; background: var(--panel); position: relative; background: var(--panel);
border: 1px solid var(--line2); padding: 36px 56px; border: 1px solid var(--line2); padding: 36px 56px;
min-width: min(560px, 92vw); min-width: min(560px, 92vw);
animation: heroRise .26s cubic-bezier(.2, 0, 0, 1) .12s both;
} }
.hud .cn { .hud .cn {
position: absolute; width: 14px; height: 14px; position: absolute; width: 14px; height: 14px;
@@ -175,10 +153,6 @@
font-family: 'Space Mono', monospace; font-weight: 700; font-family: 'Space Mono', monospace; font-weight: 700;
font-size: clamp(54px, 11vw, 108px); line-height: 1; font-size: clamp(54px, 11vw, 108px); line-height: 1;
letter-spacing: -0.02em; color: var(--text); letter-spacing: -0.02em; color: var(--text);
font-variant-numeric: tabular-nums;
}
.hud .timer.tick {
animation: timerTick .16s cubic-bezier(.2, 0, 0, 1);
} }
.hud .tl { .hud .tl {
font-family: 'Space Mono', monospace; font-size: 11px; letter-spacing: 2px; font-family: 'Space Mono', monospace; font-size: 11px; letter-spacing: 2px;
@@ -186,7 +160,6 @@
} }
.hmeta { .hmeta {
margin-top: 44px; display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; margin-top: 44px; display: flex; gap: 14px; flex-wrap: wrap; justify-content: center;
animation: heroRise .24s cubic-bezier(.2, 0, 0, 1) .16s both;
} }
.hmeta span { .hmeta span {
display: inline-flex; align-items: center; gap: 8px; display: inline-flex; align-items: center; gap: 8px;
@@ -220,7 +193,6 @@
.tl-row.done { opacity: 0.38; } .tl-row.done { opacity: 0.38; }
.tl-row.active { .tl-row.active {
background: var(--panel); border-left-color: var(--ac); background: var(--panel); border-left-color: var(--ac);
box-shadow: inset 10px 0 18px -18px rgba(0,0,0,.45);
} }
.tl-row.active .tt { color: var(--ac); } .tl-row.active .tt { color: var(--ac); }
.tl-row.active .tn { color: var(--text); } .tl-row.active .tn { color: var(--text); }
@@ -301,159 +273,41 @@
.field input { .field input {
width: 100%; background: var(--bg2); border: 1px solid var(--line2); width: 100%; background: var(--bg2); border: 1px solid var(--line2);
padding: 13px 15px; color: var(--text); font-size: 15px; font-family: inherit; padding: 13px 15px; color: var(--text); font-size: 15px; font-family: inherit;
transition: border-color .15s ease, background .15s ease;
} }
.field input:focus { outline: none; border-color: var(--ac); background: #fff; } .field input:focus { outline: none; border-color: var(--ac); }
.dropzone { .dropzone {
border: 1.5px dashed var(--line2); padding: 40px 24px; text-align: center; border: 1.5px dashed var(--line2); padding: 40px 24px; text-align: center;
cursor: pointer; cursor: pointer; transition: all .15s; background: var(--bg2);
transition: border-color .15s ease, background .15s ease, transform .15s ease;
background: var(--bg2);
}
.dropzone:hover, .dropzone.drag {
border-color: var(--ac);
background: var(--panel);
transform: translateY(-2px);
} }
.dropzone:hover, .dropzone.drag { border-color: var(--ac); background: var(--panel); }
.dropzone .dzi { color: var(--ac); margin-bottom: 12px; } .dropzone .dzi { color: var(--ac); margin-bottom: 12px; }
.dropzone .dzi svg.ic { width: 28px; height: 28px; } .dropzone .dzi svg.ic { width: 28px; height: 28px; }
.dropzone .big { font-size: 15px; font-weight: 600; margin-bottom: 4px; } .dropzone .big { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
.dropzone .sm { color: var(--muted); font-size: 13px; font-family: 'Space Mono', monospace; } .dropzone .sm { color: var(--muted); font-size: 13px; font-family: 'Space Mono', monospace; }
.btn { .btn {
position: relative; isolation: isolate; overflow: visible; background: var(--ac); color: #ffffff; border: none;
background: linear-gradient(180deg, #3c3c3f 0%, #161617 60%, #070708 100%);
color: #ffffff; border: 2px solid #0c0c0e; border-radius: 7px;
padding: 13px 30px; font-size: 14px; font-weight: 700; cursor: pointer; padding: 13px 30px; font-size: 14px; font-weight: 700; cursor: pointer;
font-family: inherit; text-transform: uppercase; letter-spacing: 1px; font-family: inherit; text-transform: uppercase; letter-spacing: 1px;
box-shadow: 0 7px 0 #000000, 0 11px 0 #343437, 0 17px 18px -10px rgba(0,0,0,.48), transition: opacity .15s;
inset 0 2px 0 rgba(255,255,255,.22);
transition: box-shadow .12s ease, transform .12s ease, background .15s ease;
} }
.btn::after { .btn:hover { opacity: 0.82; }
content: ""; position: absolute; left: 5px; right: -6px; bottom: -8px; .btn:disabled { background: var(--line2); color: var(--muted); cursor: not-allowed; }
height: 8px; z-index: -1; border: 2px solid #0c0c0e; border-top: 0;
border-radius: 0 0 7px 7px; background: #0c0c0e;
transition: height .12s ease, bottom .12s ease;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 9px 0 #000000, 0 13px 0 #343437, 0 19px 20px -10px rgba(0,0,0,.5),
inset 0 2px 0 rgba(255,255,255,.22); }
.btn:active { transform: translateY(5px) scale(.985); box-shadow: 0 2px 0 #000000, 0 4px 0 #343437, 0 9px 14px -9px rgba(0,0,0,.45); }
.btn:active::after { bottom: -3px; height: 3px; }
.btn:disabled { background: var(--line2); color: var(--muted); cursor: not-allowed; box-shadow: none; }
.btn:disabled::after { display: none; }
.status { .status {
font-family: 'Space Mono', monospace; font-size: 13px; margin-top: 16px; min-height: 20px; font-family: 'Space Mono', monospace; font-size: 13px; margin-top: 16px; min-height: 20px;
} }
.status.ok { color: var(--ac); } .status.ok { color: var(--ac); }
.status.err { color: #0c0c0e; font-weight: 700; } .status.err { color: #c0392b; }
/* ===== GALLERY ===== */ /* ===== GALLERY ===== */
.btn-ghost { .btn-ghost {
position: relative; isolation: isolate; overflow: visible; background: var(--panel); border: 1px solid var(--line2); color: var(--text);
background: linear-gradient(180deg, #ffffff 0%, #f1f1f2 62%, #dfdfe1 100%);
border: 2px solid var(--text); border-radius: 6px; color: var(--text);
padding: 10px 16px; font-size: 12px; font-weight: 600; cursor: pointer; padding: 10px 16px; font-size: 12px; font-weight: 600; cursor: pointer;
font-family: 'Space Mono', monospace; text-transform: uppercase; letter-spacing: 1px; font-family: 'Space Mono', monospace; text-transform: uppercase; letter-spacing: 1px;
display: inline-flex; align-items: center; gap: 8px; display: inline-flex; align-items: center; gap: 8px;
box-shadow: 0 5px 0 #000000, 0 8px 0 #bbbbbf, 0 13px 16px -10px rgba(0,0,0,.36);
transition: box-shadow .12s ease, transform .12s ease;
} }
.btn-ghost::after { .btn-ghost:hover { border-color: var(--ac); }
content: ""; position: absolute; left: 4px; right: -5px; bottom: -7px;
height: 7px; z-index: -1; border: 2px solid var(--text); border-top: 0;
border-radius: 0 0 6px 6px; background: #d9d9dc;
transition: height .12s ease, bottom .12s ease;
}
.btn-ghost:hover { transform: translateY(-2px); box-shadow: 0 7px 0 #000000, 0 10px 0 #bbbbbf, 0 15px 18px -10px rgba(0,0,0,.38); }
.btn-ghost:active { transform: translateY(4px) scale(.985); box-shadow: 0 1px 0 #000000, 0 3px 0 #bbbbbf, 0 7px 12px -9px rgba(0,0,0,.34); }
.btn-ghost:active::after { bottom: -3px; height: 3px; }
.btn-ghost svg.ic { width: 13px; height: 13px; } .btn-ghost svg.ic { width: 13px; height: 13px; }
.btn,
.btn-ghost {
max-width: 100%;
min-height: 40px;
line-height: 1.1;
white-space: normal;
text-align: center;
justify-content: center;
}
.btn::after,
.btn-ghost::after { display: none; }
.btn {
display: inline-flex; align-items: center;
box-shadow: 0 5px 0 #000000, 0 8px 12px -8px rgba(0,0,0,.48),
inset 0 2px 0 rgba(255,255,255,.22);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 0 #000000, 0 10px 14px -9px rgba(0,0,0,.5),
inset 0 2px 0 rgba(255,255,255,.22);
}
.btn:active {
transform: translateY(3px) scale(.96);
box-shadow: 0 2px 0 #000000, 0 7px 10px -9px rgba(0,0,0,.45);
}
.btn-ghost {
box-shadow: 0 5px 0 #000000, 0 8px 12px -8px rgba(0,0,0,.32);
}
.btn-ghost:hover {
transform: translateY(-1px);
box-shadow: 0 6px 0 #000000, 0 10px 14px -9px rgba(0,0,0,.34);
}
.btn-ghost:active {
transform: translateY(3px) scale(.96);
box-shadow: 0 2px 0 #000000, 0 7px 10px -9px rgba(0,0,0,.3);
}
.btn,
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
max-width: 100%;
min-height: 40px;
padding: 11px 20px;
border: 2px solid var(--text);
border-radius: 6px;
line-height: 1.1;
white-space: normal;
text-align: center;
overflow-wrap: anywhere;
transform: translateY(0);
box-shadow: 0 4px 0 #000000, 0 10px 16px -12px rgba(0,0,0,.42);
}
.btn::before,
.btn::after,
.btn-ghost::before,
.btn-ghost::after {
display: none;
}
.btn:hover,
.btn-ghost:hover {
transform: translateY(-1px);
box-shadow: 0 5px 0 #000000, 0 12px 18px -12px rgba(0,0,0,.44);
}
.btn:active,
.btn-ghost:active {
transform: translateY(2px) scale(.98);
box-shadow: 0 1px 0 #000000, 0 6px 10px -10px rgba(0,0,0,.4);
}
.btn {
border-color: #ffffff;
box-shadow: 0 4px 0 #9f9f9f, 0 10px 16px -12px rgba(255,255,255,.38),
inset 0 2px 0 rgba(255,255,255,.24);
}
.btn:hover {
box-shadow: 0 5px 0 #bcbcbc, 0 12px 18px -12px rgba(255,255,255,.42),
inset 0 2px 0 rgba(255,255,255,.24);
}
.btn:active {
box-shadow: 0 1px 0 #bcbcbc, 0 6px 10px -10px rgba(255,255,255,.34);
}
.games { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 6px; } .games { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 6px; }
.game { background: var(--panel); border: 1px solid var(--line); } .game { background: var(--panel); border: 1px solid var(--line); }
.game .frame { width: 100%; aspect-ratio: 16/10; border: none; background: var(--panel2); display: block; } .game .frame { width: 100%; aspect-ratio: 16/10; border: none; background: var(--panel2); display: block; }
@@ -485,22 +339,6 @@
font-family: 'Space Mono', monospace; color: var(--muted); font-size: 12px; font-family: 'Space Mono', monospace; color: var(--muted); font-size: 12px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
footer .links {
display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 14px;
font-family: 'Space Mono', monospace; font-size: 12px; letter-spacing: .5px;
color: var(--muted);
}
footer .links a { color: var(--muted); text-decoration: none; }
footer .links a:hover { color: var(--text); }
@keyframes heroRise {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes timerTick {
from { opacity: .78; }
to { opacity: 1; }
}
@media (max-width: 820px) { @media (max-width: 820px) {
.steps { grid-template-columns: 1fr; } .steps { grid-template-columns: 1fr; }
@@ -514,17 +352,6 @@
section { padding: 70px 0; } section { padding: 70px 0; }
.topbar .ev { display: none; } .topbar .ev { display: none; }
} }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*,
*::before,
*::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
}
}
</style> </style>
</head> </head>
<body> <body>
@@ -533,12 +360,12 @@
<div class="topbar"> <div class="topbar">
<div class="inner"> <div class="inner">
<div class="left"> <div class="left">
<a href="/" aria-label="Zur ToolCTRL-Startseite"><img class="tc" src="/assets/wordmark-black.svg" alt="ToolCTRL"></a> <a href="/" aria-label="Zur ToolCTRL-Startseite"><img class="tc" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNzEzLjMyIDY3OC42MSI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNSk7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNik7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNCk7CiAgICAgIH0KCiAgICAgIC5jbHMtNCB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMyk7CiAgICAgIH0KCiAgICAgIC5jbHMtNSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMik7CiAgICAgIH0KCiAgICAgIC5jbHMtNiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQpOwogICAgICB9CiAgICA8L3N0eWxlPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSI2NTguOTYiIHkxPSIzMS45NiIgeDI9IjI5Ny43IiB5Mj0iNDU4LjYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDAwIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzMzMyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNjg0LjMiIHkxPSIxMTQyLjc1IiB4Mj0iMjIwNi4wOSIgeTI9IjE1NTMuNTEiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE4MzMuNzQgLTExMDguOTYpIHJvdGF0ZSg1LjA2KSBza2V3WCgxNC4yNSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYjJiMmIyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iLjc4IiBzdG9wLWNvbG9yPSIjZmZmIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMyIgeDE9IjE3MTkuMTMiIHkxPSIxMDk4LjUxIiB4Mj0iMjI0MC45MSIgeTI9IjE1MDkuMjgiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQtMiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNCIgeDE9IjE3NjQuODIiIHkxPSIxMDQwLjQ3IiB4Mj0iMjI4Ni42IiB5Mj0iMTQ1MS4yNCIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC01IiB4MT0iMTgwNi4yNyIgeTE9Ijk4Ny44MiIgeDI9IjIzMjguMDUiIHkyPSIxMzk4LjU5IiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50LTIiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTYiIHgxPSIzNDUuMDMiIHkxPSItMTAuMTUiIHgyPSIxMDE0LjU1IiB5Mj0iNTE2LjkxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgPC9kZWZzPgogIDxnPgogICAgPHBhdGggZD0iTTY4OC43LDU3Mi4wOWwtMTYwLDk4Ljk1Yy05LjE0LDUuNzMtMTkuOTksOC4zNS0zMC43Nyw3LjM3bC0zODAuNjgtMzQuMzZjLTIyLjQ4LTIuMDEtNDAuNzYtMTkuMDEtNDQuMzUtNDEuMzFMLjY0LDE1OS4zMWMtMy4xNy0xOS41Niw1LjYtMzkuMTIsMjIuMy00OS43MkwxODkuNDYsOC4wOGMtMTMuOTUsOS43NSwyMy43NywzMS41NSwyNi44OCw0OS45NWw5MS4zLDM2MS4xM2M2LjIxLDM2LjY4LDM2LjM3LDY0LjU4LDczLjQyLDY3LjkzbDI3OC42NCw0Mi45YzguNjUuNzksMjEuODcsNDUuNjgsMjguOTQsNDIuMDNsLjA2LjA2WiIvPgogICAgPHBhdGggY2xhc3M9ImNscy02IiBkPSJNNjg4LjY0LDU3Mi4wM2MtNy4wNywzLjY2LTE1LjIzLDUuMzYtMjMuODgsNC41N2wtMzQ5LTMxLjVjLTM3LjA0LTMuMzUtNjcuMjEtMzEuMjYtNzMuNDItNjcuOTNMMTcwLjM5LDUzLjM1Yy0zLjExLTE4LjQsNS4xMi0zNS41MiwxOS4wNy00NS4yN2wxLjk1LTEuMTZjNy45OC01LjA2LDE3LjYxLTcuNjIsMjcuOTEtNi43NmwzNTguNTYsMzAuMzRjMjkuNDksMi41LDUzLjU2LDI0LjYyLDU4LjYxLDUzLjhsNzYuMTYsNDQwLjM5YzMuNTMsMjAuMjktNy4xOSwzOC45OS0yNC4wMSw0Ny4zNFoiLz4KICA8L2c+CiAgPGc+CiAgICA8Zz4KICAgICAgPHBhdGggY2xhc3M9ImNscy01IiBkPSJNMzAwLjIyLDM0My45M2wtMTUuMDEsNC4yNiwxNS43Myw5Ni4wNWM0LjIzLDI1LjgzLDIxLjg4LDQyLjU5LDQ3LjIxLDQ0LjgzbDIwLjQyLDEuODEtMi40OC0xNS4xNC0yMC40Mi0xLjgxYy0xNS44MS0xLjQtMjYuMDQtMTEuNDQtMjguNzktMjguMjhsLTguMjQtNTAuMzQsNDQuOTMsMy45Ny0yLjQ1LTE0Ljk0LTQ0LjkzLTMuOTctNS45Ny0zNi40M1oiLz4KICAgICAgPHBhdGggY2xhc3M9ImNscy00IiBkPSJNNDIxLjk2LDM4Ny45N2MtMzEuODgtMi44Mi01MS4yNiwxOC4xLTQ2LjA4LDQ5LjczLDUuMTgsMzEuNjQsMzIuMDgsNTYuNjUsNjMuOTcsNTkuNDcsMzEuODgsMi44Miw1MS4yNi0xOC4xLDQ2LjA4LTQ5LjczLTUuMTgtMzEuNjQtMzIuMDgtNTYuNjUtNjMuOTctNTkuNDdaTTQzNy4zNiw0ODIuMDNjLTIyLjktMi4wMy00MS41NC0xOS42Ny00NS4zNS00Mi45LTMuOC0yMy4yMyw5LjUzLTM4LjA0LDMyLjQzLTM2LjAyLDIyLjksMi4wMyw0MS41NCwxOS42Niw0NS4zNSw0Mi45LDMuOCwyMy4yMy05LjUzLDM4LjA0LTMyLjQyLDM2LjAyWiIvPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik01NDEuMzcsMzk4LjUzYy0zMS44OC0yLjgyLTUxLjI2LDE4LjEtNDYuMDgsNDkuNzMsNS4xOCwzMS42NCwzMi4wOCw1Ni42NSw2My45Nyw1OS40NywzMS44OCwyLjgyLDUxLjI2LTE4LjEsNDYuMDgtNDkuNzMtNS4xOC0zMS42NC0zMi4wOC01Ni42NS02My45Ny01OS40N1pNNTU2Ljc3LDQ5Mi41OWMtMjIuOS0yLjAzLTQxLjU0LTE5LjY3LTQ1LjM1LTQyLjktMy44LTIzLjIzLDkuNTMtMzguMDQsMzIuNDMtMzYuMDIsMjIuOSwyLjAzLDQxLjU0LDE5LjY2LDQ1LjM1LDQyLjksMy44LDIzLjIzLTkuNTMsMzguMDQtMzIuNDIsMzYuMDJaIi8+CiAgICAgIDxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI2MDcuNjcgMzY4LjQxIDYzMS4xIDUxMS40NiA2NDcuMDQgNTEyLjg3IDYyMi42OCAzNjQuMTUgNjA3LjY3IDM2OC40MSIvPgogICAgPC9nPgogICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTgyLjM5LDE2NC4wN2wxOC4zLDEuNjItNTYuMjktNzUuMTJjLTMuMTUtNC4yLTcuOTMtNi44Ni0xMy4xNi03LjMzaDBjLTcuNjEtLjY3LTE0Ljg2LDMuNDEtMTguMjIsMTAuMjdsLTMwLjMsNjEuNzUsMTkuMiwxLjcsMjcuNTctNTYuMmMuOTktMi4wMSwzLjc1LTIuMjcsNS4xLS40OGw0Ny44MSw2My43OVoiLz4KICA8L2c+Cjwvc3ZnPg==" alt="ToolCTRL"></a>
<a class="back" href="/"><svg class="ic" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Startseite</a> <a class="back" href="/"><svg class="ic" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Startseite</a>
</div> </div>
<div class="right"> <div class="right">
<span class="ev">TechDay Bruchsal · 27.05.2026</span> <span class="ev">TechDay Bruchsal · 27.05.2026</span>
<img class="mit" src="/assets/mitup-logo.webp" alt="mITup Bruchsal"> <a href="https://www.linkedin.com/company/it-meetup-bruchsal/" target="_blank" rel="noopener" aria-label="mITup Bruchsal auf LinkedIn"><img class="mit" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAIAAADdvvtQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA7XRFWHRYTUw6Y29tLmFkb2JlLnhtcAA8P3hwYWNrZXQgYmVnaW49IiIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJHbyBYTVAgU0RLIDEuMCI+PHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj48L3JkZjpSREY+PC94OnhtcG1ldGE+Cjw/eHBhY2tldCBlbmQ9InciPz60gmxpAAAAtGVYSWZJSSoACAAAAAYAEgEDAAEAAAABAAAAGgEFAAEAAABWAAAAGwEFAAEAAABeAAAAKAEDAAEAAAACAAAAEwIDAAEAAAABAAAAaYcEAAEAAABmAAAAAAAAAEgAAAABAAAASAAAAAEAAAAGAACQBwAEAAAAMDIxMAGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA//8AAAKgBAABAAAAwAAAAAOgBAABAAAAwAAAAAAAAABW20EPAAAgAElEQVR4nO19CXhU1fn+3G32fZKQQEJIgCQk7IECiYhsNQGEIgJaltDaPoraqtW/YhW62V9bLVCVihsFIiqiQgsIaIUAGmWJbIFsLFkgQtbJ7Mude+//ufdLxjGZmQxzw1bO+/SpeYZ7zz333Pd85zvfdrDU1AESBIRogUd9JwICIhCCWCAJhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKiEAIooAIhCAKpOSWBYZhON7NBGBZluM4fqLgOIZh17pLHMexLCu5nXCrEgjDMJ/PZ7fbw1zDcZxCoZDL5RKJxOl0ut3ua8ohjuPkcrlCoQDK3ia4JQmEYRhN07169XrggftDfS2O42QyWUlJyenTZzAMGzVq1LBhQz0ezzXiEMdxUqm0vLz8yJGjUqn09uHQLUkgHMedTmdubs5f/vJ/DMMQBNH1GpZlcRxfuvTREydOMgybl/fjpUuX+nw+krwmr8wI3SgsfPfgwS/lcjnDMJLbA7ckgQQhhKenpzMMYzabuxKI4zgcxz0ez6lTp6RSqcvlcjpdDMO0tbUFZZt4MAyj1+ucTud10LRuKtySBGJZViaTDh6cJZFIKIrqqkqzLKtUKmpra5uamiiKcjqdBIETBEGS5DUiEIZhJBmkJ//zwG9RBSguLjYpKcnr9Qad8RzHkSR19uw5i8VCkteEMQi3NoGSkvrGxcXRNB1qycAw7PTp0yzLSSS315pynXGrEigrK1Mul4cyuoACVFlZRRD47bMhusV0oKCaR480xQkIf31GRkYo2SOsX2Rra+v58+dlMhk0Be2Fb7lb/TfMvVx7y1yY97ra8cEEdHlEyAu6bc3fjR6cVNETyOl0Bo4IhmEKhSKKdjiOC7QHchxHUZT/w3cFwzAqlSojIz3U+gUmGdCgSZKEcScI/ltSFBVKieY4rtu9N0mSoT4YLli6cZzw95plWafTGXiNXC6PXMvGMMwrAJ4Is0Imk4W6IDzg7YRxIGQCxEx4sQSCrv/mN0+kp2e4XC4YmsrKytdeW0NR1FWxG+jy/PO/jYmJoWma4ziVSnXgwMFNmzYplcqubwiP7tOnT9++fcMQiCTJysoqh8Oh1+tZllWpVB9/vPXw4aNBb8Fx3G53jBs39plnnnY4HEG/sbCtU65Z88+iov0qlaprxzjhoZcvX1YqeUs0TdN9+/Z94onHMQwDmwJN06tWrb5y5UokQyR0yT5jxozZs3/icDgkEolCoTh58tSbb74JVkocxx0Ox9133z1v3ly73R6BS4dxu90NDY0VFRVnzpRVV1cTBKFSqcTbq6IkkM/nmzBhwqhRoxiGgck3ffq0gwe/LCkpCTq+QUEQRFtb23333ffggz8Hux+Y4+x2+/r164OSA5Sb1NQUo9HgdLpCDRzHcWfOlPn/JkmypqamqqoqaJvQDa1WDeIqTG/Lyyv27dun1+uDjjvXITtBTOp0ujlz7gUCAe/ffvvt+nomEjs1juNerzcjI2PatHx4FkEQGo3m9ddfhwugwbS0gfn5eVdlHfX5fC0tLcXFxW+++c6JE8e1Wq1IORQNgWBE7Ha71+sF+jMMYzDof/azJUeOHIm8HViMlixZHNiOXq8Pb45jWTYjI4OipBz3gwXCDxzH3W53RUW5nxCwqAXK/0AQBOHzMZH4sORyuUajUavVoSYuJwA6D3ZLIBBBEG63m2HYCFUWaMTtdrMs29bWJpFINBqNsNB/f7twgYdlWYvFAutyt65cmOoqleree2dPmTLlD3/403vvvafRaMRwKDoJBL0hALjwl8PhnDp1Snb2yOPHT0QihIR5b5k1a2Z2drbdbqcoyv97GIEMH2PIkMFhXGBSqfTy5csXL14KnOth1GcMw1iWYVmu248LXwjQzaUSvlmCIPwEEv5u1+UjBI7z4QZADpIkhWHhgl5AEAQI2lBqKHTD6XTSNM2yrNncRlHUypUvEwS+cWOhTqeLei0je9aWX1Cw+Ntvj0UyzViWVSjkBQWLI38EhmEgolJT+4dRgCiKqqmpaWpqksvlt8kenuvYN3z44ZagSp5are7XL3ns2LGxsbFWq40kSYZh7Hb7ihXLS0tLT50qDapxXlcC4Thus9nz8vKGDx9eWtpNhwiCsFgs+fn5Y8eOiUQH9AOU06SkxDA2aIIgzpwp83q9SqXyNnFqcsK0uXz5yiuvvBp0myno3cSAAanPPbcsLy8P9go0Tev1+od5LL3xhkQQD1qtdvHihT6fL/xiz7KsVCpdsmQxiNYI5QSolunpaRqNhmGYUI9gWbaiokJym4HjlzDCYDDog8FgMOh02traul/+8uFdu3aDGicoHo7x48dnZPC76egceT1GINhb2u32adOmDR482Ol0huoQXHbnneNzcnJA/ERuDQMNOsx2iSRJm81WWVl5WwXlADiOVyTCQKVS4Ti2atUqm41fxWBTZjQasrNHRh0phfcge/xSceHCBeENXARBFBQUAA8EAkX0FJBbmZmDWDb4dqZDkl+ur6+/WovU7QCfz6dUKs+fv3Ds2DGFQiE4CnmkpaVFHUcghkDffx4cx4uLv/Z4PBRF2e32mTPvycjICGqnEVQlW25uzoQJd9psNqlUajabDx8+KpPJwitxsEQajYb+/UNq0MCw8+fPm81t4Y06ty0wDHO5XGfPnhM2bvyAsyybkJAA+7gbI4EYhlGr1fv27Ssu/lqtVns8HpPJtGDBT0NJRRzHCwoKpFIKJsSnn+46efJktysOhmEejycpKalXr15erzfMZWVl5d0qYbc3MJvN1uEd43VQlUoV9XCJIdAPHknTvg0bNrIsC6rZ7Nk/SUsb4HK5AnsG2s+YMT+aOPEuED8Oh6Ow8F2pVBqZ+ZsZOHAgKIBBX1iQUr7Tp8/g+G0UA8R9P+8iFCH8nt9/I4bxIiBqad1jOpBGoy4qKjp06JBGo3G73XFxcQsWLHC73V1XscWLFysUCpr2aTSaXbt2nzx5SqlURvYUdsiQId054c3V1dVSKXX7pNdgVyM7QE1MTk4WjN38DxiGNzc3Rx9JEd1tIRwIno0bC8ES43Q67713dmpqqj+ZBvx/I0aMmDp1CpiebTZbYWFhhKsvwzBKpTItbWAo8QND852A23ALJqAbKhEEIbiie48ePcq/b8cwrLKyMoxZ5DoRiGEYjUb9xRf7jh4tUavVLpc7Pj7+gQfuD+wowzAFBYvUarXX69VoNJ999vmxY8cFvwcXifvWaDSmpKSGUq3AGltRUXlVlsn/JWAYP0uDAtwdNE3bbPZHHnkkISEBtskEQdhstsOHD0c95fCezrZxbNiwQYiMwZxO59y59yUnJ3s8HlCMhg4dmpeXb7PZINB948bCrv6doADnc2pqakyMKbyCXFFREfVkuqWBCfPTarXagqGtrc1sNsvl8r/+9f8WLVpos9nAhaxSqY4cOVpaelrY1d9QVwZsCPV6/eef//fYsWMjRgy3Wm19+vSZP3/eSy+9rFQqaZpetGihTqdtaWkxGo3bt+8oKSnR6XRms7nblkECpaenKRQKoGOoSI+ysrLbcwPPCAEkU6dO6Sp9CYLUaDQDBvTPy8tLT0+zWq04zkf6EgRvt1u7di1MuegGrScJxHHtsTUbNhRmZ2eDyWHevHmbN39YX//dkCGD77lnBmg/brd748aNV9Myb2/MysoKJa5Ag25ubr5wofp2MyFy/LDzk2fAgAHr16/reoGwVPH5TG6322KxQNgMhmE6ne6Pf3zx4MGv9ProvfE9rCsImpBmz549J06cEDQhV3Jy33nz5ra2tixevNhgMHg8Ho1GU1S0/9Chw2q1OkKxCVGF6elpNB18/epwR9c1NTXdbho01jEeLMu63Z6u/3M6XVLine truncated
</div> </div>
</div> </div>
</div> </div>
@@ -618,6 +445,18 @@
</div> </div>
</section> </section>
<!-- ===================== MITBRINGEN ===================== -->
<section id="mitbringen">
<div class="wrap">
<div class="seclabel">Was du brauchst</div>
<h2 class="big">Mitbringen</h2>
<p class="lead">
Dein eigener Mac- oder Linux-Laptop. Sonst nichts. Nur Bock,
mit KI ein Spiel zu bauen.
</p>
</div>
</section>
<!-- ===================== BEWERTUNG ===================== --> <!-- ===================== BEWERTUNG ===================== -->
<section id="bewertung"> <section id="bewertung">
<div class="wrap"> <div class="wrap">
@@ -633,30 +472,6 @@
</div> </div>
</section> </section>
<!-- ===================== PREISE ===================== -->
<section id="preise">
<div class="wrap">
<div class="seclabel">Preise</div>
<div class="prizes">
<div class="prizeitem first">
<div class="pr">Erster Platz</div>
<h3>Claude Max 5x</h3>
<div class="dur">Ein Monat</div>
</div>
<div class="prizeitem">
<div class="pr">Zweiter Platz</div>
<h3>Claude Pro</h3>
<div class="dur">Ein Monat</div>
</div>
<div class="prizeitem">
<div class="pr">Dritter Platz</div>
<h3>Claude Pro</h3>
<div class="dur">Ein Monat</div>
</div>
</div>
</div>
</section>
<!-- ===================== GAMES ===================== --> <!-- ===================== GAMES ===================== -->
<section id="games"> <section id="games">
<div class="wrap"> <div class="wrap">
@@ -695,23 +510,40 @@
</div> </div>
</section> </section>
<!-- ===================== GEWINNER ===================== -->
<section id="gewinner">
<div class="wrap">
<div class="seclabel">Ergebnisse</div>
<h2 class="big">Die Gewinner</h2>
<p class="lead" style="margin-bottom:44px;">Die AI GameJam ist gelaufen. Das sind die Top 3.</p>
<div class="prizes">
<div class="prizeitem first">
<div class="pr"><svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px;margin-right:6px;"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>Platz 1</div>
<h3>Banana Bandit</h3>
<div class="dur">50 € Amazon-Gutschein</div>
</div>
<div class="prizeitem">
<div class="pr">Platz 2</div>
<h3>Ship IT</h3>
<div class="dur">25 € Amazon-Gutschein</div>
</div>
<div class="prizeitem">
<div class="pr">Platz 3</div>
<h3>Firestarter</h3>
<div class="dur">25 € Amazon-Gutschein</div>
</div>
</div>
</div>
</section>
<!-- ===================== FOOTER ===================== --> <!-- ===================== FOOTER ===================== -->
<footer> <footer>
<div class="inner"> <div class="inner">
<div class="logos"> <div class="logos">
<img class="tc" src="/assets/wordmark-black.svg" alt="ToolCTRL"> <a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener" aria-label="ToolCTRL auf LinkedIn"><img class="tc" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNzEzLjMyIDY3OC42MSI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNSk7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNik7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNCk7CiAgICAgIH0KCiAgICAgIC5jbHMtNCB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMyk7CiAgICAgIH0KCiAgICAgIC5jbHMtNSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMik7CiAgICAgIH0KCiAgICAgIC5jbHMtNiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQpOwogICAgICB9CiAgICA8L3N0eWxlPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSI2NTguOTYiIHkxPSIzMS45NiIgeDI9IjI5Ny43IiB5Mj0iNDU4LjYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDAwIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzMzMyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNjg0LjMiIHkxPSIxMTQyLjc1IiB4Mj0iMjIwNi4wOSIgeTI9IjE1NTMuNTEiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE4MzMuNzQgLTExMDguOTYpIHJvdGF0ZSg1LjA2KSBza2V3WCgxNC4yNSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYjJiMmIyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iLjc4IiBzdG9wLWNvbG9yPSIjZmZmIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMyIgeDE9IjE3MTkuMTMiIHkxPSIxMDk4LjUxIiB4Mj0iMjI0MC45MSIgeTI9IjE1MDkuMjgiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQtMiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNCIgeDE9IjE3NjQuODIiIHkxPSIxMDQwLjQ3IiB4Mj0iMjI4Ni42IiB5Mj0iMTQ1MS4yNCIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC01IiB4MT0iMTgwNi4yNyIgeTE9Ijk4Ny44MiIgeDI9IjIzMjguMDUiIHkyPSIxMzk4LjU5IiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50LTIiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTYiIHgxPSIzNDUuMDMiIHkxPSItMTAuMTUiIHgyPSIxMDE0LjU1IiB5Mj0iNTE2LjkxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgPC9kZWZzPgogIDxnPgogICAgPHBhdGggZD0iTTY4OC43LDU3Mi4wOWwtMTYwLDk4Ljk1Yy05LjE0LDUuNzMtMTkuOTksOC4zNS0zMC43Nyw3LjM3bC0zODAuNjgtMzQuMzZjLTIyLjQ4LTIuMDEtNDAuNzYtMTkuMDEtNDQuMzUtNDEuMzFMLjY0LDE1OS4zMWMtMy4xNy0xOS41Niw1LjYtMzkuMTIsMjIuMy00OS43MkwxODkuNDYsOC4wOGMtMTMuOTUsOS43NSwyMy43NywzMS41NSwyNi44OCw0OS45NWw5MS4zLDM2MS4xM2M2LjIxLDM2LjY4LDM2LjM3LDY0LjU4LDczLjQyLDY3LjkzbDI3OC42NCw0Mi45YzguNjUuNzksMjEuODcsNDUuNjgsMjguOTQsNDIuMDNsLjA2LjA2WiIvPgogICAgPHBhdGggY2xhc3M9ImNscy02IiBkPSJNNjg4LjY0LDU3Mi4wM2MtNy4wNywzLjY2LTE1LjIzLDUuMzYtMjMuODgsNC41N2wtMzQ5LTMxLjVjLTM3LjA0LTMuMzUtNjcuMjEtMzEuMjYtNzMuNDItNjcuOTNMMTcwLjM5LDUzLjM1Yy0zLjExLTE4LjQsNS4xMi0zNS41MiwxOS4wNy00NS4yN2wxLjk1LTEuMTZjNy45OC01LjA2LDE3LjYxLTcuNjIsMjcuOTEtNi43NmwzNTguNTYsMzAuMzRjMjkuNDksMi41LDUzLjU2LDI0LjYyLDU4LjYxLDUzLjhsNzYuMTYsNDQwLjM5YzMuNTMsMjAuMjktNy4xOSwzOC45OS0yNC4wMSw0Ny4zNFoiLz4KICA8L2c+CiAgPGc+CiAgICA8Zz4KICAgICAgPHBhdGggY2xhc3M9ImNscy01IiBkPSJNMzAwLjIyLDM0My45M2wtMTUuMDEsNC4yNiwxNS43Myw5Ni4wNWM0LjIzLDI1LjgzLDIxLjg4LDQyLjU5LDQ3LjIxLDQ0LjgzbDIwLjQyLDEuODEtMi40OC0xNS4xNC0yMC40Mi0xLjgxYy0xNS44MS0xLjQtMjYuMDQtMTEuNDQtMjguNzktMjguMjhsLTguMjQtNTAuMzQsNDQuOTMsMy45Ny0yLjQ1LTE0Ljk0LTQ0LjkzLTMuOTctNS45Ny0zNi40M1oiLz4KICAgICAgPHBhdGggY2xhc3M9ImNscy00IiBkPSJNNDIxLjk2LDM4Ny45N2MtMzEuODgtMi44Mi01MS4yNiwxOC4xLTQ2LjA4LDQ5LjczLDUuMTgsMzEuNjQsMzIuMDgsNTYuNjUsNjMuOTcsNTkuNDcsMzEuODgsMi44Miw1MS4yNi0xOC4xLDQ2LjA4LTQ5LjczLTUuMTgtMzEuNjQtMzIuMDgtNTYuNjUtNjMuOTctNTkuNDdaTTQzNy4zNiw0ODIuMDNjLTIyLjktMi4wMy00MS41NC0xOS42Ny00NS4zNS00Mi45LTMuOC0yMy4yMyw5LjUzLTM4LjA0LDMyLjQzLTM2LjAyLDIyLjksMi4wMyw0MS41NCwxOS42Niw0NS4zNSw0Mi45LDMuOCwyMy4yMy05LjUzLDM4LjA0LTMyLjQyLDM2LjAyWiIvPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik01NDEuMzcsMzk4LjUzYy0zMS44OC0yLjgyLTUxLjI2LDE4LjEtNDYuMDgsNDkuNzMsNS4xOCwzMS42NCwzMi4wOCw1Ni42NSw2My45Nyw1OS40NywzMS44OCwyLjgyLDUxLjI2LTE4LjEsNDYuMDgtNDkuNzMtNS4xOC0zMS42NC0zMi4wOC01Ni42NS02My45Ny01OS40N1pNNTU2Ljc3LDQ5Mi41OWMtMjIuOS0yLjAzLTQxLjU0LTE5LjY3LTQ1LjM1LTQyLjktMy44LTIzLjIzLDkuNTMtMzguMDQsMzIuNDMtMzYuMDIsMjIuOSwyLjAzLDQxLjU0LDE5LjY2LDQ1LjM1LDQyLjksMy44LDIzLjIzLTkuNTMsMzguMDQtMzIuNDIsMzYuMDJaIi8+CiAgICAgIDxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI2MDcuNjcgMzY4LjQxIDYzMS4xIDUxMS40NiA2NDcuMDQgNTEyLjg3IDYyMi42OCAzNjQuMTUgNjA3LjY3IDM2OC40MSIvPgogICAgPC9nPgogICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTgyLjM5LDE2NC4wN2wxOC4zLDEuNjItNTYuMjktNzUuMTJjLTMuMTUtNC4yLTcuOTMtNi44Ni0xMy4xNi03LjMzaDBjLTcuNjEtLjY3LTE0Ljg2LDMuNDEtMTguMjIsMTAuMjdsLTMwLjMsNjEuNzUsMTkuMiwxLjcsMjcuNTctNTYuMmMuOTktMi4wMSwzLjc1LTIuMjcsNS4xLS40OGw0Ny44MSw2My43OVoiLz4KICA8L2c+Cjwvc3ZnPg==" alt="ToolCTRL"></a>
<img src="/assets/mitup-logo.webp" alt="mITup Bruchsal"> <a href="https://www.linkedin.com/company/it-meetup-bruchsal/" target="_blank" rel="noopener" aria-label="mITup Bruchsal auf LinkedIn"><img src="data:image/png;base64,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 truncated
</div> </div>
<div class="ft">AI GameJam · Human in the Loop · veranstaltet von ToolCTRL beim TechDay Bruchsal 2026</div> <div class="ft">AI GameJam · Human in the Loop · veranstaltet von ToolCTRL beim TechDay Bruchsal 2026</div>
<div class="links">
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
<span>·</span>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
<span>·</span>
<a href="/impressum">Impressum</a>
<span>·</span>
<a href="/datenschutz">Datenschutz</a>
</div>
</div> </div>
</footer> </footer>
@@ -761,13 +593,13 @@ function renderHud() {
nowLabelEl.innerHTML = DOT + "Startet bald"; nowLabelEl.innerHTML = DOT + "Startet bald";
nameEl.textContent = "Gleich geht's los"; nameEl.textContent = "Gleich geht's los";
subEl.textContent = "Die AI GameJam beginnt um " + PHASES[0].t + " Uhr"; subEl.textContent = "Die AI GameJam beginnt um " + PHASES[0].t + " Uhr";
setTimerText(timerEl, fmt(start - now)); timerEl.textContent = fmt(start - now);
timerLabelEl.textContent = "bis Start"; timerLabelEl.textContent = "bis Start";
} else if (now >= end) { } else if (now >= end) {
nowLabelEl.innerHTML = DOT + "Vorbei"; nowLabelEl.innerHTML = DOT + "Vorbei";
nameEl.textContent = "Event beendet"; nameEl.textContent = "Event beendet";
subEl.textContent = "Danke an alle — die Games gibt's unten zum Spielen"; subEl.textContent = "Danke an alle — die Games gibt's unten zum Spielen";
setTimerText(timerEl, "//"); timerEl.textContent = "//";
timerLabelEl.textContent = "bis bald"; timerLabelEl.textContent = "bis bald";
} else { } else {
let idx = 0; let idx = 0;
@@ -779,26 +611,14 @@ function renderHud() {
nameEl.textContent = cur.name; nameEl.textContent = cur.name;
subEl.textContent = cur.sub; subEl.textContent = cur.sub;
if (next) { if (next) {
setTimerText(timerEl, fmt(phaseDate(next.t) - now)); timerEl.textContent = fmt(phaseDate(next.t) - now);
timerLabelEl.textContent = "bis: " + next.name; timerLabelEl.textContent = "bis: " + next.name;
} else { setTimerText(timerEl, ""); timerLabelEl.textContent = ""; } } else { timerEl.textContent = ""; timerLabelEl.textContent = ""; }
} }
renderSchedule(now); renderSchedule(now);
} }
const CHECK = '<svg class="ic" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>'; const CHECK = '<svg class="ic" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';
let lastTimerText = "";
function setTimerText(el, text) {
if (el.textContent === text) return;
el.textContent = text;
if (lastTimerText) {
el.classList.remove("tick");
void el.offsetWidth;
el.classList.add("tick");
}
lastTimerText = text;
}
function renderSchedule(now) { function renderSchedule(now) {
const box = document.getElementById("schedule"); const box = document.getElementById("schedule");
@@ -827,6 +647,10 @@ setInterval(renderHud, 1000);
POST {API_BASE}/upload -> FormData { teilnehmer, game, file } */ POST {API_BASE}/upload -> FormData { teilnehmer, game, file } */
const API_BASE = "/api/gamejam"; const API_BASE = "/api/gamejam";
/* Hart-Deadline für Einreichungen — danach ist die UI gesperrt und der Server lehnt ab. */
const UPLOAD_DEADLINE = new Date("2026-05-27T22:00:00+02:00");
function uploadsOpen() { return Date.now() < UPLOAD_DEADLINE.getTime(); }
const teamEl = document.getElementById("teamName"); const teamEl = document.getElementById("teamName");
const gameEl = document.getElementById("gameName"); const gameEl = document.getElementById("gameName");
const dz = document.getElementById("dropzone"); const dz = document.getElementById("dropzone");
@@ -862,11 +686,23 @@ function pickFile(f) {
refreshUploadBtn(); refreshUploadBtn();
} }
function refreshUploadBtn() { function refreshUploadBtn() {
uploadBtn.disabled = !(API_BASE && chosenFile && teamEl.value.trim() && gameEl.value.trim()); uploadBtn.disabled = !(uploadsOpen() && API_BASE && chosenFile && teamEl.value.trim() && gameEl.value.trim());
} }
teamEl.addEventListener("input", refreshUploadBtn); teamEl.addEventListener("input", refreshUploadBtn);
gameEl.addEventListener("input", refreshUploadBtn); gameEl.addEventListener("input", refreshUploadBtn);
/* Wenn die Deadline überschritten ist: Formular sperren und Hinweis anzeigen. */
if (!uploadsOpen()) {
teamEl.disabled = true;
gameEl.disabled = true;
fileInput.disabled = true;
dz.style.pointerEvents = "none";
dz.style.opacity = "0.5";
uploadBtn.textContent = "Einreichungen geschlossen";
uploadBtn.disabled = true;
setStatus("Die Einreichungsfrist ist am 27.05.2026 um 22:00 Uhr abgelaufen.", "err");
}
uploadBtn.addEventListener("click", async () => { uploadBtn.addEventListener("click", async () => {
if (!API_BASE || !chosenFile) return; if (!API_BASE || !chosenFile) return;
uploadBtn.disabled = true; uploadBtn.disabled = true;
@@ -918,7 +754,7 @@ async function loadGallery() {
gallery.insertAdjacentHTML("beforeend", ` gallery.insertAdjacentHTML("beforeend", `
<div class="game"> <div class="game">
<iframe class="frame" src="${url}" sandbox="allow-scripts allow-pointer-lock" <iframe class="frame" src="${url}" sandbox="allow-scripts allow-pointer-lock"
loading="lazy" title="${name}"></iframe> allow="autoplay 'none'" loading="lazy" title="${name}"></iframe>
<div class="gm"> <div class="gm">
<div><div class="gt">${name}</div><div class="gte">${who}</div></div> <div><div class="gt">${name}</div><div class="gte">${who}</div></div>
<a href="${url}" target="_blank" rel="noopener">Spielen <a href="${url}" target="_blank" rel="noopener">Spielen
+17 -45
View File
@@ -3,42 +3,21 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Impressum — ToolCTRL</title> <title>Impressum: ToolCTRL</title>
<meta name="description" content="Impressum und Anbieterkennzeichnung von ToolCTRL." /> <meta name="description" content="Impressum und Anbieterkennzeichnung von ToolCTRL." />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<link rel="canonical" href="https://tool-ctrl.de/impressum" /> <link rel="canonical" href="https://tool-ctrl.de/impressum.html" />
<link rel="icon" type="image/png" href="/assets/favicon.png" /> <link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin /> <link rel="stylesheet" href="/styles.css?v=20260610" />
<link rel="stylesheet" href="/styles.css?v=20260564" />
</head> </head>
<body> <body>
<header class="site-header"> <header class="site-header">
<div class="wrap header-inner"> <div class="wrap header-inner">
<a class="brand" href="/" aria-label="ToolCTRL Startseite"> <a class="brand" href="/" aria-label="ToolCTRL Startseite">
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<nav class="nav" aria-label="Hauptnavigation"> <a class="legal-back" href="/">Zurück zur Startseite</a>
<a href="/#leistungen">Leistungen</a>
<a href="/#branchen">Branchen</a>
<a href="/#vorgehen">Vorgehen</a>
<a href="/#arbeit">Organisation</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid btn-sm" href="/#kontakt">Ablauf prüfen</a>
</nav>
<button class="nav-toggle" aria-label="Menü" aria-expanded="false">
<span></span><span></span>
</button>
</div>
<div class="mobile-nav" hidden>
<a href="/#leistungen">Leistungen</a>
<a href="/#branchen">Branchen</a>
<a href="/#vorgehen">Vorgehen</a>
<a href="/#arbeit">Organisation</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid" href="/#kontakt">Ablauf prüfen</a>
</div> </div>
</header> </header>
@@ -49,7 +28,7 @@
<h2>Anbieter</h2> <h2>Anbieter</h2>
<p> <p>
R&amp;D² UG (haftungsbeschränkt)<br /> R&amp;D² UG (haftungsbeschränkt)<br />
Orbinstr. 1<br /> Orbinstraße 1<br />
76646 Bruchsal<br /> 76646 Bruchsal<br />
Deutschland Deutschland
</p> </p>
@@ -61,6 +40,7 @@
<h2>Kontakt</h2> <h2>Kontakt</h2>
<p> <p>
Telefon: <a href="tel:+4915237750939">+49 1523 7750939</a><br />
E-Mail: <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a><br /> E-Mail: <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a><br />
Web: <a href="https://tool-ctrl.de">tool-ctrl.de</a> Web: <a href="https://tool-ctrl.de">tool-ctrl.de</a>
</p> </p>
@@ -80,7 +60,7 @@
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2> <h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
<p> <p>
Raffael Sheikh<br /> Raffael Sheikh<br />
Orbinstr. 1<br /> Orbinstraße 1<br />
76646 Bruchsal 76646 Bruchsal
</p> </p>
@@ -110,24 +90,16 @@
</p> </p>
</main> </main>
<footer class="site-footer"> <footer class="legal-footer">
<div class="wrap footer-bar"> <div class="wrap legal-footer-inner">
<div class="footer-brand"> <span>© 2026 ToolCTRL</span>
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" /> <span>
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p> <a href="/">Startseite</a> ·
</div> <a href="/impressum.html">Impressum</a> ·
<div class="footer-links"> <a href="/datenschutz.html">Datenschutz</a>
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a> </span>
<i></i>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
<i></i>
<a href="/impressum">Impressum</a>
·
<a href="/datenschutz">Datenschutz</a>
</div>
</div> </div>
</footer> </footer>
<script src="/script.js?v=20260554"></script>
</body> </body>
</html> </html>
+248 -336
View File
@@ -3,39 +3,44 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ToolCTRL — EU-first Software für souveräne Betriebe</title> <title>ToolCTRL: Moderne All-in-One-Software für eure Branche</title>
<meta name="description" content="ToolCTRL baut schnelle, bezahlbare SaaS-Systeme für Mittelständler: EU-first gedacht, DSGVO-konform und ohne unnötige US-Cloud-Pflicht." /> <meta name="description" content="Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt." />
<link rel="canonical" href="https://tool-ctrl.de/" /> <link rel="canonical" href="https://tool-ctrl.de/" />
<meta name="robots" content="index, follow" />
<!-- Open Graph -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content="de_DE" />
<meta property="og:site_name" content="ToolCTRL" /> <meta property="og:site_name" content="ToolCTRL" />
<meta property="og:title" content="ToolCTRL — EU-first Software für souveräne Betriebe" /> <meta property="og:locale" content="de_DE" />
<meta property="og:description" content="Schnelle SaaS-Systeme für Mittelständler, die keine US-Cloud-Pflicht und keine aufgeblasenen Agenturprojekte wollen." />
<meta property="og:url" content="https://tool-ctrl.de/" /> <meta property="og:url" content="https://tool-ctrl.de/" />
<meta property="og:image" content="https://tool-ctrl.de/assets/logo-a.webp" /> <meta property="og:title" content="ToolCTRL: Moderne All-in-One-Software für eure Branche" />
<meta property="og:description" content="Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt." />
<meta property="og:image" content="https://tool-ctrl.de/assets/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="ToolCTRL Logo mit Tagline: Die Software, die eure Branche längst verdient hat." />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="theme-color" content="#0A0A0A" /> <meta name="twitter:title" content="ToolCTRL: Moderne All-in-One-Software für eure Branche" />
<link rel="icon" type="image/png" href="/assets/favicon.png" /> <meta name="twitter:description" content="Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt." />
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin /> <meta name="twitter:image" content="https://tool-ctrl.de/assets/og-image.png" />
<link rel="stylesheet" href="/styles.css?v=20260564" />
<!-- Structured Data -->
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Organization", "@type": "Organization",
"name": "ToolCTRL", "name": "ToolCTRL",
"legalName": "R&D² UG (haftungsbeschränkt)", "url": "https://tool-ctrl.de",
"url": "https://tool-ctrl.de/", "logo": "https://tool-ctrl.de/assets/logo-a.png",
"email": "info@tool-ctrl.de", "description": "Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt.",
"address": {
"@type": "PostalAddress",
"streetAddress": "Orbinstr. 1",
"postalCode": "76646",
"addressLocality": "Bruchsal",
"addressCountry": "DE"
},
"sameAs": ["https://www.linkedin.com/company/toolctrl/"] "sameAs": ["https://www.linkedin.com/company/toolctrl/"]
} }
</script> </script>
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="/styles.css?v=20260610" />
</head> </head>
<body> <body>
@@ -43,29 +48,23 @@
<header class="site-header" id="top"> <header class="site-header" id="top">
<div class="wrap header-inner"> <div class="wrap header-inner">
<a class="brand" href="#top" aria-label="ToolCTRL Startseite"> <a class="brand" href="#top" aria-label="ToolCTRL Startseite">
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<nav class="nav" aria-label="Hauptnavigation"> <nav class="nav" aria-label="Hauptnavigation">
<a href="#leistungen">Leistungen</a> <a href="/">Start</a>
<a href="#branchen">Branchen</a> <a href="/techday.html">TechDay</a>
<a href="#vorgehen">Vorgehen</a> <a href="/gamejam.html">GameJam</a>
<a href="#arbeit">Organisation</a> <a class="btn btn-solid btn-sm" href="#kontakt">Kontakt aufnehmen</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid btn-sm" href="#kontakt">Ablauf prüfen</a>
</nav> </nav>
<button class="nav-toggle" aria-label="Menü" aria-expanded="false"> <button class="nav-toggle" aria-label="Menü" aria-expanded="false">
<span></span><span></span> <span></span><span></span>
</button> </button>
</div> </div>
<div class="mobile-nav" hidden> <div class="mobile-nav" hidden>
<a href="#leistungen">Leistungen</a> <a href="/">Start</a>
<a href="#branchen">Branchen</a> <a href="/techday.html">TechDay</a>
<a href="#vorgehen">Vorgehen</a> <a href="/gamejam.html">GameJam</a>
<a href="#arbeit">Organisation</a> <a class="btn btn-solid" href="#kontakt">Kontakt aufnehmen</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid" href="#kontakt">Ablauf prüfen</a>
</div> </div>
</header> </header>
@@ -76,344 +75,197 @@
<div class="wrap hero-grid"> <div class="wrap hero-grid">
<div class="hero-copy"> <div class="hero-copy">
<h1 class="hero-title"> <h1 class="hero-title">
<span class="no-break">EU-first</span> Software für souveräne Betriebe. Moderne, durchdachte All-in-One-Software für Branchen, die zu
lange übersehen&nbsp;wurden.
</h1> </h1>
<p class="hero-lead"> <p class="hero-lead">
Keine US-Cloud-Pflicht. Kein Agentur-Bullshit. ToolCTRL baut Ganze Branchen arbeiten noch mit veralteten, zusammengestückelten
individuelle SaaS-Systeme für Mittelständler, bei denen Anfragen, Insellösungen. Wir bei ToolCTRL bauen die Alternative: Software mit
Termine, Daten und Freigaben nicht länger in Tabellen, Postfächern echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert,
und halbfertigen Tools hängen bleiben. klar aussieht und sich schnell bedienen lässt.
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
<a class="btn btn-solid" href="#kontakt">Ablauf prüfen lassen</a> <a class="btn btn-solid" href="#kontakt">Kontakt aufnehmen</a>
<a class="btn btn-ghost" href="#leistungen">Leistungen ansehen</a>
</div>
<div class="trust-strip" aria-label="ToolCTRL Vertrauenssignale">
<span>Made in Germany</span>
<span>DSGVO-konform</span>
<span>Keine US-Cloud</span>
</div> </div>
</div> </div>
<div class="hero-visual" aria-label="Interaktive Workflow-Control-Grafik"> <!-- Automatisierung trifft Kontrolle — generisch, kein reales Produkt -->
<div class="hero-visual" aria-hidden="true">
<div class="visual-frame"> <div class="visual-frame">
<div class="visual-label" aria-hidden="true"> <div class="visual-label">
<span class="window-dots"><i></i><i></i><i></i></span> <span class="dot"></span> Workflow · ctrl
</div>
<div class="control-board" data-control-board>
<svg class="control-lines" viewBox="0 0 540 400" preserveAspectRatio="none" aria-hidden="true">
<path data-line="inbox"></path>
<path data-line="docs"></path>
<path data-line="chat"></path>
<path data-line="crm"></path>
<path data-line="team"></path>
<path data-line="report"></path>
</svg>
<div class="logo-node" data-logo-node>
<img src="/assets/emblem-black.svg" alt="" draggable="false" />
</div>
<button class="flow-node is-input" type="button" data-node="inbox" style="--x: 6%; --y: 18%; --r: -2deg;">Anfrage</button>
<button class="flow-node is-input" type="button" data-node="docs" style="--x: 7%; --y: 56%; --r: 1deg;">Dokumente</button>
<button class="flow-node is-input" type="button" data-node="chat" style="--x: 18%; --y: 78%; --r: -1deg;">Chat</button>
<button class="flow-node is-output" type="button" data-node="crm" style="--x: 72%; --y: 16%; --r: 2deg;">CRM</button>
<button class="flow-node is-output" type="button" data-node="team" style="--x: 76%; --y: 56%; --r: -1deg;">Team</button>
<button class="flow-node is-output" type="button" data-node="report" style="--x: 63%; --y: 78%; --r: 1.5deg;">Report</button>
</div> </div>
<svg class="node-graph" viewBox="0 0 540 400" role="presentation">
<defs>
<pattern id="grid" width="26" height="26" patternUnits="userSpaceOnUse">
<path d="M26 0H0V26" fill="none" stroke="#ECECEC" stroke-width="1" />
</pattern>
<linearGradient id="capface" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2A2A2A" />
<stop offset="1" stop-color="#101010" />
</linearGradient>
</defs>
<rect width="540" height="400" fill="url(#grid)" />
<!-- Leitungen -->
<g fill="none" stroke="#1A1A1A" stroke-width="1.6">
<path d="M116 78 C 165 78, 152 182, 200 182" />
<path d="M116 202 C 158 202, 168 202, 200 202" />
<path d="M116 326 C 165 326, 152 224, 200 224" />
<path d="M340 192 C 388 192, 378 118, 424 118" />
<path d="M340 214 C 388 214, 378 292, 424 292" />
</g>
<g fill="#1A1A1A">
<circle cx="116" cy="78" r="3" /><circle cx="116" cy="202" r="3" />
<circle cx="116" cy="326" r="3" /><circle cx="424" cy="118" r="3" />
<circle cx="424" cy="292" r="3" />
</g>
<!-- Eingangs-Knoten -->
<g class="gnode">
<rect x="24" y="56" width="92" height="44" />
<text x="70" y="83" text-anchor="middle">Daten</text>
</g>
<g class="gnode">
<rect x="24" y="180" width="92" height="44" />
<text x="70" y="207" text-anchor="middle">Auslöser</text>
</g>
<g class="gnode">
<rect x="24" y="304" width="92" height="44" />
<text x="70" y="331" text-anchor="middle">System</text>
</g>
<!-- Ausgangs-Knoten -->
<g class="gnode">
<rect x="424" y="96" width="92" height="44" />
<text x="470" y="123" text-anchor="middle">Aktion</text>
</g>
<g class="gnode">
<rect x="424" y="270" width="92" height="44" />
<text x="470" y="297" text-anchor="middle">Report</text>
</g>
<!-- TEST: echtes Logo (Emblem) in der Mitte -->
<image href="/assets/emblem-black.svg" x="190" y="128"
width="160" height="152" />
<g fill="#000000">
<circle cx="200" cy="182" r="3" /><circle cx="200" cy="202" r="3" />
<circle cx="200" cy="224" r="3" /><circle cx="340" cy="192" r="3" />
<circle cx="340" cy="214" r="3" />
</g>
</svg>
</div> </div>
</div> <div class="visual-meta">
</div> <span>Automatisiert</span>
</section> <span>Kontrolliert</span>
<span>Wartbar</span>
<!-- ============ KAMPF / WERTE ============ -->
<section class="section stance-section" id="kampf">
<div class="wrap">
<div class="section-head stance-head">
<span class="eyebrow eyebrow-light">Wofür wir kämpfen</span>
<h2 class="section-title">Keine Konzernleine. Keine Folienfabrik.</h2>
<p class="section-intro">
Wir bauen für Mittelständler, die Kontrolle über Daten, Kosten und
Abläufe zurückholen wollen. EU-first, pragmatisch und allergisch gegen
digitale Showprojekte.
</p>
</div>
<div class="stance-grid">
<article class="stance-card">
<span>01</span>
<h3>Digitale Souveränität</h3>
<p>
Keine automatische US-Cloud, kein blinder Plattformgehorsam. Wenn ein
System sinnvoll europäisch laufen kann, wird es so geplant.
</p>
</article>
<article class="stance-card stance-card-dark">
<span>02</span>
<h3>Agenturapparat aus</h3>
<p>
Keine Workshop-Show, keine Tagessatzmaschine, keine sechsstelligen
Projekte, die nach Monaten immer noch vor allem gut aussehen.
</p>
</article>
<article class="stance-card">
<span>03</span>
<h3>Ablauf vor Ego</h3>
<p>
Wir bauen keine Tools für Pitchdecks. Wir bauen Systeme, die im
Alltag führen, Zuständigkeiten klären und Arbeit sichtbar machen.
</p>
</article>
</div>
</div>
</section>
<!-- ============ PRODUKTWIRKUNG ============ -->
<section class="section product-section" id="produkt">
<div class="wrap product-grid">
<div class="product-copy">
<span class="eyebrow">Produktwirkung</span>
<h2 class="section-title">Nicht noch ein Login. Ein Arbeitsfluss, der führt.</h2>
<p class="section-intro">
Wir übersetzen gewachsene Prozesse in eine Oberfläche, die Teams
sofort verstehen: klare Zustände, weniger Rückfragen, automatische
Übergaben und ein Ort, an dem Arbeit sichtbar wird.
</p>
<div class="proof-points" aria-label="Typische Ergebnisse">
<div>
<strong>1 Oberfläche</strong>
<span>statt verstreuter Tabellen, Chats und Ordner</span>
</div>
<div>
<strong>0 Medienbrüche</strong>
<span>zwischen Anfrage, Bearbeitung und Auswertung</span>
</div>
<div>
<strong>Live-Status</strong>
<span>für Teams, Leitung und Kundenkommunikation</span>
</div>
</div> </div>
</div> </div>
<div class="product-ui" aria-label="Beispielhafte ToolCTRL Oberfläche">
<div class="ui-top">
<span class="ui-mark">toolctrl</span>
<span class="ui-status"><i></i> Auftrag #1842 · Live</span>
</div>
<div class="ui-layout">
<aside class="ui-sidebar">
<button class="ui-nav active" type="button" data-product-tab="eingang">Eingang</button>
<button class="ui-nav" type="button" data-product-tab="angebote">Angebote</button>
<button class="ui-nav" type="button" data-product-tab="planung">Planung</button>
<button class="ui-nav" type="button" data-product-tab="abschluss">Abschluss</button>
</aside>
<div class="ui-main">
<div class="ui-main-head">
<div>
<span class="ui-kicker" data-product-kicker>Heute · 14 aktive Vorgänge</span>
<strong data-product-title>Workflow Control</strong>
</div>
<span class="ui-pill" data-product-pill>3 Blocker</span>
</div>
<div class="ui-flow" data-product-flow>
<span class="done">Anfrage</span>
<span class="active">Prüfung</span>
<span>Angebot</span>
<span>Termin</span>
<span>Rechnung</span>
</div>
<div class="ui-metrics" data-product-metrics>
<div><span>Antwortzeit</span><strong>18 min</strong></div>
<div><span>Auto-Schritte</span><strong>74%</strong></div>
<div><span>Offene Übergaben</span><strong>03</strong></div>
</div>
<div class="ui-table" data-product-table>
<div class="ui-row is-hot"><span>Kunde wartet auf Rückmeldung</span><b>Jetzt</b></div>
<div class="ui-row"><span>Dokumente aus Postfach übernommen</span><b>Auto</b></div>
<div class="ui-row"><span>Team-Zuständigkeit gesetzt</span><b>11:15</b></div>
<div class="ui-row is-done"><span>Status an Leitung gemeldet</span><b>Erledigt</b></div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============ VORSPRUNG ============ -->
<section class="section velocity-section" id="vorsprung">
<div class="wrap velocity-grid">
<div class="velocity-copy">
<span class="eyebrow">Unser Vorsprung</span>
<h2 class="section-title">Schneller SaaS bauen. Ohne Baukasten-Vibe.</h2>
<p class="section-intro">
Wir starten nicht mit leerem Repository und endlosen Abstimmungen. Unsere
eigene technische Grundlage deckt Rollen, Datenmodelle, Workflows,
Dashboards, Schnittstellen und Betrieb ab. Dadurch wird Individualsoftware
schneller, ohne nach Standardtool zu riechen.
</p>
</div>
<div class="velocity-stack" aria-label="Warum ToolCTRL schneller liefert">
<article class="velocity-item">
<span>01</span>
<h3>Eigene SaaS-Basis</h3>
<p>Bewährte Infrastruktur statt leerem Repository. Das spart Zeit,
Kosten und Projektchaos.</p>
</article>
<article class="velocity-item">
<span>02</span>
<h3>Früh in den Betrieb</h3>
<p>Wir bauen schnell eine echte Version, testen sie am Ablauf und
verbessern dort, wo Arbeit wirklich passiert.</p>
</article>
<article class="velocity-item">
<span>03</span>
<h3>Budget ins Produkt</h3>
<p>Weniger Reibung, weniger Meeting-Schleifen, weniger Neuentwicklung.
Das Budget landet im System.</p>
</article>
<article class="velocity-item">
<span>04</span>
<h3>Individuell im Ergebnis</h3>
<p>Die Oberfläche und Logik werden auf Ihren Betrieb zugeschnitten.
Die Geschwindigkeit kommt aus unserer Technik.</p>
</article>
</div>
</div> </div>
</section> </section>
<!-- ============ LEISTUNGEN ============ --> <!-- ============ LEISTUNGEN ============ -->
<section class="section services-section" id="leistungen"> <section class="section" id="leistungen">
<div class="wrap"> <div class="wrap">
<div class="section-head"> <div class="section-head">
<span class="eyebrow">Leistungen</span> <span class="eyebrow">Leistungen</span>
<h2 class="section-title">Die digitale Infrastruktur Ihres Betriebs.</h2> <h2 class="section-title">Die digitale Infrastruktur Ihres Betriebs.</h2>
<p class="section-intro"> <p class="section-intro">
Wir arbeiten dort, wo Tabellen, manuelle Schritte und getrennte Wir arbeiten dort, wo manuelle Schritte an alleinstehenden Tabellen
Systeme zu viel Zeit kosten. Daraus bauen wir eine tragfähige und veralteten Insellösungen zu viel Zeit und Nerven kosten. Daraus
Infrastruktur: Software, Automatisierung und Schnittstellen, bauen wir moderne und tragfähige Infrastrukturen aus Prozessen,
zugeschnitten auf Ihren Ablauf, nicht umgekehrt. Automatisierung und Schnittstellen, zugeschnitten auf Ihren
Betrieb und Ihren Ablauf, nicht angepasst.
</p> </p>
</div> </div>
<div class="cards"> <div class="cards">
<article class="card"> <article class="card">
<span class="icon-key"><img src="/assets/icons/graph.svg" alt="" class="card-icon" /></span> <span class="icon-key"><img src="/assets/icons/graph.svg" alt="" class="card-icon" /></span>
<h3>Automation</h3> <h3>Prozessautomatisierung</h3>
<p>Wiederkehrende Abläufe werden zu verlässlichen Workflows, die <p>ToolCTRL wandelt repetitive Prozesse in eine komplett
ohne manuelles Zutun ausgelöst, verarbeitet und dokumentiert automatische Hands-Off-Erfahrung, während Sie sich um die
werden.</p> wichtigeren Aufgaben im Betrieb kümmern.</p>
</article> </article>
<article class="card"> <article class="card">
<span class="icon-key"><img src="/assets/icons/settings.svg" alt="" class="card-icon" /></span> <span class="icon-key"><img src="/assets/icons/settings.svg" alt="" class="card-icon" /></span>
<h3>Individuelle Software</h3> <h3>Individuelle Software</h3>
<p>Anwendungen, die exakt zu Ihrem Geschäft passen. Schlank <p>Wir bauen Software, die Ihrem Betrieb so gut sitzt wie ein
gebaut, klar bedienbar und auf Dauer wartbar.</p> maßgeschneiderter Anzug.</p>
</article> </article>
<article class="card"> <article class="card">
<span class="icon-key"><img src="/assets/icons/chart.svg" alt="" class="card-icon" /></span> <span class="icon-key"><img src="/assets/icons/chart.svg" alt="" class="card-icon" /></span>
<h3>Dashboards &amp; Datenflüsse</h3> <h3>Dashboards &amp; Datenflüsse</h3>
<p>Verstreute Daten an einem Ort: aktuell, übersichtlich und <p>Alle Datenpunkte integriert und aufbereitet. Ihre ganz eigene
als Grundlage für schnelle Entscheidungen.</p> Kommandozentrale.</p>
</article> </article>
<article class="card"> <article class="card">
<span class="icon-key"><img src="/assets/icons/share.svg" alt="" class="card-icon" /></span> <span class="icon-key"><img src="/assets/icons/share.svg" alt="" class="card-icon" /></span>
<h3>Schnittstellen &amp; Integrationen</h3> <h3>Schnittstellen &amp; Integrationen</h3>
<p>Bestehende Tools, APIs und Systeme greifen ineinander, statt <p>Bestehende Tools arbeiten zusammen, statt aneinander vorbei.
nebeneinander zu laufen.</p> Was in einem System passiert, ist im nächsten sofort da.</p>
</article> </article>
<article class="card"> <article class="card">
<span class="icon-key"><img src="/assets/icons/columns.svg" alt="" class="card-icon" /></span> <span class="icon-key"><img src="/assets/icons/columns.svg" alt="" class="card-icon" /></span>
<h3>Interne Tools</h3> <h3>Interne Tools</h3>
<p>Werkzeuge für das eigene Team, die verstreute Tabellenlösungen <p>Tools, die ganze Teams durch ihre Arbeitstage tragen. Keine
ablösen und mit Ihren Abläufen mitwachsen.</p> zerstückelten Teillösungen und Tabellenblätter mehr.</p>
</article> </article>
<article class="card"> <article class="card">
<span class="icon-key"><img src="/assets/icons/time.svg" alt="" class="card-icon" /></span> <span class="icon-key"><img src="/assets/icons/time.svg" alt="" class="card-icon" /></span>
<h3>Betrieb &amp; Ausbau</h3> <h3>Betrieb &amp; Weiterentwicklung</h3>
<p>Nach dem Launch ist nicht Schluss: Wartung, Anpassung und <p>Nach dem Launch ist nicht Schluss: Wartung, Anpassung,
Ausbau aus einer Hand.</p> Erweiterungen und Updates aus einer Hand. Unsere Software
wächst mit Ihrem Betrieb.</p>
</article> </article>
</div> </div>
</div> </div>
</section> </section>
<!-- ============ BRANCHEN ============ -->
<section class="section industry-section" id="branchen">
<div class="wrap">
<div class="section-head">
<span class="eyebrow">Branchen</span>
<h2 class="section-title">Nicht die Branche ist speziell. Der Ablauf ist es.</h2>
<p class="section-intro">
ToolCTRL ist interessant, wenn Ihr Ablauf speziell genug ist, dass
Baukastenlösungen ständig Workarounds erzwingen und niemand mehr
genau weiß, wo der aktuelle Stand liegt.
</p>
</div>
<div class="industry-map" aria-label="Typische Abläufe">
<div class="industry-row">
<span>01</span>
<strong>Handwerk &amp; Service</strong>
<p>Anfrage, Angebot, Termin, mobile Rückmeldung und Abschluss in einem kontrollierten Ablauf.</p>
<small>Anfrage → Angebot → Termin → Abschluss</small>
</div>
<div class="industry-row">
<span>02</span>
<strong>Events</strong>
<p>Anmeldung, Check-in und Kommunikation laufen sichtbar zusammen.</p>
<small>Ticket → Check-in → Info → Auswertung</small>
</div>
<div class="industry-row">
<span>03</span>
<strong>Vereine &amp; Verbände</strong>
<p>Mitglieder, Rollen, Beiträge und Projekte werden verwaltbar, auch wenn vieles nebenbei passiert.</p>
<small>Mitglied → Rolle → Beitrag → Projekt</small>
</div>
<div class="industry-row">
<span>04</span>
<strong>Lokale Services</strong>
<p>Leads, Termine und Angebote bleiben im Blick.</p>
<small>Lead → Beratung → Auftrag → Bindung</small>
</div>
</div>
</div>
</section>
<!-- ============ VORGEHEN ============ --> <!-- ============ VORGEHEN ============ -->
<section class="section" id="vorgehen"> <section class="section section-alt" id="vorgehen">
<div class="wrap"> <div class="wrap">
<div class="section-head"> <div class="section-head">
<span class="eyebrow">Vorgehen</span> <span class="eyebrow">Konzept</span>
<h2 class="section-title">In vier klaren Schritten.</h2> <h2 class="section-title">Das ToolCTRL Konzept.</h2>
<p class="section-intro"> <p class="section-intro">
Kein Wasserfall, kein Blindflug. Ein Ablauf, der Ergebnisse früh Ihr Anliegen wird in vier klare Schritte unterteilt: von der
sichtbar macht und Sie an jeder Stelle einbezieht. Situationsanalyse über die ersten Grundkonzepte in die konkrete
Umsetzung und den Betrieb. Unser Team ist an jeder Stelle für
Sie da, um bestmögliche Ergebnisse zu liefern und
sicherzustellen.
</p> </p>
</div> </div>
<ol class="steps"> <ol class="steps">
<li class="step"> <li class="step">
<span class="step-num">01</span> <span class="step-num">01</span>
<h3>Analyse</h3> <h3>Situationsanalyse</h3>
<p>Wir sehen uns Ihren Ablauf an und legen offen, wo Zeit, Daten <p>Unser Team geht mit Ihnen alle Prozesse und Abläufe in Ihrem
und Klarheit verloren gehen.</p> Betrieb durch, um klarzustellen, wo Zeit, Effizienz und damit
auch Geld verloren gehen.</p>
</li> </li>
<li class="step"> <li class="step">
<span class="step-num">02</span> <span class="step-num">02</span>
<h3>Konzept</h3> <h3>Grundkonzepte</h3>
<p>Ein konkreter Lösungsvorschlag mit Aufwand, Umfang und <p>Aus der Situationsanalyse erstellen wir zusammen ein
erwartetem Nutzen, transparent kalkuliert.</p> Lastenheft. Daraus bauen wir Ihnen erste Lösungskonzepte, die
wir iterativ auf Basis Ihres Feedbacks anpassen, bis wir zu
Ihrem gewünschten Endergebnis kommen.</p>
</li> </li>
<li class="step"> <li class="step">
<span class="step-num">03</span> <span class="step-num">03</span>
<h3>Umsetzung</h3> <h3>Umsetzung</h3>
<p>Wir bauen in kurzen Zyklen. Sie sehen früh ein funktionierendes <p>Nach Vereinbarung des Lösungswegs bekommen Sie früh Ihre
Ergebnis statt nur Folien.</p> erste funktionierende Version mit allen Grundfunktionen.
Iterativ wird Ihre Software verbessert und zum gewünschten
Endergebnis geformt.</p>
</li> </li>
<li class="step"> <li class="step">
<span class="step-num">04</span> <span class="step-num">04</span>
<h3>Betrieb</h3> <h3>Betrieb &amp; Wartung</h3>
<p>Übergabe, Einweisung und laufende Pflege, damit die Lösung <p>Übergabe, Einweisungen, Updates und Wartung, damit Ihre
dauerhaft trägt.</p> Software langfristig tragfähig bleibt.</p>
</li> </li>
</ol> </ol>
</div> </div>
@@ -466,6 +318,47 @@
</div> </div>
</section> </section>
<!-- ============ STANDORT / EU-HOSTING ============ -->
<section class="section section-alt" id="standort">
<div class="wrap">
<div class="section-head standort-head">
<span class="eyebrow">Standort</span>
<h2 class="section-title">Ihre Daten gehören nicht nach Virginia.</h2>
<p class="section-intro">
Die meisten Software-Anbieter, mit denen deutsche Unternehmen täglich
arbeiten, sind US-Konzerne. Heißt: Ihre Daten unterliegen US-Recht,
unabhängig davon, was im Vertrag steht oder wo der Server tatsächlich
läuft.
</p>
<p class="standort-claim">
Wir entwickeln in Deutschland, hosten in Deutschland und haften in
Deutschland.
</p>
</div>
<ol class="steps steps-3">
<li class="step">
<span class="step-num">01</span>
<h3>In Deutschland gehostet</h3>
<p>Unsere Server stehen in deutschen Rechenzentren. Keine versteckte
Subunternehmer-Kette, die im US-Cloud-Stack endet.</p>
</li>
<li class="step">
<span class="step-num">02</span>
<h3>Dem deutschen Recht unterstellt</h3>
<p>Verträge nach BGB, Datenschutz nach DSGVO, ohne Hintertür. Der
CLOUD Act hat hier nichts verloren.</p>
</li>
<li class="step">
<span class="step-num">03</span>
<h3>Von Menschen erreichbar</h3>
<p>Kein Support-Ticket im Niemandsland. Wenn etwas brennt, sprechen
Sie mit den Leuten, die Ihre Software gebaut haben.</p>
</li>
</ol>
</div>
</section>
<!-- ============ EVENTS / COMMUNITY ============ --> <!-- ============ EVENTS / COMMUNITY ============ -->
<section class="section event-section" id="events"> <section class="section event-section" id="events">
<div class="wrap"> <div class="wrap">
@@ -477,17 +370,16 @@
anfassen: lokal, direkt und mit Ergebnissen, über die man reden kann. anfassen: lokal, direkt und mit Ergebnissen, über die man reden kann.
</p> </p>
</div> </div>
<div class="event-grid"> <div class="event-grid">
<article class="event-card event-card-media"> <article class="event-card event-card-media">
<div class="event-poster"> <div class="event-poster">
<img src="/assets/techday-event.webp" alt="TechDay Bruchsal 2026 Motiv" width="683" height="384" loading="lazy" decoding="async" /> <img src="/assets/techday-event.png" alt="TechDay Bruchsal 2026 Motiv" width="683" height="384" loading="lazy" decoding="async" />
</div> </div>
<div> <div>
<span class="event-label">27. Mai 2026</span> <span class="event-label">27. Mai 2026</span>
<h3>TechDay Bruchsal</h3> <h3>TechDay Bruchsal</h3>
<p>Talks, Workshops und Austausch rund um Technologie in Bruchsal.</p> <p>Talks, Workshops und Austausch rund um Technologie in Bruchsal.</p>
<a href="/techday">TechDay ansehen</a> <a href="/techday.html">TechDay ansehen</a>
</div> </div>
</article> </article>
<article class="event-card event-card-code"> <article class="event-card event-card-code">
@@ -500,7 +392,7 @@
<span class="event-label">TechDay Format</span> <span class="event-label">TechDay Format</span>
<h3>AI GameJam</h3> <h3>AI GameJam</h3>
<p>Ein kompaktes Format, bei dem aus Idee, KI-Unterstützung und menschlicher Steuerung ein spielbares Ergebnis wird.</p> <p>Ein kompaktes Format, bei dem aus Idee, KI-Unterstützung und menschlicher Steuerung ein spielbares Ergebnis wird.</p>
<a href="/gamejam">GameJam ansehen</a> <a href="/gamejam.html">GameJam ansehen</a>
</div> </div>
</article> </article>
</div> </div>
@@ -512,16 +404,11 @@
<div class="wrap kontakt-grid"> <div class="wrap kontakt-grid">
<div class="kontakt-copy"> <div class="kontakt-copy">
<span class="eyebrow eyebrow-light">Kontakt</span> <span class="eyebrow eyebrow-light">Kontakt</span>
<h2 class="section-title">Lassen Sie uns Ihren Ablauf prüfen.</h2> <h2 class="section-title">Sprechen wir über Ihr Vorhaben.</h2>
<p class="section-intro"> <p class="section-intro">
Schicken Sie uns kurz, wo Arbeit aktuell hängen bleibt. Wir melden uns Interesse? Fragen? Lassen Sie uns darüber reden. Füllen Sie das
mit einer ehrlichen Einschätzung, ob ein eigenes System sinnvoll ist. Formular aus, wir melden uns in Kürze bei Ihnen.
</p> </p>
<ul class="contact-promises">
<li>Antwort in der Regel innerhalb eines Werktags</li>
<li>Erstgespräch ohne Kosten und ohne Pitch-Deck</li>
<li>Klare Einschätzung, auch wenn wir nicht die Richtigen sind</li>
</ul>
<dl class="contact-list"> <dl class="contact-list">
<div> <div>
<dt>E-Mail</dt> <dt>E-Mail</dt>
@@ -535,13 +422,25 @@
</div> </div>
<form class="lead-form" id="lead-form" novalidate> <form class="lead-form" id="lead-form" novalidate>
<div class="form-row"> <div class="form-grid">
<label for="f-name">Name <span>*</span></label> <div class="form-row">
<input id="f-name" name="name" type="text" autocomplete="name" required /> <label for="f-vorname">Vorname <span>*</span></label>
<input id="f-vorname" name="vorname" type="text" autocomplete="given-name" required />
</div>
<div class="form-row">
<label for="f-nachname">Nachname <span>*</span></label>
<input id="f-nachname" name="nachname" type="text" autocomplete="family-name" required />
</div>
</div> </div>
<div class="form-row"> <div class="form-grid">
<label for="f-firma">Firma</label> <div class="form-row">
<input id="f-firma" name="firma" type="text" autocomplete="organization" /> <label for="f-firmenname">Firmenname</label>
<input id="f-firmenname" name="firmenname" type="text" autocomplete="organization" />
</div>
<div class="form-row">
<label for="f-branche">Branche</label>
<input id="f-branche" name="branche" type="text" />
</div>
</div> </div>
<div class="form-grid"> <div class="form-grid">
<div class="form-row"> <div class="form-row">
@@ -549,14 +448,14 @@
<input id="f-email" name="email" type="email" autocomplete="email" required /> <input id="f-email" name="email" type="email" autocomplete="email" required />
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="f-telefon">Telefon</label> <label for="f-telefon">Telefonnummer</label>
<input id="f-telefon" name="telefon" type="tel" autocomplete="tel" /> <input id="f-telefon" name="telefon" type="tel" autocomplete="tel" />
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="f-nachricht">Worum geht es? <span>*</span></label> <label for="f-nachricht">Worum geht es Ihnen? <span>*</span></label>
<textarea id="f-nachricht" name="nachricht" rows="4" required <textarea id="f-nachricht" name="nachricht" rows="4" required
placeholder="Kurz Ihr Anliegen — wir kümmern uns um die Details."></textarea> placeholder="Kurz Ihr Anliegen, wir kümmern uns um die Details."></textarea>
</div> </div>
<!-- Honeypot gegen Spam --> <!-- Honeypot gegen Spam -->
<div class="hp" aria-hidden="true"> <div class="hp" aria-hidden="true">
@@ -567,7 +466,7 @@
<button type="submit" class="btn btn-solid">Anfrage senden</button> <button type="submit" class="btn btn-solid">Anfrage senden</button>
<p class="form-note">Mit dem Absenden stimmen Sie der Bearbeitung <p class="form-note">Mit dem Absenden stimmen Sie der Bearbeitung
Ihrer Angaben gemäß der Ihrer Angaben gemäß der
<a href="/datenschutz">Datenschutzerklärung</a> zu.</p> <a href="/datenschutz.html">Datenschutzerklärung</a> zu.</p>
</div> </div>
<p class="form-status" id="form-status" role="status" hidden></p> <p class="form-status" id="form-status" role="status" hidden></p>
</form> </form>
@@ -578,23 +477,36 @@
<!-- ============ FOOTER ============ --> <!-- ============ FOOTER ============ -->
<footer class="site-footer"> <footer class="site-footer">
<div class="wrap footer-bar"> <div class="wrap footer-inner">
<div class="footer-brand"> <div class="footer-brand">
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" /> <img src="/assets/logo-white.svg" alt="ToolCTRL" class="footer-logo" />
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p> <p>Die Software, die eure Branche längst verdient hat.</p>
</div> </div>
<div class="footer-links"> <div class="footer-cols">
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a> <div class="footer-col">
<i></i> <span class="footer-h">Navigation</span>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a> <a href="#leistungen">Leistungen</a>
<i></i> <a href="#vorgehen">Vorgehen</a>
<a href="/impressum">Impressum</a> <a href="#arbeit">Organisation</a>
· <a href="/techday.html">TechDay</a>
<a href="/datenschutz">Datenschutz</a> <a href="/gamejam.html">GameJam</a>
<a href="#kontakt">Kontakt</a>
</div>
<div class="footer-col">
<span class="footer-h">Kontakt</span>
<a href="tel:+4915237750939">+49 1523 7750939</a>
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
<a href="https://tool-ctrl.de">tool-ctrl.de</a>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
</div>
</div> </div>
</div> </div>
<div class="wrap footer-base">
<span>© <span id="year">2026</span> ToolCTRL</span>
<span><a href="/impressum.html">Impressum</a> · <a href="/datenschutz.html">Datenschutz</a></span>
</div>
</footer> </footer>
<script src="/script.js?v=20260554"></script> <script src="/script.js?v=20260610"></script>
</body> </body>
</html> </html>
+1
View File
@@ -1,4 +1,5 @@
User-agent: * User-agent: *
Allow: / Allow: /
Disallow: /gamejam-games/
Sitemap: https://tool-ctrl.de/sitemap.xml Sitemap: https://tool-ctrl.de/sitemap.xml
+13 -309
View File
@@ -9,14 +9,6 @@
/* ---- Mobile-Navigation ---- */ /* ---- Mobile-Navigation ---- */
var toggle = document.querySelector(".nav-toggle"); var toggle = document.querySelector(".nav-toggle");
var mobileNav = document.querySelector(".mobile-nav"); var mobileNav = document.querySelector(".mobile-nav");
var header = document.querySelector(".site-header");
function updateHeaderState() {
if (!header) return;
header.classList.toggle("is-scrolled", window.scrollY > 12);
}
updateHeaderState();
window.addEventListener("scroll", updateHeaderState, { passive: true });
if (toggle && mobileNav) { if (toggle && mobileNav) {
toggle.addEventListener("click", function () { toggle.addEventListener("click", function () {
var open = mobileNav.hasAttribute("hidden"); var open = mobileNav.hasAttribute("hidden");
@@ -36,303 +28,6 @@
}); });
} }
/* ---- Console-Easter-Eggs fuer neugierige Inspektoren ---- */
var inspectAscii = [
" _______ _______ _______ ___ _______ _______ _______ ",
"| || || || | | || || |",
"|_ _|| _ || _ || | | ||_ _|| ___|",
" | | | | | || | | || | | | | | | |___ ",
" | | | |_| || |_| || |___ | _| | | | ___|",
" | | | || || || |_ | | | |___ ",
" |___| |_______||_______||_______||_______| |___| |_______|"
].join("\n");
var inspectHints = [
"CTRL-KEY EVENT: Inspect gedrueckt. Bitte keine Produktivsysteme anfassen.",
"Hidden combo: window.toolctrl.press()",
"Status: schwarz/weiss. Farbe wurde zurueck in den Werkzeugkoffer gelegt."
];
var inspectLogged = false;
function logControlConsole(reason) {
if (reason === "devtools" && inspectLogged) return;
if (reason === "devtools") inspectLogged = true;
console.log(
"%c" + inspectAscii,
"font-family: monospace; font-weight: 900; color: #000; background: #fff; padding: 10px; border: 3px solid #000;"
);
console.log("%c" + inspectHints[Math.floor(Math.random() * inspectHints.length)],
"font-family: monospace; color: #fff; background: #000; padding: 6px 8px;");
}
window.toolctrl = window.toolctrl || {};
window.toolctrl.press = function () {
console.log("%c[ CTRL ] *klack* Ablauf uebernommen.", "font-family: monospace; font-weight: 900;");
return "pressed";
};
window.addEventListener("load", function () {
setTimeout(function () { logControlConsole("load"); }, 450);
});
setInterval(function () {
var open = Math.abs(window.outerWidth - window.innerWidth) > 160 ||
Math.abs(window.outerHeight - window.innerHeight) > 160;
document.documentElement.toggleAttribute("data-inspecting", open);
if (open) logControlConsole("devtools");
}, 1200);
/* ---- Dezente Scroll-Reveals ---- */
var reduceMotion = window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var revealEls = Array.prototype.slice.call(document.querySelectorAll([
".hero-copy > *",
".hero-visual",
".trust-strip span",
".stance-head > *",
".stance-card",
".product-copy > *",
".product-ui",
".velocity-copy > *",
".velocity-item",
".section-head",
".card",
".industry-card",
".step",
".arbeit-copy > *",
".kanban-wrap",
".event-poster",
".mini-terminal",
".event-card",
".kontakt-copy > *",
".contact-promises li",
".lead-form",
".td-hero .wrap > *",
".td-feature > *",
".td-talk",
".speaker-card",
".shot",
".gj-teaser"
].join(",")));
revealEls.forEach(function (el, index) {
el.classList.add("reveal", "reveal-delay-" + (index % 5));
});
if (reduceMotion || !("IntersectionObserver" in window)) {
revealEls.forEach(function (el) { el.classList.add("is-visible"); });
} else {
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
});
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
revealEls.forEach(function (el) { observer.observe(el); });
}
/* ---- Hero Workflow Nodes: ziehbare Kontrollpunkte ---- */
var board = document.querySelector("[data-control-board]");
if (board) {
var lineSvg = board.querySelector(".control-lines");
var logoNode = board.querySelector("[data-logo-node]");
var nodes = Array.prototype.slice.call(board.querySelectorAll("[data-node]"));
var draggableItems = nodes.concat(logoNode ? [logoNode] : []);
var activeDrag = null;
var lineFrame = 0;
if (logoNode) {
logoNode.addEventListener("dragstart", function (e) {
e.preventDefault();
});
}
function boardRect() {
return board.getBoundingClientRect();
}
function centerOf(el, b) {
var r = el.getBoundingClientRect();
return {
x: r.left - b.left + r.width / 2,
y: r.top - b.top + r.height / 2
};
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function updateLines() {
if (!lineSvg || !logoNode) return;
var b = boardRect();
var target = centerOf(logoNode, b);
lineSvg.setAttribute("viewBox", "0 0 " + b.width + " " + b.height);
nodes.forEach(function (node) {
var path = lineSvg.querySelector('[data-line="' + node.dataset.node + '"]');
if (!path) return;
var nodeCenter = centerOf(node, b);
var isOutput = node.classList.contains("is-output");
var start = isOutput ? target : nodeCenter;
var end = isOutput ? nodeCenter : target;
var bend = Math.max(44, Math.abs(end.x - start.x) * 0.38);
var c1x = start.x + (end.x > start.x ? bend : -bend);
var c2x = end.x + (end.x > start.x ? -bend : bend);
var d = "M " + start.x + " " + start.y +
" C " + c1x + " " + start.y +
", " + c2x + " " + end.y +
", " + end.x + " " + end.y;
path.classList.toggle("is-output", isOutput);
path.setAttribute("d", d);
});
lineFrame = 0;
}
function requestLineUpdate() {
if (lineFrame) return;
lineFrame = window.requestAnimationFrame(updateLines);
}
draggableItems.forEach(function (item) {
item.addEventListener("pointerdown", function (e) {
var r = item.getBoundingClientRect();
if (item === logoNode) item.classList.add("is-positioned");
activeDrag = {
node: item,
isLogo: item === logoNode,
offsetX: e.clientX - r.left,
offsetY: e.clientY - r.top
};
item.classList.add("is-dragging");
item.setPointerCapture(e.pointerId);
e.preventDefault();
});
item.addEventListener("pointermove", function (e) {
if (!activeDrag || activeDrag.node !== item) return;
var b = boardRect();
var x = clamp(e.clientX - b.left - activeDrag.offsetX, 8, b.width - item.offsetWidth - 8);
var y = clamp(e.clientY - b.top - activeDrag.offsetY, 8, b.height - item.offsetHeight - 8);
item.style.left = x + "px";
item.style.top = y + "px";
if (activeDrag.isLogo) {
item.classList.add("is-positioned");
} else {
item.style.removeProperty("--x");
item.style.removeProperty("--y");
}
requestLineUpdate();
});
item.addEventListener("pointerup", function (e) {
if (!activeDrag || activeDrag.node !== item) return;
item.classList.remove("is-dragging");
item.releasePointerCapture(e.pointerId);
activeDrag = null;
requestLineUpdate();
});
item.addEventListener("pointercancel", function () {
item.classList.remove("is-dragging");
activeDrag = null;
});
});
window.addEventListener("resize", requestLineUpdate, { passive: true });
updateLines();
setTimeout(requestLineUpdate, 150);
}
/* ---- Produktwirkung: klickbare Workflow-Zustaende ---- */
var productUi = document.querySelector(".product-ui");
if (productUi) {
var productStates = {
eingang: {
kicker: "Heute · 14 aktive Vorgänge",
title: "Workflow Control",
pill: "3 Blocker",
flow: ["Anfrage", "Prüfung", "Angebot", "Termin", "Rechnung"],
active: 1,
done: 0,
metrics: [["Antwortzeit", "18 min"], ["Auto-Schritte", "74%"], ["Offene Übergaben", "03"]],
rows: [["Kunde wartet auf Rückmeldung", "Jetzt", "is-hot"], ["Dokumente aus Postfach übernommen", "Auto", ""], ["Team-Zuständigkeit gesetzt", "11:15", ""], ["Status an Leitung gemeldet", "Erledigt", "is-done"]]
},
angebote: {
kicker: "Vertrieb · 8 Angebote offen",
title: "Angebote steuern",
pill: "2 fällig",
flow: ["Lead", "Kalkulation", "Freigabe", "Versand", "Follow-up"],
active: 2,
done: 1,
metrics: [["Durchlauf", "2,4 h"], ["Vorlagen", "91%"], ["Rückfragen", "05"]],
rows: [["Angebot braucht interne Freigabe", "Heute", "is-hot"], ["PDF aus Kundendaten erstellt", "Auto", ""], ["Nachfass-Termin gesetzt", "Morgen", ""], ["Alte Version archiviert", "Erledigt", "is-done"]]
},
planung: {
kicker: "Team · 21 Einsätze geplant",
title: "Planung sichtbar",
pill: "1 Konflikt",
flow: ["Auftrag", "Ressource", "Slot", "Route", "Update"],
active: 2,
done: 1,
metrics: [["Auslastung", "82%"], ["Auto-Routen", "68%"], ["Konflikte", "01"]],
rows: [["Termin überschneidet sich", "Prüfen", "is-hot"], ["Route für Außendienst gebaut", "Auto", ""], ["Materialliste angehängt", "09:40", ""], ["Kunde informiert", "Erledigt", "is-done"]]
},
abschluss: {
kicker: "Abschluss · 36 Vorgänge fertig",
title: "Betrieb im Blick",
pill: "0 kritisch",
flow: ["Abnahme", "Doku", "Rechnung", "Feedback", "Archiv"],
active: 3,
done: 2,
metrics: [["Abnahmen", "96%"], ["Auto-Doku", "88%"], ["Offen", "00"]],
rows: [["Feedback für nächste Version notiert", "Neu", ""], ["Rechnung vorbereitet", "Auto", ""], ["Abnahme protokolliert", "13:20", "is-done"], ["Projekt sauber archiviert", "Erledigt", "is-done"]]
}
};
var productTabs = Array.prototype.slice.call(productUi.querySelectorAll("[data-product-tab]"));
var productKicker = productUi.querySelector("[data-product-kicker]");
var productTitle = productUi.querySelector("[data-product-title]");
var productPill = productUi.querySelector("[data-product-pill]");
var productFlow = productUi.querySelector("[data-product-flow]");
var productMetrics = productUi.querySelector("[data-product-metrics]");
var productTable = productUi.querySelector("[data-product-table]");
function setProductState(key) {
var state = productStates[key];
if (!state) return;
productUi.classList.add("is-switching");
productTabs.forEach(function (tab) {
tab.classList.toggle("active", tab.dataset.productTab === key);
});
window.setTimeout(function () {
if (productKicker) productKicker.textContent = state.kicker;
if (productTitle) productTitle.textContent = state.title;
if (productPill) productPill.textContent = state.pill;
if (productFlow) {
productFlow.innerHTML = state.flow.map(function (label, index) {
var cls = index === state.active ? "active" : (index <= state.done ? "done" : "");
return '<span class="' + cls + '">' + label + '</span>';
}).join("");
}
if (productMetrics) {
productMetrics.innerHTML = state.metrics.map(function (metric) {
return "<div><span>" + metric[0] + "</span><strong>" + metric[1] + "</strong></div>";
}).join("");
}
if (productTable) {
productTable.innerHTML = state.rows.map(function (row) {
return '<div class="ui-row ' + row[2] + '"><span>' + row[0] + '</span><b>' + row[1] + '</b></div>';
}).join("");
}
productUi.classList.remove("is-switching");
}, 90);
}
productTabs.forEach(function (tab) {
tab.addEventListener("click", function () {
setProductState(tab.dataset.productTab);
});
});
}
/* ---- Lead-Formular ---- */ /* ---- Lead-Formular ---- */
var form = document.getElementById("lead-form"); var form = document.getElementById("lead-form");
var status = document.getElementById("form-status"); var status = document.getElementById("form-status");
@@ -351,8 +46,9 @@
function validate() { function validate() {
var ok = true; var ok = true;
["f-name", "f-email", "f-nachricht"].forEach(function (id) { ["f-vorname", "f-nachname", "f-email", "f-nachricht"].forEach(function (id) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (!el) return;
var row = fieldRow(el); var row = fieldRow(el);
var valid = el.value.trim() !== ""; var valid = el.value.trim() !== "";
if (id === "f-email" && valid) { if (id === "f-email" && valid) {
@@ -373,14 +69,22 @@
} }
var btn = form.querySelector("button[type=submit]"); var btn = form.querySelector("button[type=submit]");
var vorname = form.vorname.value.trim();
var nachname = form.nachname.value.trim();
var firmenname = form.firmenname.value.trim();
var branche = form.branche.value.trim();
var payload = { var payload = {
name: form.name.value.trim(), name: (vorname + " " + nachname).trim(),
firma: form.firma.value.trim(), vorname: vorname,
nachname: nachname,
firma: firmenname + (branche ? " (" + branche + ")" : ""),
firmenname: firmenname,
branche: branche,
email: form.email.value.trim(), email: form.email.value.trim(),
telefon: form.telefon.value.trim(), telefon: form.telefon.value.trim(),
nachricht: form.nachricht.value.trim(), nachricht: form.nachricht.value.trim(),
website: form.website.value.trim(), // Honeypot website: form.website.value.trim(), // Honeypot
quelle: "Messe — Landing Page" quelle: "Landing Page"
}; };
if (btn) { btn.disabled = true; btn.textContent = "Wird gesendet …"; } if (btn) { btn.disabled = true; btn.textContent = "Wird gesendet …"; }
+8 -8
View File
@@ -2,17 +2,17 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url> <url>
<loc>https://tool-ctrl.de/</loc> <loc>https://tool-ctrl.de/</loc>
<lastmod>2026-06-10</lastmod>
<priority>1.0</priority>
</url> </url>
<url> <url>
<loc>https://tool-ctrl.de/techday</loc> <loc>https://tool-ctrl.de/techday.html</loc>
<lastmod>2026-06-10</lastmod>
<priority>0.8</priority>
</url> </url>
<url> <url>
<loc>https://tool-ctrl.de/gamejam</loc> <loc>https://tool-ctrl.de/gamejam.html</loc>
</url> <lastmod>2026-06-10</lastmod>
<url> <priority>0.6</priority>
<loc>https://tool-ctrl.de/impressum</loc>
</url>
<url>
<loc>https://tool-ctrl.de/datenschutz</loc>
</url> </url>
</urlset> </urlset>
+329 -1864
View File
File diff suppressed because it is too large. Load diff
+79 -41
View File
@@ -3,21 +3,52 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TechDay Bruchsal 2026 — ToolCTRL</title> <title>TechDay Bruchsal 2026: ToolCTRL</title>
<meta name="description" content="ToolCTRL beim Bruchsaler TechDay 2026: Vortrag, AI GameJam und die mITapp. Am 27. Mai 2026 im HubWerk01 Bruchsal." /> <meta name="description" content="ToolCTRL beim Bruchsaler TechDay 2026: Vortrag, AI GameJam und die mITapp. Am 27. Mai 2026 im HubWerk01 Bruchsal." />
<link rel="canonical" href="https://tool-ctrl.de/techday" /> <link rel="canonical" href="https://tool-ctrl.de/techday.html" />
<!-- Open Graph -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content="de_DE" />
<meta property="og:site_name" content="ToolCTRL" /> <meta property="og:site_name" content="ToolCTRL" />
<meta property="og:title" content="ToolCTRL beim TechDay Bruchsal 2026" /> <meta property="og:locale" content="de_DE" />
<meta property="og:description" content="Vortrag, AI GameJam und mITapp beim ersten Bruchsaler TechDay am 27. Mai 2026." /> <meta property="og:url" content="https://tool-ctrl.de/techday.html" />
<meta property="og:url" content="https://tool-ctrl.de/techday" /> <meta property="og:title" content="TechDay Bruchsal 2026: ToolCTRL" />
<meta property="og:image" content="https://tool-ctrl.de/assets/techday-event.webp" /> <meta property="og:description" content="ToolCTRL beim Bruchsaler TechDay 2026: Vortrag, AI GameJam und die mITapp. Am 27. Mai 2026 im HubWerk01 Bruchsal." />
<meta property="og:image" content="https://tool-ctrl.de/assets/techday-event.png" />
<meta property="og:image:width" content="683" />
<meta property="og:image:height" content="384" />
<meta property="og:image:alt" content="TechDay Bruchsal 2026 Eventmotiv" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="theme-color" content="#0A0A0A" />
<link rel="icon" type="image/png" href="/assets/favicon.png" /> <!-- Structured Data -->
<link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin /> <script type="application/ld+json">
<link rel="stylesheet" href="/styles.css?v=20260564" /> {
"@context": "https://schema.org",
"@type": "Event",
"name": "TechDay Bruchsal 2026",
"startDate": "2026-05-27",
"eventStatus": "https://schema.org/EventScheduled",
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
"location": {
"@type": "Place",
"name": "HubWerk01",
"address": {
"@type": "PostalAddress",
"addressLocality": "Bruchsal",
"addressCountry": "DE"
}
},
"image": "https://tool-ctrl.de/assets/techday-event.png",
"description": "ToolCTRL beim Bruchsaler TechDay 2026: Vortrag, AI GameJam und die mITapp. Am 27. Mai 2026 im HubWerk01 Bruchsal.",
"organizer": {
"@type": "Organization",
"name": "ToolCTRL",
"url": "https://tool-ctrl.de"
}
}
</script>
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" />
<link rel="stylesheet" href="/styles.css?v=20260610" />
</head> </head>
<body> <body>
@@ -25,29 +56,23 @@
<header class="site-header" id="top"> <header class="site-header" id="top">
<div class="wrap header-inner"> <div class="wrap header-inner">
<a class="brand" href="/" aria-label="ToolCTRL Startseite"> <a class="brand" href="/" aria-label="ToolCTRL Startseite">
<img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/logo-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<nav class="nav" aria-label="Hauptnavigation"> <nav class="nav" aria-label="Hauptnavigation">
<a href="/#leistungen">Leistungen</a> <a href="/">Start</a>
<a href="/#branchen">Branchen</a> <a href="/techday.html">TechDay</a>
<a href="/#vorgehen">Vorgehen</a> <a href="/gamejam.html">GameJam</a>
<a href="/#arbeit">Organisation</a> <a class="btn btn-solid btn-sm" href="/#kontakt">Kontakt aufnehmen</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid btn-sm" href="/#kontakt">Ablauf prüfen</a>
</nav> </nav>
<button class="nav-toggle" aria-label="Menü" aria-expanded="false"> <button class="nav-toggle" aria-label="Menü" aria-expanded="false">
<span></span><span></span> <span></span><span></span>
</button> </button>
</div> </div>
<div class="mobile-nav" hidden> <div class="mobile-nav" hidden>
<a href="/#leistungen">Leistungen</a> <a href="/">Start</a>
<a href="/#branchen">Branchen</a> <a href="/techday.html">TechDay</a>
<a href="/#vorgehen">Vorgehen</a> <a href="/gamejam.html">GameJam</a>
<a href="/#arbeit">Organisation</a> <a class="btn btn-solid" href="/#kontakt">Kontakt aufnehmen</a>
<a href="/techday">TechDay</a>
<a href="/gamejam">GameJam</a>
<a class="btn btn-solid" href="/#kontakt">Ablauf prüfen</a>
</div> </div>
</header> </header>
@@ -90,7 +115,7 @@
mit der mITapp als digitalem Zuhause des Events. mit der mITapp als digitalem Zuhause des Events.
</p> </p>
</div> </div>
<img class="td-feature-img" src="/assets/techday-event.webp" width="683" height="384" decoding="async" fetchpriority="high" <img class="td-feature-img" src="/assets/techday-event.png"
alt="TechDay Bruchsal 2026: Human in the Loop" /> alt="TechDay Bruchsal 2026: Human in the Loop" />
</div> </div>
</div> </div>
@@ -137,7 +162,7 @@
<aside class="speaker-card"> <aside class="speaker-card">
<div class="sp-label">Speaker</div> <div class="sp-label">Speaker</div>
<div class="sp-head"> <div class="sp-head">
<img class="sp-photo" src="/assets/speaker-david.webp" alt="David Bertram" width="320" height="320" loading="lazy" decoding="async" /> <img class="sp-photo" src="/assets/speaker-david.jpg" alt="David Bertram" width="320" height="320" loading="lazy" decoding="async" />
<div> <div>
<div class="sp-name">David Bertram</div> <div class="sp-name">David Bertram</div>
<div class="sp-role">Co-Founder &amp; Head of Product, ToolCTRL</div> <div class="sp-role">Co-Founder &amp; Head of Product, ToolCTRL</div>
@@ -205,7 +230,7 @@
Upload für die fertigen Spiele. Die GameJam hat ihre eigene Seite. Upload für die fertigen Spiele. Die GameJam hat ihre eigene Seite.
</p> </p>
<div class="hero-actions" style="margin-top:28px;"> <div class="hero-actions" style="margin-top:28px;">
<a class="btn btn-solid" href="/gamejam">Zur GameJam-Seite</a> <a class="btn btn-solid" href="/gamejam.html">Zur GameJam-Seite</a>
</div> </div>
</div> </div>
<div class="gj-teaser" aria-hidden="true"> <div class="gj-teaser" aria-hidden="true">
@@ -254,23 +279,36 @@
<!-- ============ FOOTER ============ --> <!-- ============ FOOTER ============ -->
<footer class="site-footer"> <footer class="site-footer">
<div class="wrap footer-bar"> <div class="wrap footer-inner">
<div class="footer-brand"> <div class="footer-brand">
<img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" /> <img src="/assets/logo-white.svg" alt="ToolCTRL" class="footer-logo" />
<p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p> <p>Die Software, die eure Branche längst verdient hat.</p>
</div> </div>
<div class="footer-links"> <div class="footer-cols">
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a> <div class="footer-col">
<i></i> <span class="footer-h">Navigation</span>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a> <a href="/#leistungen">Leistungen</a>
<i></i> <a href="/#vorgehen">Vorgehen</a>
<a href="/impressum">Impressum</a> <a href="/#arbeit">Organisation</a>
· <a href="/techday.html">TechDay</a>
<a href="/datenschutz">Datenschutz</a> <a href="/gamejam.html">GameJam</a>
<a href="/#kontakt">Kontakt</a>
</div>
<div class="footer-col">
<span class="footer-h">Kontakt</span>
<a href="tel:+4915237750939">+49 1523 7750939</a>
<a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
<a href="https://tool-ctrl.de">tool-ctrl.de</a>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a>
</div>
</div> </div>
</div> </div>
<div class="wrap footer-base">
<span>© <span id="year">2026</span> ToolCTRL</span>
<span><a href="/impressum.html">Impressum</a> · <a href="/datenschutz.html">Datenschutz</a></span>
</div>
</footer> </footer>
<script src="/script.js?v=20260554"></script> <script src="/script.js?v=20260610"></script>
</body> </body>
</html> </html>
+175 -106
View File
@@ -16,36 +16,82 @@ const path = require("path");
const crypto = require("crypto"); const crypto = require("crypto");
const os = require("os"); const os = require("os");
const net = require("net"); const net = require("net");
const zlib = require("zlib");
const ROOT = __dirname; const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, "public"); const PUBLIC_DIR = path.join(ROOT, "public");
const LEADS_DIR = path.join(ROOT, "leads"); const DATA_DIR = process.env.DATA_DIR
? path.resolve(process.env.DATA_DIR)
: ROOT;
const LEADS_DIR = path.join(DATA_DIR, "leads");
const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson"); const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson");
const LEADS_CSV = path.join(LEADS_DIR, "leads.csv"); const LEADS_CSV = path.join(LEADS_DIR, "leads.csv");
/* GameJam-Speicher */ /* GameJam-Speicher */
const GAMEJAM_DIR = path.join(ROOT, "gamejam"); const GAMEJAM_DIR = path.join(DATA_DIR, "gamejam");
const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson"); const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson");
const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games"); const GAMES_DIR = process.env.DATA_DIR
? path.join(DATA_DIR, "gamejam-games")
: path.join(PUBLIC_DIR, "gamejam-games");
const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */ const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */
/* Hart-Deadline für GameJam-Uploads: 27.05.2026, 22:00 Europe/Berlin (CEST = UTC+2). */
const UPLOAD_DEADLINE = new Date("2026-05-27T22:00:00+02:00");
const APP_REVISION = process.env.APP_REVISION || "local";
/* Central lead-inbox (infra ADR 012). Sites forward leads here; local files
stay as a fallback. Unset env => old local-only behavior. */
const LEAD_INBOX_URL = process.env.LEAD_INBOX_URL || "";
const LEAD_INBOX_KEY = process.env.LEAD_INBOX_KEY || "";
async function forwardLeadToInbox(lead) {
if (!LEAD_INBOX_URL || !LEAD_INBOX_KEY) return false;
try {
const res = await fetch(LEAD_INBOX_URL.replace(/\/+$/, "") + "/v1/leads", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + LEAD_INBOX_KEY
},
/* inbox accepts DE aliases: firma, telefon, nachricht, quelle */
body: JSON.stringify(lead),
signal: AbortSignal.timeout(5000)
});
return res.ok;
} catch (e) {
console.error("[lead] inbox forward failed:", e.message);
return false;
}
}
function envFlag(name, defaultValue) {
const raw = process.env[name];
if (raw == null || raw === "") return defaultValue;
return !/^(0|false|no|off)$/i.test(raw);
}
function envInt(name, defaultValue) {
const raw = Number.parseInt(process.env[name] || "", 10);
return Number.isFinite(raw) ? raw : defaultValue;
}
/* Virenscan über ClamAV. Der Server spricht das clamd-Socket-Protokoll /* Virenscan über ClamAV. Der Server spricht das clamd-Socket-Protokoll
direkt — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist direkt — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist
installiert und der Daemon "clamd" läuft (Einrichtung siehe README). installiert und der Daemon "clamd" läuft (Einrichtung siehe README).
enabled:false schaltet den Scan ab (z. B. lokal ohne ClamAV). */ enabled:false schaltet den Scan ab (z. B. lokal ohne ClamAV). */
const CLAMAV = { const CLAMAV = {
enabled: true, enabled: envFlag("CLAMAV_ENABLED", true),
host: "127.0.0.1", host: process.env.CLAMAV_HOST || "127.0.0.1",
port: 3310, /* clamd TCPSocket-Standardport */ port: envInt("CLAMAV_PORT", 3310), /* clamd TCPSocket-Standardport */
timeoutMs: 15000 timeoutMs: envInt("CLAMAV_TIMEOUT_MS", 15000)
}; };
/* Bevorzugte Ports — der erste freie wird genutzt. */ /* Bevorzugte Ports — der erste freie wird genutzt. */
const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0]; const PORT_CANDIDATES = process.env.PORT
? [envInt("PORT", 3000)]
: [4317, 4318, 4417, 5317, 8317, 0];
/* 0.0.0.0 = auf allen Schnittstellen erreichbar, auch für andere /* 0.0.0.0 = auf allen Schnittstellen erreichbar, auch für andere
Geräte im selben WLAN (z. B. Handys, die den QR-Code scannen). */ Geräte im selben WLAN (z. B. Handys, die den QR-Code scannen). */
const HOST = "0.0.0.0"; const HOST = process.env.HOST || process.env.HOSTNAME || "0.0.0.0";
/* Erste IPv4-LAN-Adresse dieses Rechners ermitteln. */ /* Erste IPv4-LAN-Adresse dieses Rechners ermitteln. */
function getLanIP() { function getLanIP() {
@@ -69,45 +115,11 @@ const MIME = {
".webp": "image/webp", ".webp": "image/webp",
".ico": "image/x-icon", ".ico": "image/x-icon",
".json": "application/json; charset=utf-8", ".json": "application/json; charset=utf-8",
".woff2": "font/woff2" ".woff2": "font/woff2",
".txt": "text/plain; charset=utf-8",
".xml": "application/xml; charset=utf-8"
}; };
const CLEAN_ROUTES = {
"/techday": "/techday.html",
"/gamejam": "/gamejam.html",
"/impressum": "/impressum.html",
"/datenschutz": "/datenschutz.html"
};
const IMMUTABLE_EXTS = new Set([".webp", ".svg", ".woff2", ".png", ".jpg", ".jpeg", ".ico"]);
const COMPRESSIBLE_EXTS = new Set([".html", ".css", ".js", ".svg", ".json", ".txt"]);
function redirect(res, location) {
res.writeHead(308, { Location: location });
res.end();
}
function cacheControlFor(urlPath, ext, cacheKey) {
if (urlPath.startsWith("/gamejam-games/")) return "public, max-age=60";
if (ext === ".html") return "no-cache";
if (ext === ".css" || ext === ".js") {
return cacheKey ? "public, max-age=31536000, immutable" : "no-cache";
}
if (IMMUTABLE_EXTS.has(ext)) return "public, max-age=31536000, immutable";
return "no-cache";
}
function etagFor(stat) {
return 'W/"' + stat.size.toString(16) + "-" + Math.floor(stat.mtimeMs).toString(16) + '"';
}
function preferredEncoding(req) {
const accepted = req.headers["accept-encoding"] || "";
if (/\bbr\b/.test(accepted)) return "br";
if (/\bgzip\b/.test(accepted)) return "gzip";
return null;
}
/* ---------- Lead-Speicher vorbereiten ---------- */ /* ---------- Lead-Speicher vorbereiten ---------- */
function ensureLeadStore() { function ensureLeadStore() {
if (!fs.existsSync(LEADS_DIR)) fs.mkdirSync(LEADS_DIR, { recursive: true }); if (!fs.existsSync(LEADS_DIR)) fs.mkdirSync(LEADS_DIR, { recursive: true });
@@ -126,6 +138,18 @@ function ensureGamejamStore() {
if (!fs.existsSync(GAMES_DIR)) fs.mkdirSync(GAMES_DIR, { recursive: true }); if (!fs.existsSync(GAMES_DIR)) fs.mkdirSync(GAMES_DIR, { recursive: true });
} }
function assertInside(baseDir, candidate) {
const relative = path.relative(baseDir, candidate);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function writeProbe(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, ".write-probe-" + process.pid + "-" + Date.now());
fs.writeFileSync(file, "ok", "utf8");
fs.unlinkSync(file);
}
function csvCell(value) { function csvCell(value) {
const s = String(value == null ? "" : value); const s = String(value == null ? "" : value);
return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"'; return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"';
@@ -150,34 +174,31 @@ function saveLead(lead) {
/* ---------- Statische Dateien ---------- */ /* ---------- Statische Dateien ---------- */
function serveStatic(req, res) { function serveStatic(req, res) {
let parsedUrl; let urlPath = decodeURIComponent(req.url.split("?")[0]);
try {
parsedUrl = new URL(req.url, "http://local"); /* SEO: kanonischer Host (www -> non-www) und /index.html -> / */
} catch (e) { const host = (req.headers.host || "").toLowerCase();
res.writeHead(400).end("Bad Request"); if (host === "www.tool-ctrl.de") {
res.writeHead(301, { Location: "https://tool-ctrl.de" + req.url });
res.end();
return;
}
if (urlPath === "/index.html") {
res.writeHead(301, { Location: "/" });
res.end();
return; return;
} }
let urlPath = decodeURIComponent(parsedUrl.pathname);
if (urlPath === "/") urlPath = "/index.html"; if (urlPath === "/") urlPath = "/index.html";
if (urlPath.endsWith("/") && CLEAN_ROUTES[urlPath.slice(0, -1)]) {
redirect(res, urlPath.slice(0, -1) + parsedUrl.search);
return;
}
for (const cleanPath of Object.keys(CLEAN_ROUTES)) {
if (urlPath === CLEAN_ROUTES[cleanPath]) {
redirect(res, cleanPath + parsedUrl.search);
return;
}
}
if (CLEAN_ROUTES[urlPath]) urlPath = CLEAN_ROUTES[urlPath];
/* Path-Traversal verhindern */ /* Path-Traversal verhindern */
const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath)); let baseDir = PUBLIC_DIR;
if (!filePath.startsWith(PUBLIC_DIR)) { let filePath = path.normalize(path.join(PUBLIC_DIR, urlPath));
if (urlPath.startsWith("/gamejam-games/")) {
baseDir = GAMES_DIR;
filePath = path.normalize(path.join(GAMES_DIR, urlPath.replace(/^\/gamejam-games\//, "")));
}
if (!assertInside(baseDir, filePath)) {
res.writeHead(403).end("Forbidden"); res.writeHead(403).end("Forbidden");
return; return;
} }
@@ -189,35 +210,20 @@ function serveStatic(req, res) {
return; return;
} }
const ext = path.extname(filePath).toLowerCase(); const ext = path.extname(filePath).toLowerCase();
const etag = etagFor(stat); /* HTML immer frisch, statische Assets lange cachen (Versionierung via ?v=) */
const cacheControl = ext === ".html"
? "no-cache"
: "public, max-age=31536000, immutable";
const headers = { const headers = {
"Content-Type": MIME[ext] || "application/octet-stream", "Content-Type": MIME[ext] || "application/octet-stream",
"Cache-Control": cacheControlFor(urlPath, ext, parsedUrl.search), "Cache-Control": cacheControl
"ETag": etag,
"Last-Modified": stat.mtime.toUTCString(),
"X-Content-Type-Options": "nosniff"
}; };
/* Hochgeladene GameJam-Spiele nicht indexieren (Fremdcontent) */
if (COMPRESSIBLE_EXTS.has(ext)) headers["Vary"] = "Accept-Encoding"; if (urlPath.startsWith("/gamejam-games/")) {
headers["X-Robots-Tag"] = "noindex";
if ((req.headers["if-none-match"] || "").includes(etag)) {
res.writeHead(304, headers);
res.end();
return;
} }
let stream = fs.createReadStream(filePath);
const encoding = COMPRESSIBLE_EXTS.has(ext) ? preferredEncoding(req) : null;
if (encoding === "br") {
headers["Content-Encoding"] = "br";
stream = stream.pipe(zlib.createBrotliCompress());
} else if (encoding === "gzip") {
headers["Content-Encoding"] = "gzip";
stream = stream.pipe(zlib.createGzip({ level: 6 }));
}
res.writeHead(200, headers); res.writeHead(200, headers);
stream.pipe(res); fs.createReadStream(filePath).pipe(res);
}); });
} }
@@ -272,20 +278,27 @@ function handleLead(req, res) {
quelle: String(data.quelle || "Landing Page").slice(0, 120) quelle: String(data.quelle || "Landing Page").slice(0, 120)
}; };
try { /* Central inbox is source of truth; local files are the fallback so a
saveLead(lead); lead is never lost if the inbox is unreachable (infra ADR 012). */
} catch (e) { forwardLeadToInbox(lead).then((forwarded) => {
console.error("[lead] Speichern fehlgeschlagen:", e.message); if (!forwarded) {
return sendJSON(res, 500, { try {
error: "Speichern fehlgeschlagen. Bitte später erneut versuchen." saveLead(lead);
}); } catch (e) {
} console.error("[lead] Speichern fehlgeschlagen:", e.message);
return sendJSON(res, 500, {
error: "Speichern fehlgeschlagen. Bitte später erneut versuchen."
});
}
}
console.log( console.log(
"[lead] " + lead.zeitpunkt + " — " + lead.name + "[lead] " + (forwarded ? "→inbox " : "→local ") +
(lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">" lead.zeitpunkt + " — " + lead.name +
); (lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">"
sendJSON(res, 200, { ok: true }); );
sendJSON(res, 200, { ok: true });
});
}); });
} }
@@ -393,6 +406,13 @@ function clamavPing() {
/* ---------- GameJam: Spiel einreichen ---------- */ /* ---------- GameJam: Spiel einreichen ---------- */
function handleGamejamUpload(req, res) { function handleGamejamUpload(req, res) {
/* Hart-Deadline durchsetzen — danach werden keine Einreichungen mehr angenommen. */
if (Date.now() > UPLOAD_DEADLINE.getTime()) {
return sendJSON(res, 410, {
error: "Einreichungen sind seit dem 27.05.2026, 22:00 Uhr geschlossen."
});
}
const ctype = req.headers["content-type"] || ""; const ctype = req.headers["content-type"] || "";
const bM = ctype.match(/boundary=(?:"([^"]+)"|([^;]+))/i); const bM = ctype.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
if (!/multipart\/form-data/i.test(ctype) || !bM) { if (!/multipart\/form-data/i.test(ctype) || !bM) {
@@ -523,10 +543,57 @@ function sendJSON(res, code, obj) {
res.end(JSON.stringify(obj)); res.end(JSON.stringify(obj));
} }
function handleHealthz(req, res) {
if (req.method !== "GET" && req.method !== "HEAD") {
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
}
sendJSON(res, 200, {
ok: true,
service: "toolctrl-website",
revision: APP_REVISION
});
}
async function handleReadyz(req, res) {
if (req.method !== "GET" && req.method !== "HEAD") {
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
}
const checks = {
leadsWritable: false,
gamejamWritable: false,
gamesWritable: false,
clamav: !CLAMAV.enabled
};
try {
writeProbe(LEADS_DIR);
checks.leadsWritable = true;
writeProbe(GAMEJAM_DIR);
checks.gamejamWritable = true;
writeProbe(GAMES_DIR);
checks.gamesWritable = true;
if (CLAMAV.enabled) checks.clamav = await clamavPing();
} catch (e) {
console.error("[readyz] Prüfung fehlgeschlagen:", e.message);
}
const ok = Object.keys(checks).every((key) => checks[key] === true);
sendJSON(res, ok ? 200 : 503, {
ok: ok,
service: "toolctrl-website",
revision: APP_REVISION,
checks: checks
});
}
/* ---------- Router ---------- */ /* ---------- Router ---------- */
const server = http.createServer((req, res) => { const server = http.createServer((req, res) => {
const route = req.url.split("?")[0]; const route = req.url.split("?")[0];
if (route === "/healthz") return handleHealthz(req, res);
if (route === "/readyz") return handleReadyz(req, res);
if (route === "/api/lead") { if (route === "/api/lead") {
if (req.method !== "POST") { if (req.method !== "POST") {
return sendJSON(res, 405, { error: "Methode nicht erlaubt." }); return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
@@ -586,6 +653,8 @@ function listen(candidates) {
} }
console.log(" Leads: " + LEADS_CSV); console.log(" Leads: " + LEADS_CSV);
console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON); console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON);
console.log(" Daten: " + DATA_DIR);
console.log(" Revision: " + APP_REVISION);
if (CLAMAV.enabled) { if (CLAMAV.enabled) {
clamavPing().then((ok) => { clamavPing().then((ok) => {
console.log(" Virenscan: " + (ok console.log(" Virenscan: " + (ok