Compare commits

..
15 Commits
37 changed files with 3028 additions and 1098 deletions

No files matched your search

-8
View File
@@ -1,8 +0,0 @@
.git
.gitea
.DS_Store
node_modules
*.log
leads
gamejam
public/gamejam-games
-62
View File
@@ -1,62 +0,0 @@
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
@@ -1,35 +0,0 @@
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,22 +83,6 @@ 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
@@ -109,17 +93,3 @@ 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.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+3
View File
@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 19 KiB

+4
View File
@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 377 B

+3
View File
@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+12
View File
@@ -0,0 +1,12 @@
<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>

After

Width:  |  Height:  |  Size: 1.8 KiB

+12
View File
@@ -0,0 +1,12 @@
<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>

After

Width:  |  Height:  |  Size: 1.8 KiB

+48 -20
View File
@@ -3,21 +3,42 @@
<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.html" /> <link rel="canonical" href="https://tool-ctrl.de/datenschutz" />
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" /> <link rel="icon" type="image/png" href="/assets/favicon.png" />
<link rel="stylesheet" href="/styles.css?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=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/logo-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<a class="legal-back" href="/">Zurück zur Startseite</a> <nav class="nav" aria-label="Hauptnavigation">
<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>
@@ -36,19 +57,18 @@
</p> </p>
<p> <p>
R&amp;D² UG (haftungsbeschränkt)<br /> R&amp;D² UG (haftungsbeschränkt)<br />
Orbinstraße 1<br /> Orbinstr. 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.html">Impressum</a>.</p> <p>Die vollständigen Anbieterangaben finden Sie im <a href="/impressum">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 Beim Aufruf der Website verarbeitet der Hosting-Anbieter automatisch technische
technische Informationen, die Ihr Browser übermittelt (sogenannte Informationen, die Ihr Browser übermittelt (sogenannte Server-Logfiles):
Server-Logfiles):
</p> </p>
<ul> <ul>
<li>IP-Adresse des anfragenden Geräts</li> <li>IP-Adresse des anfragenden Geräts</li>
@@ -155,16 +175,24 @@
</p> </p>
</main> </main>
<footer class="legal-footer"> <footer class="site-footer">
<div class="wrap legal-footer-inner"> <div class="wrap footer-bar">
<span>© 2026 ToolCTRL</span> <div class="footer-brand">
<span> <img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
<a href="/">Startseite</a> · <p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
<a href="/impressum.html">Impressum</a> · </div>
<a href="/datenschutz.html">Datenschutz</a> <div class="footer-links">
</span> <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
<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>
+253 -89
View File
@@ -3,25 +3,43 @@
<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 von ToolCTRL beim TechDay Bruchsal 2026: In wenigen Stunden mit KI ein spielbares Game bauen. Human in the Loop, im HubWerk01 Bruchsal."> <meta name="description" content="AI GameJam beim TechDay Bruchsal 2026: In zwei Stunden mit KI ein spielbares Game bauen. Human in the Loop." />
<link rel="canonical" href="https://tool-ctrl.de/gamejam.html"> <link rel="canonical" href="https://tool-ctrl.de/gamejam">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:site_name" content="ToolCTRL">
<meta property="og:locale" content="de_DE"> <meta property="og:locale" content="de_DE">
<meta property="og:url" content="https://tool-ctrl.de/gamejam.html"> <meta property="og:site_name" content="ToolCTRL">
<meta property="og:title" content="AI GameJam: Human in the Loop, TechDay Bruchsal 2026"> <meta property="og:title" content="AI GameJam — Human in the Loop">
<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:description" content="Zwei Stunden, KI, ein spielbares Ergebnis: die AI GameJam beim TechDay Bruchsal 2026.">
<meta property="og:image" content="https://tool-ctrl.de/assets/og-image.png"> <meta property="og:url" content="https://tool-ctrl.de/gamejam">
<meta property="og:image:width" content="1200"> <meta property="og:image" content="https://tool-ctrl.de/assets/logo-a.webp">
<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">
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610"> <meta name="theme-color" content="#0c0c0e">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="icon" type="image/png" href="/assets/favicon.png">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" 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;
@@ -109,15 +127,18 @@
.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; }
@@ -126,6 +147,7 @@
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;
@@ -153,6 +175,10 @@
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;
@@ -160,6 +186,7 @@
} }
.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;
@@ -193,6 +220,7 @@
.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); }
@@ -273,41 +301,159 @@
.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); } .field input:focus { outline: none; border-color: var(--ac); background: #fff; }
.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; transition: all .15s; background: var(--bg2); cursor: pointer;
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 {
background: var(--ac); color: #ffffff; border: none; position: relative; isolation: isolate; overflow: visible;
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;
transition: opacity .15s; box-shadow: 0 7px 0 #000000, 0 11px 0 #343437, 0 17px 18px -10px rgba(0,0,0,.48),
inset 0 2px 0 rgba(255,255,255,.22);
transition: box-shadow .12s ease, transform .12s ease, background .15s ease;
} }
.btn:hover { opacity: 0.82; } .btn::after {
.btn:disabled { background: var(--line2); color: var(--muted); cursor: not-allowed; } content: ""; position: absolute; left: 5px; right: -6px; bottom: -8px;
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: #c0392b; } .status.err { color: #0c0c0e; font-weight: 700; }
/* ===== GALLERY ===== */ /* ===== GALLERY ===== */
.btn-ghost { .btn-ghost {
background: var(--panel); border: 1px solid var(--line2); color: var(--text); position: relative; isolation: isolate; overflow: visible;
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:hover { border-color: var(--ac); } .btn-ghost::after {
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; }
@@ -339,6 +485,22 @@
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; }
@@ -352,6 +514,17 @@
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>
@@ -360,12 +533,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="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNzEzLjMyIDY3OC42MSI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNSk7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNik7CiAgICAgIH0KCiAgICAgIC5jbHMtMyB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtNCk7CiAgICAgIH0KCiAgICAgIC5jbHMtNCB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMyk7CiAgICAgIH0KCiAgICAgIC5jbHMtNSB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQtMik7CiAgICAgIH0KCiAgICAgIC5jbHMtNiB7CiAgICAgICAgZmlsbDogdXJsKCNsaW5lYXItZ3JhZGllbnQpOwogICAgICB9CiAgICA8L3N0eWxlPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSI2NTguOTYiIHkxPSIzMS45NiIgeDI9IjI5Ny43IiB5Mj0iNDU4LjYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDAwIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzMzMyIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTIiIHgxPSIxNjg0LjMiIHkxPSIxMTQyLjc1IiB4Mj0iMjIwNi4wOSIgeTI9IjE1NTMuNTEiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE4MzMuNzQgLTExMDguOTYpIHJvdGF0ZSg1LjA2KSBza2V3WCgxNC4yNSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYjJiMmIyIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iLjc4IiBzdG9wLWNvbG9yPSIjZmZmIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtMyIgeDE9IjE3MTkuMTMiIHkxPSIxMDk4LjUxIiB4Mj0iMjI0MC45MSIgeTI9IjE1MDkuMjgiIHhsaW5rOmhyZWY9IiNsaW5lYXItZ3JhZGllbnQtMiIvPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQtNCIgeDE9IjE3NjQuODIiIHkxPSIxMDQwLjQ3IiB4Mj0iMjI4Ni42IiB5Mj0iMTQ1MS4yNCIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImxpbmVhci1ncmFkaWVudC01IiB4MT0iMTgwNi4yNyIgeTE9Ijk4Ny44MiIgeDI9IjIzMjguMDUiIHkyPSIxMzk4LjU5IiB4bGluazpocmVmPSIjbGluZWFyLWdyYWRpZW50LTIiLz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50LTYiIHgxPSIzNDUuMDMiIHkxPSItMTAuMTUiIHgyPSIxMDE0LjU1IiB5Mj0iNTE2LjkxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDEsMCwwKSIgeGxpbms6aHJlZj0iI2xpbmVhci1ncmFkaWVudC0yIi8+CiAgPC9kZWZzPgogIDxnPgogICAgPHBhdGggZD0iTTY4OC43LDU3Mi4wOWwtMTYwLDk4Ljk1Yy05LjE0LDUuNzMtMTkuOTksOC4zNS0zMC43Nyw3LjM3bC0zODAuNjgtMzQuMzZjLTIyLjQ4LTIuMDEtNDAuNzYtMTkuMDEtNDQuMzUtNDEuMzFMLjY0LDE1OS4zMWMtMy4xNy0xOS41Niw1LjYtMzkuMTIsMjIuMy00OS43MkwxODkuNDYsOC4wOGMtMTMuOTUsOS43NSwyMy43NywzMS41NSwyNi44OCw0OS45NWw5MS4zLDM2MS4xM2M2LjIxLDM2LjY4LDM2LjM3LDY0LjU4LDczLjQyLDY3LjkzbDI3OC42NCw0Mi45YzguNjUuNzksMjEuODcsNDUuNjgsMjguOTQsNDIuMDNsLjA2LjA2WiIvPgogICAgPHBhdGggY2xhc3M9ImNscy02IiBkPSJNNjg4LjY0LDU3Mi4wM2MtNy4wNywzLjY2LTE1LjIzLDUuMzYtMjMuODgsNC41N2wtMzQ5LTMxLjVjLTM3LjA0LTMuMzUtNjcuMjEtMzEuMjYtNzMuNDItNjcuOTNMMTcwLjM5LDUzLjM1Yy0zLjExLTE4LjQsNS4xMi0zNS41MiwxOS4wNy00NS4yN2wxLjk1LTEuMTZjNy45OC01LjA2LDE3LjYxLTcuNjIsMjcuOTEtNi43NmwzNTguNTYsMzAuMzRjMjkuNDksMi41LDUzLjU2LDI0LjYyLDU4LjYxLDUzLjhsNzYuMTYsNDQwLjM5YzMuNTMsMjAuMjktNy4xOSwzOC45OS0yNC4wMSw0Ny4zNFoiLz4KICA8L2c+CiAgPGc+CiAgICA8Zz4KICAgICAgPHBhdGggY2xhc3M9ImNscy01IiBkPSJNMzAwLjIyLDM0My45M2wtMTUuMDEsNC4yNiwxNS43Myw5Ni4wNWM0LjIzLDI1LjgzLDIxLjg4LDQyLjU5LDQ3LjIxLDQ0LjgzbDIwLjQyLDEuODEtMi40OC0xNS4xNC0yMC40Mi0xLjgxYy0xNS44MS0xLjQtMjYuMDQtMTEuNDQtMjguNzktMjguMjhsLTguMjQtNTAuMzQsNDQuOTMsMy45Ny0yLjQ1LTE0Ljk0LTQ0LjkzLTMuOTctNS45Ny0zNi40M1oiLz4KICAgICAgPHBhdGggY2xhc3M9ImNscy00IiBkPSJNNDIxLjk2LDM4Ny45N2MtMzEuODgtMi44Mi01MS4yNiwxOC4xLTQ2LjA4LDQ5LjczLDUuMTgsMzEuNjQsMzIuMDgsNTYuNjUsNjMuOTcsNTkuNDcsMzEuODgsMi44Miw1MS4yNi0xOC4xLDQ2LjA4LTQ5LjczLTUuMTgtMzEuNjQtMzIuMDgtNTYuNjUtNjMuOTctNTkuNDdaTTQzNy4zNiw0ODIuMDNjLTIyLjktMi4wMy00MS41NC0xOS42Ny00NS4zNS00Mi45LTMuOC0yMy4yMyw5LjUzLTM4LjA0LDMyLjQzLTM2LjAyLDIyLjksMi4wMyw0MS41NCwxOS42Niw0NS4zNSw0Mi45LDMuOCwyMy4yMy05LjUzLDM4LjA0LTMyLjQyLDM2LjAyWiIvPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik01NDEuMzcsMzk4LjUzYy0zMS44OC0yLjgyLTUxLjI2LDE4LjEtNDYuMDgsNDkuNzMsNS4xOCwzMS42NCwzMi4wOCw1Ni42NSw2My45Nyw1OS40NywzMS44OCwyLjgyLDUxLjI2LTE4LjEsNDYuMDgtNDkuNzMtNS4xOC0zMS42NC0zMi4wOC01Ni42NS02My45Ny01OS40N1pNNTU2Ljc3LDQ5Mi41OWMtMjIuOS0yLjAzLTQxLjU0LTE5LjY3LTQ1LjM1LTQyLjktMy44LTIzLjIzLDkuNTMtMzguMDQsMzIuNDMtMzYuMDIsMjIuOSwyLjAzLDQxLjU0LDE5LjY2LDQ1LjM1LDQyLjksMy44LDIzLjIzLTkuNTMsMzguMDQtMzIuNDIsMzYuMDJaIi8+CiAgICAgIDxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI2MDcuNjcgMzY4LjQxIDYzMS4xIDUxMS40NiA2NDcuMDQgNTEyLjg3IDYyMi42OCAzNjQuMTUgNjA3LjY3IDM2OC40MSIvPgogICAgPC9nPgogICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTgyLjM5LDE2NC4wN2wxOC4zLDEuNjItNTYuMjktNzUuMTJjLTMuMTUtNC4yLTcuOTMtNi44Ni0xMy4xNi03LjMzaDBjLTcuNjEtLjY3LTE0Ljg2LDMuNDEtMTguMjIsMTAuMjdsLTMwLjMsNjEuNzUsMTkuMiwxLjcsMjcuNTctNTYuMmMuOTktMi4wMSwzLjc1LTIuMjcsNS4xLS40OGw0Ny44MSw2My43OVoiLz4KICA8L2c+Cjwvc3ZnPg==" alt="ToolCTRL"></a> <a href="/" aria-label="Zur ToolCTRL-Startseite"><img class="tc" src="/assets/wordmark-black.svg" 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>
<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,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 truncated <img class="mit" src="/assets/mitup-logo.webp" alt="mITup Bruchsal">
</div> </div>
</div> </div>
</div> </div>
@@ -445,18 +618,6 @@
</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">
@@ -472,6 +633,30 @@
</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">
@@ -510,40 +695,23 @@
</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">
<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 class="tc" src="/assets/wordmark-black.svg" alt="ToolCTRL">
<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 <img src="/assets/mitup-logo.webp" alt="mITup Bruchsal">
</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>
@@ -593,13 +761,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";
timerEl.textContent = fmt(start - now); setTimerText(timerEl, 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";
timerEl.textContent = "//"; setTimerText(timerEl, "//");
timerLabelEl.textContent = "bis bald"; timerLabelEl.textContent = "bis bald";
} else { } else {
let idx = 0; let idx = 0;
@@ -611,14 +779,26 @@ function renderHud() {
nameEl.textContent = cur.name; nameEl.textContent = cur.name;
subEl.textContent = cur.sub; subEl.textContent = cur.sub;
if (next) { if (next) {
timerEl.textContent = fmt(phaseDate(next.t) - now); setTimerText(timerEl, fmt(phaseDate(next.t) - now));
timerLabelEl.textContent = "bis: " + next.name; timerLabelEl.textContent = "bis: " + next.name;
} else { timerEl.textContent = ""; timerLabelEl.textContent = ""; } } else { setTimerText(timerEl, ""); 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");
@@ -647,10 +827,6 @@ 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");
@@ -686,23 +862,11 @@ function pickFile(f) {
refreshUploadBtn(); refreshUploadBtn();
} }
function refreshUploadBtn() { function refreshUploadBtn() {
uploadBtn.disabled = !(uploadsOpen() && API_BASE && chosenFile && teamEl.value.trim() && gameEl.value.trim()); uploadBtn.disabled = !(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;
@@ -754,7 +918,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"
allow="autoplay 'none'" loading="lazy" title="${name}"></iframe> 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
+45 -17
View File
@@ -3,21 +3,42 @@
<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.html" /> <link rel="canonical" href="https://tool-ctrl.de/impressum" />
<link rel="icon" type="image/png" href="/assets/favicon.png?v=20260610" /> <link rel="icon" type="image/png" href="/assets/favicon.png" />
<link rel="stylesheet" href="/styles.css?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=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/logo-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<a class="legal-back" href="/">Zurück zur Startseite</a> <nav class="nav" aria-label="Hauptnavigation">
<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>
@@ -28,7 +49,7 @@
<h2>Anbieter</h2> <h2>Anbieter</h2>
<p> <p>
R&amp;D² UG (haftungsbeschränkt)<br /> R&amp;D² UG (haftungsbeschränkt)<br />
Orbinstraße 1<br /> Orbinstr. 1<br />
76646 Bruchsal<br /> 76646 Bruchsal<br />
Deutschland Deutschland
</p> </p>
@@ -40,7 +61,6 @@
<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>
@@ -60,7 +80,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 />
Orbinstraße 1<br /> Orbinstr. 1<br />
76646 Bruchsal 76646 Bruchsal
</p> </p>
@@ -90,16 +110,24 @@
</p> </p>
</main> </main>
<footer class="legal-footer"> <footer class="site-footer">
<div class="wrap legal-footer-inner"> <div class="wrap footer-bar">
<span>© 2026 ToolCTRL</span> <div class="footer-brand">
<span> <img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
<a href="/">Startseite</a> · <p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
<a href="/impressum.html">Impressum</a> · </div>
<a href="/datenschutz.html">Datenschutz</a> <div class="footer-links">
</span> <a href="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
<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>
+332 -244
View File
@@ -3,44 +3,39 @@
<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: Moderne All-in-One-Software für eure Branche</title> <title>ToolCTRL — EU-first Software für souveräne Betriebe</title>
<meta name="description" content="Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt." /> <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." />
<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:site_name" content="ToolCTRL" />
<meta property="og:locale" content="de_DE" /> <meta property="og:locale" content="de_DE" />
<meta property="og:site_name" content="ToolCTRL" />
<meta property="og:title" content="ToolCTRL — EU-first Software für souveräne Betriebe" />
<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:title" content="ToolCTRL: Moderne All-in-One-Software für eure Branche" /> <meta property="og:image" content="https://tool-ctrl.de/assets/logo-a.webp" />
<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="twitter:title" content="ToolCTRL: Moderne All-in-One-Software für eure Branche" /> <meta name="theme-color" content="#0A0A0A" />
<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="icon" type="image/png" href="/assets/favicon.png" />
<meta name="twitter:image" content="https://tool-ctrl.de/assets/og-image.png" /> <link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin />
<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",
"url": "https://tool-ctrl.de", "legalName": "R&D² UG (haftungsbeschränkt)",
"logo": "https://tool-ctrl.de/assets/logo-a.png", "url": "https://tool-ctrl.de/",
"description": "Software mit echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, klar aussieht und sich schnell bedienen lässt.", "email": "info@tool-ctrl.de",
"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>
@@ -48,23 +43,29 @@
<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/logo-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<nav class="nav" aria-label="Hauptnavigation"> <nav class="nav" aria-label="Hauptnavigation">
<a href="/">Start</a> <a href="#leistungen">Leistungen</a>
<a href="/techday.html">TechDay</a> <a href="#branchen">Branchen</a>
<a href="/gamejam.html">GameJam</a> <a href="#vorgehen">Vorgehen</a>
<a class="btn btn-solid btn-sm" href="#kontakt">Kontakt aufnehmen</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> </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="/">Start</a> <a href="#leistungen">Leistungen</a>
<a href="/techday.html">TechDay</a> <a href="#branchen">Branchen</a>
<a href="/gamejam.html">GameJam</a> <a href="#vorgehen">Vorgehen</a>
<a class="btn btn-solid" href="#kontakt">Kontakt aufnehmen</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>
@@ -75,197 +76,344 @@
<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">
Moderne, durchdachte All-in-One-Software für Branchen, die zu <span class="no-break">EU-first</span> Software für souveräne Betriebe.
lange übersehen&nbsp;wurden.
</h1> </h1>
<p class="hero-lead"> <p class="hero-lead">
Ganze Branchen arbeiten noch mit veralteten, zusammengestückelten Keine US-Cloud-Pflicht. Kein Agentur-Bullshit. ToolCTRL baut
Insellösungen. Wir bei ToolCTRL bauen die Alternative: Software mit individuelle SaaS-Systeme für Mittelständler, bei denen Anfragen,
echtem Fokus auf Bedienung und Gestaltung, die gut funktioniert, Termine, Daten und Freigaben nicht länger in Tabellen, Postfächern
klar aussieht und sich schnell bedienen lässt. und halbfertigen Tools hängen bleiben.
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
<a class="btn btn-solid" href="#kontakt">Kontakt aufnehmen</a> <a class="btn btn-solid" href="#kontakt">Ablauf prüfen lassen</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>
<!-- Automatisierung trifft Kontrolle — generisch, kein reales Produkt --> <div class="hero-visual" aria-label="Interaktive Workflow-Control-Grafik">
<div class="hero-visual" aria-hidden="true">
<div class="visual-frame"> <div class="visual-frame">
<div class="visual-label"> <div class="visual-label" aria-hidden="true">
<span class="dot"></span> Workflow · ctrl <span class="window-dots"><i></i><i></i><i></i></span>
</div> </div>
<svg class="node-graph" viewBox="0 0 540 400" role="presentation"> <div class="control-board" data-control-board>
<defs> <svg class="control-lines" viewBox="0 0 540 400" preserveAspectRatio="none" aria-hidden="true">
<pattern id="grid" width="26" height="26" patternUnits="userSpaceOnUse"> <path data-line="inbox"></path>
<path d="M26 0H0V26" fill="none" stroke="#ECECEC" stroke-width="1" /> <path data-line="docs"></path>
</pattern> <path data-line="chat"></path>
<linearGradient id="capface" x1="0" y1="0" x2="0" y2="1"> <path data-line="crm"></path>
<stop offset="0" stop-color="#2A2A2A" /> <path data-line="team"></path>
<stop offset="1" stop-color="#101010" /> <path data-line="report"></path>
</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> </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>
<div class="visual-meta">
<span>Automatisiert</span>
<span>Kontrolliert</span>
<span>Wartbar</span>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- ============ 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 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>
</section>
<!-- ============ LEISTUNGEN ============ --> <!-- ============ LEISTUNGEN ============ -->
<section class="section" id="leistungen"> <section class="section services-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 manuelle Schritte an alleinstehenden Tabellen Wir arbeiten dort, wo Tabellen, manuelle Schritte und getrennte
und veralteten Insellösungen zu viel Zeit und Nerven kosten. Daraus Systeme zu viel Zeit kosten. Daraus bauen wir eine tragfähige
bauen wir moderne und tragfähige Infrastrukturen aus Prozessen, Infrastruktur: Software, Automatisierung und Schnittstellen,
Automatisierung und Schnittstellen, zugeschnitten auf Ihren zugeschnitten auf Ihren Ablauf, nicht umgekehrt.
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>Prozessautomatisierung</h3> <h3>Automation</h3>
<p>ToolCTRL wandelt repetitive Prozesse in eine komplett <p>Wiederkehrende Abläufe werden zu verlässlichen Workflows, die
automatische Hands-Off-Erfahrung, während Sie sich um die ohne manuelles Zutun ausgelöst, verarbeitet und dokumentiert
wichtigeren Aufgaben im Betrieb kümmern.</p> werden.</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>Wir bauen Software, die Ihrem Betrieb so gut sitzt wie ein <p>Anwendungen, die exakt zu Ihrem Geschäft passen. Schlank
maßgeschneiderter Anzug.</p> gebaut, klar bedienbar und auf Dauer wartbar.</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>Alle Datenpunkte integriert und aufbereitet. Ihre ganz eigene <p>Verstreute Daten an einem Ort: aktuell, übersichtlich und
Kommandozentrale.</p> als Grundlage für schnelle Entscheidungen.</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 arbeiten zusammen, statt aneinander vorbei. <p>Bestehende Tools, APIs und Systeme greifen ineinander, statt
Was in einem System passiert, ist im nächsten sofort da.</p> nebeneinander zu laufen.</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>Tools, die ganze Teams durch ihre Arbeitstage tragen. Keine <p>Werkzeuge für das eigene Team, die verstreute Tabellenlösungen
zerstückelten Teillösungen und Tabellenblätter mehr.</p> ablösen und mit Ihren Abläufen mitwachsen.</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; Weiterentwicklung</h3> <h3>Betrieb &amp; Ausbau</h3>
<p>Nach dem Launch ist nicht Schluss: Wartung, Anpassung, <p>Nach dem Launch ist nicht Schluss: Wartung, Anpassung und
Erweiterungen und Updates aus einer Hand. Unsere Software Ausbau aus einer Hand.</p>
wächst mit Ihrem Betrieb.</p>
</article> </article>
</div> </div>
</div> </div>
</section> </section>
<!-- ============ VORGEHEN ============ --> <!-- ============ BRANCHEN ============ -->
<section class="section section-alt" id="vorgehen"> <section class="section industry-section" id="branchen">
<div class="wrap"> <div class="wrap">
<div class="section-head"> <div class="section-head">
<span class="eyebrow">Konzept</span> <span class="eyebrow">Branchen</span>
<h2 class="section-title">Das ToolCTRL Konzept.</h2> <h2 class="section-title">Nicht die Branche ist speziell. Der Ablauf ist es.</h2>
<p class="section-intro"> <p class="section-intro">
Ihr Anliegen wird in vier klare Schritte unterteilt: von der ToolCTRL ist interessant, wenn Ihr Ablauf speziell genug ist, dass
Situationsanalyse über die ersten Grundkonzepte in die konkrete Baukastenlösungen ständig Workarounds erzwingen und niemand mehr
Umsetzung und den Betrieb. Unser Team ist an jeder Stelle für genau weiß, wo der aktuelle Stand liegt.
Sie da, um bestmögliche Ergebnisse zu liefern und </p>
sicherzustellen. </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 ============ -->
<section class="section" id="vorgehen">
<div class="wrap">
<div class="section-head">
<span class="eyebrow">Vorgehen</span>
<h2 class="section-title">In vier klaren Schritten.</h2>
<p class="section-intro">
Kein Wasserfall, kein Blindflug. Ein Ablauf, der Ergebnisse früh
sichtbar macht und Sie an jeder Stelle einbezieht.
</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>Situationsanalyse</h3> <h3>Analyse</h3>
<p>Unser Team geht mit Ihnen alle Prozesse und Abläufe in Ihrem <p>Wir sehen uns Ihren Ablauf an und legen offen, wo Zeit, Daten
Betrieb durch, um klarzustellen, wo Zeit, Effizienz und damit und Klarheit verloren gehen.</p>
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>Grundkonzepte</h3> <h3>Konzept</h3>
<p>Aus der Situationsanalyse erstellen wir zusammen ein <p>Ein konkreter Lösungsvorschlag mit Aufwand, Umfang und
Lastenheft. Daraus bauen wir Ihnen erste Lösungskonzepte, die erwartetem Nutzen, transparent kalkuliert.</p>
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>Nach Vereinbarung des Lösungswegs bekommen Sie früh Ihre <p>Wir bauen in kurzen Zyklen. Sie sehen früh ein funktionierendes
erste funktionierende Version mit allen Grundfunktionen. Ergebnis statt nur Folien.</p>
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 &amp; Wartung</h3> <h3>Betrieb</h3>
<p>Übergabe, Einweisungen, Updates und Wartung, damit Ihre <p>Übergabe, Einweisung und laufende Pflege, damit die Lösung
Software langfristig tragfähig bleibt.</p> dauerhaft trägt.</p>
</li> </li>
</ol> </ol>
</div> </div>
@@ -318,47 +466,6 @@
</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">
@@ -370,16 +477,17 @@
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.png" alt="TechDay Bruchsal 2026 Motiv" width="683" height="384" loading="lazy" decoding="async" /> <img src="/assets/techday-event.webp" 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.html">TechDay ansehen</a> <a href="/techday">TechDay ansehen</a>
</div> </div>
</article> </article>
<article class="event-card event-card-code"> <article class="event-card event-card-code">
@@ -392,7 +500,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.html">GameJam ansehen</a> <a href="/gamejam">GameJam ansehen</a>
</div> </div>
</article> </article>
</div> </div>
@@ -404,11 +512,16 @@
<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">Sprechen wir über Ihr Vorhaben.</h2> <h2 class="section-title">Lassen Sie uns Ihren Ablauf prüfen.</h2>
<p class="section-intro"> <p class="section-intro">
Interesse? Fragen? Lassen Sie uns darüber reden. Füllen Sie das Schicken Sie uns kurz, wo Arbeit aktuell hängen bleibt. Wir melden uns
Formular aus, wir melden uns in Kürze bei Ihnen. mit einer ehrlichen Einschätzung, ob ein eigenes System sinnvoll ist.
</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>
@@ -422,25 +535,13 @@
</div> </div>
<form class="lead-form" id="lead-form" novalidate> <form class="lead-form" id="lead-form" novalidate>
<div class="form-grid">
<div class="form-row"> <div class="form-row">
<label for="f-vorname">Vorname <span>*</span></label> <label for="f-name">Name <span>*</span></label>
<input id="f-vorname" name="vorname" type="text" autocomplete="given-name" required /> <input id="f-name" name="name" type="text" autocomplete="name" required />
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="f-nachname">Nachname <span>*</span></label> <label for="f-firma">Firma</label>
<input id="f-nachname" name="nachname" type="text" autocomplete="family-name" required /> <input id="f-firma" name="firma" type="text" autocomplete="organization" />
</div>
</div>
<div class="form-grid">
<div class="form-row">
<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">
@@ -448,14 +549,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">Telefonnummer</label> <label for="f-telefon">Telefon</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 Ihnen? <span>*</span></label> <label for="f-nachricht">Worum geht es? <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">
@@ -466,7 +567,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.html">Datenschutzerklärung</a> zu.</p> <a href="/datenschutz">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>
@@ -477,36 +578,23 @@
<!-- ============ FOOTER ============ --> <!-- ============ FOOTER ============ -->
<footer class="site-footer"> <footer class="site-footer">
<div class="wrap footer-inner"> <div class="wrap footer-bar">
<div class="footer-brand"> <div class="footer-brand">
<img src="/assets/logo-white.svg" alt="ToolCTRL" class="footer-logo" /> <img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
<p>Die Software, die eure Branche längst verdient hat.</p> <p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
</div> </div>
<div class="footer-cols"> <div class="footer-links">
<div class="footer-col">
<span class="footer-h">Navigation</span>
<a href="#leistungen">Leistungen</a>
<a href="#vorgehen">Vorgehen</a>
<a href="#arbeit">Organisation</a>
<a href="/techday.html">TechDay</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="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
<a href="https://tool-ctrl.de">tool-ctrl.de</a> <i></i>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a> <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> </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=20260610"></script> <script src="/script.js?v=20260554"></script>
</body> </body>
</html> </html>
-1
View File
@@ -1,5 +1,4 @@
User-agent: * User-agent: *
Allow: / Allow: /
Disallow: /gamejam-games/
Sitemap: https://tool-ctrl.de/sitemap.xml Sitemap: https://tool-ctrl.de/sitemap.xml
+309 -13
View File
@@ -9,6 +9,14 @@
/* ---- 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");
@@ -28,6 +36,303 @@
}); });
} }
/* ---- 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");
@@ -46,9 +351,8 @@
function validate() { function validate() {
var ok = true; var ok = true;
["f-vorname", "f-nachname", "f-email", "f-nachricht"].forEach(function (id) { ["f-name", "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) {
@@ -69,22 +373,14 @@
} }
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: (vorname + " " + nachname).trim(), name: form.name.value.trim(),
vorname: vorname, firma: form.firma.value.trim(),
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: "Landing Page" quelle: "Messe — 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.html</loc> <loc>https://tool-ctrl.de/techday</loc>
<lastmod>2026-06-10</lastmod>
<priority>0.8</priority>
</url> </url>
<url> <url>
<loc>https://tool-ctrl.de/gamejam.html</loc> <loc>https://tool-ctrl.de/gamejam</loc>
<lastmod>2026-06-10</lastmod> </url>
<priority>0.6</priority> <url>
<loc>https://tool-ctrl.de/impressum</loc>
</url>
<url>
<loc>https://tool-ctrl.de/datenschutz</loc>
</url> </url>
</urlset> </urlset>
+1866 -331
View File
File diff suppressed because it is too large. Load diff
+39 -77
View File
@@ -3,52 +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>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.html" /> <link rel="canonical" href="https://tool-ctrl.de/techday" />
<!-- Open Graph -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="ToolCTRL" />
<meta property="og:locale" content="de_DE" /> <meta property="og:locale" content="de_DE" />
<meta property="og:url" content="https://tool-ctrl.de/techday.html" /> <meta property="og:site_name" content="ToolCTRL" />
<meta property="og:title" content="TechDay Bruchsal 2026: ToolCTRL" /> <meta property="og:title" content="ToolCTRL beim TechDay Bruchsal 2026" />
<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:description" content="Vortrag, AI GameJam und mITapp beim ersten Bruchsaler TechDay am 27. Mai 2026." />
<meta property="og:image" content="https://tool-ctrl.de/assets/techday-event.png" /> <meta property="og:url" content="https://tool-ctrl.de/techday" />
<meta property="og:image:width" content="683" /> <meta property="og:image" content="https://tool-ctrl.de/assets/techday-event.webp" />
<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" />
<!-- Structured Data --> <link rel="icon" type="image/png" href="/assets/favicon.png" />
<script type="application/ld+json"> <link rel="preload" href="/assets/fonts/space-grotesk-latin.woff2" as="font" type="font/woff2" crossorigin />
{ <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>
@@ -56,23 +25,29 @@
<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/logo-black.svg" alt="ToolCTRL" class="brand-logo" /> <img src="/assets/wordmark-black.svg" alt="ToolCTRL" class="brand-logo" />
</a> </a>
<nav class="nav" aria-label="Hauptnavigation"> <nav class="nav" aria-label="Hauptnavigation">
<a href="/">Start</a> <a href="/#leistungen">Leistungen</a>
<a href="/techday.html">TechDay</a> <a href="/#branchen">Branchen</a>
<a href="/gamejam.html">GameJam</a> <a href="/#vorgehen">Vorgehen</a>
<a class="btn btn-solid btn-sm" href="/#kontakt">Kontakt aufnehmen</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> </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="/">Start</a> <a href="/#leistungen">Leistungen</a>
<a href="/techday.html">TechDay</a> <a href="/#branchen">Branchen</a>
<a href="/gamejam.html">GameJam</a> <a href="/#vorgehen">Vorgehen</a>
<a class="btn btn-solid" href="/#kontakt">Kontakt aufnehmen</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>
@@ -115,7 +90,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.png" <img class="td-feature-img" src="/assets/techday-event.webp" width="683" height="384" decoding="async" fetchpriority="high"
alt="TechDay Bruchsal 2026: Human in the Loop" /> alt="TechDay Bruchsal 2026: Human in the Loop" />
</div> </div>
</div> </div>
@@ -162,7 +137,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.jpg" alt="David Bertram" width="320" height="320" loading="lazy" decoding="async" /> <img class="sp-photo" src="/assets/speaker-david.webp" 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>
@@ -230,7 +205,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.html">Zur GameJam-Seite</a> <a class="btn btn-solid" href="/gamejam">Zur GameJam-Seite</a>
</div> </div>
</div> </div>
<div class="gj-teaser" aria-hidden="true"> <div class="gj-teaser" aria-hidden="true">
@@ -279,36 +254,23 @@
<!-- ============ FOOTER ============ --> <!-- ============ FOOTER ============ -->
<footer class="site-footer"> <footer class="site-footer">
<div class="wrap footer-inner"> <div class="wrap footer-bar">
<div class="footer-brand"> <div class="footer-brand">
<img src="/assets/logo-white.svg" alt="ToolCTRL" class="footer-logo" /> <img src="/assets/wordmark-white.svg" alt="ToolCTRL" class="footer-logo" />
<p>Die Software, die eure Branche längst verdient hat.</p> <p>© <span id="year">2026</span> ToolCTRL · Bruchsal</p>
</div> </div>
<div class="footer-cols"> <div class="footer-links">
<div class="footer-col">
<span class="footer-h">Navigation</span>
<a href="/#leistungen">Leistungen</a>
<a href="/#vorgehen">Vorgehen</a>
<a href="/#arbeit">Organisation</a>
<a href="/techday.html">TechDay</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="mailto:info@tool-ctrl.de">info@tool-ctrl.de</a>
<a href="https://tool-ctrl.de">tool-ctrl.de</a> <i></i>
<a href="https://www.linkedin.com/company/toolctrl/" target="_blank" rel="noopener">LinkedIn</a> <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> </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=20260610"></script> <script src="/script.js?v=20260554"></script>
</body> </body>
</html> </html>
+94 -163
View File
@@ -16,82 +16,36 @@ 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 DATA_DIR = process.env.DATA_DIR const LEADS_DIR = path.join(ROOT, "leads");
? 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(DATA_DIR, "gamejam"); const GAMEJAM_DIR = path.join(ROOT, "gamejam");
const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson"); const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson");
const GAMES_DIR = process.env.DATA_DIR const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games");
? 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: envFlag("CLAMAV_ENABLED", true), enabled: true,
host: process.env.CLAMAV_HOST || "127.0.0.1", host: "127.0.0.1",
port: envInt("CLAMAV_PORT", 3310), /* clamd TCPSocket-Standardport */ port: 3310, /* clamd TCPSocket-Standardport */
timeoutMs: envInt("CLAMAV_TIMEOUT_MS", 15000) timeoutMs: 15000
}; };
/* Bevorzugte Ports — der erste freie wird genutzt. */ /* Bevorzugte Ports — der erste freie wird genutzt. */
const PORT_CANDIDATES = process.env.PORT const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0];
? [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 = process.env.HOST || process.env.HOSTNAME || "0.0.0.0"; const HOST = "0.0.0.0";
/* Erste IPv4-LAN-Adresse dieses Rechners ermitteln. */ /* Erste IPv4-LAN-Adresse dieses Rechners ermitteln. */
function getLanIP() { function getLanIP() {
@@ -115,11 +69,45 @@ 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 });
@@ -138,18 +126,6 @@ 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, " ") + '"';
@@ -174,31 +150,34 @@ function saveLead(lead) {
/* ---------- Statische Dateien ---------- */ /* ---------- Statische Dateien ---------- */
function serveStatic(req, res) { function serveStatic(req, res) {
let urlPath = decodeURIComponent(req.url.split("?")[0]); let parsedUrl;
try {
/* SEO: kanonischer Host (www -> non-www) und /index.html -> / */ parsedUrl = new URL(req.url, "http://local");
const host = (req.headers.host || "").toLowerCase(); } catch (e) {
if (host === "www.tool-ctrl.de") { res.writeHead(400).end("Bad Request");
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";
/* Path-Traversal verhindern */ if (urlPath.endsWith("/") && CLEAN_ROUTES[urlPath.slice(0, -1)]) {
let baseDir = PUBLIC_DIR; redirect(res, urlPath.slice(0, -1) + parsedUrl.search);
let filePath = path.normalize(path.join(PUBLIC_DIR, urlPath)); return;
if (urlPath.startsWith("/gamejam-games/")) {
baseDir = GAMES_DIR;
filePath = path.normalize(path.join(GAMES_DIR, urlPath.replace(/^\/gamejam-games\//, "")));
} }
if (!assertInside(baseDir, filePath)) {
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 */
const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath));
if (!filePath.startsWith(PUBLIC_DIR)) {
res.writeHead(403).end("Forbidden"); res.writeHead(403).end("Forbidden");
return; return;
} }
@@ -210,20 +189,35 @@ function serveStatic(req, res) {
return; return;
} }
const ext = path.extname(filePath).toLowerCase(); const ext = path.extname(filePath).toLowerCase();
/* HTML immer frisch, statische Assets lange cachen (Versionierung via ?v=) */ const etag = etagFor(stat);
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": cacheControl "Cache-Control": cacheControlFor(urlPath, ext, parsedUrl.search),
"ETag": etag,
"Last-Modified": stat.mtime.toUTCString(),
"X-Content-Type-Options": "nosniff"
}; };
/* Hochgeladene GameJam-Spiele nicht indexieren (Fremdcontent) */
if (urlPath.startsWith("/gamejam-games/")) { if (COMPRESSIBLE_EXTS.has(ext)) headers["Vary"] = "Accept-Encoding";
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);
fs.createReadStream(filePath).pipe(res); stream.pipe(res);
}); });
} }
@@ -278,10 +272,6 @@ function handleLead(req, res) {
quelle: String(data.quelle || "Landing Page").slice(0, 120) quelle: String(data.quelle || "Landing Page").slice(0, 120)
}; };
/* Central inbox is source of truth; local files are the fallback so a
lead is never lost if the inbox is unreachable (infra ADR 012). */
forwardLeadToInbox(lead).then((forwarded) => {
if (!forwarded) {
try { try {
saveLead(lead); saveLead(lead);
} catch (e) { } catch (e) {
@@ -290,16 +280,13 @@ function handleLead(req, res) {
error: "Speichern fehlgeschlagen. Bitte später erneut versuchen." error: "Speichern fehlgeschlagen. Bitte später erneut versuchen."
}); });
} }
}
console.log( console.log(
"[lead] " + (forwarded ? "→inbox " : "→local ") + "[lead] " + lead.zeitpunkt + " — " + lead.name +
lead.zeitpunkt + " — " + lead.name +
(lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">" (lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">"
); );
sendJSON(res, 200, { ok: true }); sendJSON(res, 200, { ok: true });
}); });
});
} }
/* ---------- Multipart-Parser (abhängigkeitsfrei) ---------- */ /* ---------- Multipart-Parser (abhängigkeitsfrei) ---------- */
@@ -406,13 +393,6 @@ 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) {
@@ -543,57 +523,10 @@ 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." });
@@ -653,8 +586,6 @@ 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