anpassung style + readme

This commit is contained in:
Mara Zöllner 2026-07-01 17:25:18 +02:00
parent 88cd686a49
commit 1bd6f47f73
5 changed files with 280 additions and 377 deletions

101
README.md
View file

@ -20,14 +20,7 @@ Der bisher dokumentierte Serverpfad lautet:
Glueck-Auf/
├── flask_app/
│ ├── app.py Flask-Anwendung und Routen
│ ├── templates/
│ │ ├── base.html Gemeinsames Layout und Styling
│ │ ├── index.html Startseite des Stadion-Escape-Rooms
│ │ └── raetsel.html Rätselseite mit Formular
│ └── assets/
│ ├── css/ Platz für ausgelagerte Stylesheets
│ ├── img/ Platz für Bilder
│ └── js/ Platz für JavaScript
│ └── init_db.py Legt die SQLite-Datenbank für das Spiel an
├── index.html Startseite der bisherigen statischen Website
├── ueber_mich.html Persönliche Vorstellungsseite
@ -71,81 +64,43 @@ Die Anwendung läuft anschließend auf:
http://localhost:9007/
```
Der Entwicklungsserver lauscht auf `0.0.0.0` und verwendet Port `9007`.
Der Entwicklungsserver lauscht auf `0.0.0.0` und verwendet Port `9000`.
## Flask-Routen
| Route | Methoden | Inhalt |
| --- | --- | --- |
| `/` und `/app/` | GET | Startseite des Stadion-Escape-Rooms |
| `/raetsel` und `/app/raetsel` | GET, POST | Rätsel am Stadiontor |
| `/frage` und `/app/frage` | GET, POST | Alternative Adressen für dieselbe Rätselseite |
Die Routen funktionieren jeweils auch mit abschließendem Schrägstrich.
| `/` und `/app/` | GET, POST | Startseite mit Namenseingabe |
| `/raum/<raum_id>` und `/app/raum/<raum_id>` | GET, POST | Räume, Ausgänge und Rätsel des Stadion-Escape-Rooms |
## Aufbau der Flask-App
### `flask_app/app.py`
- Erstellt die Flask-Anwendung.
- Rendert die HTML-Seiten mit Jinja.
- Nimmt die Antwort des Rätsel-Formulars per `POST` entgegen.
- Vergleicht die Eingabe mit dem gesuchten Code `4`.
- Übergibt die passende Rückmeldung an das Template.
- Stellt zukünftige statische Dateien aus `flask_app/assets/` unter
`/assets` bereit.
- Baut die HTML-Seiten direkt in Python-Strings zusammen.
- Enthält das gemeinsame Styling direkt in der Konstante `STYLE`.
- Nimmt den Namen und Rätselantworten per `POST` entgegen.
- Speichert Spielstart und Abschlusszeit in SQLite.
- Zeigt nach dem gelösten Rätsel eine Highscore-Liste an.
### Templates
### `flask_app/init_db.py`
`base.html` enthält das gemeinsame HTML-Grundgerüst, die Navigation und das
aktuelle Styling. `index.html` und `raetsel.html` erweitern dieses
Basis-Template mit Jinja-Blöcken.
Die Startseite führt in den Stadiongang ein. Auf der Rätselseite kann der Code
für das Stadiontor eingegeben werden. Die Auswertung erfolgt durch Flask auf
dem Server und wird anschließend auf derselben Seite angezeigt.
Legt die SQLite-Datenbank `adventure.db` an und füllt sie mit den Räumen,
Ausgängen und der Rätselantwort.
### Ablauf des Rätsels
1. Die Startseite wird über `/` oder `/app/` aufgerufen.
2. Der Link „Zum Stadiontor“ führt zur Rätselseite.
3. Das Formular sendet die eingegebene Antwort mit der Methode `POST` an
dieselbe Route.
4. Flask liest den Wert aus `request.form["antwort"]`.
5. Bei der Antwort `4` wird eine Erfolgsmeldung angezeigt, bei jeder anderen
Eingabe eine Fehlermeldung.
2. Nach der Namenseingabe wird ein Spielstand in der Datenbank angelegt.
3. Der Link „Zum Stadiontor“ führt in den ersten Raum.
4. Über Ausgänge bewegt man sich durch Tunnel, Kabine, Kiosk und Nordkurve.
5. Im Rätselraum liest Flask `request.form["antwort"]`.
6. Bei der richtigen Antwort wird die Abschlusszeit gespeichert und der
Highscore angezeigt.
Die Rückmeldung wird in `app.py` erzeugt und als Variable `ergebnis` an
`raetsel.html` übergeben. Jinja zeigt den Ergebnisbereich nur an, wenn diese
Variable vorhanden ist.
### Gemeinsames Template `base.html`
Das Basis-Template stellt die wiederverwendbaren Bestandteile der Flask-Seiten
bereit:
- HTML-Grundgerüst und deutsche Spracheinstellung,
- responsive Meta-Angaben,
- Google Fonts,
- Navigation zur Start- und Rätselseite,
- gemeinsames blaues Stadion-Design,
- responsive Darstellung für kleinere Bildschirme,
- Jinja-Blöcke für Seitentitel und Seiteninhalt.
Die beiden Unterseiten verwenden `{% extends "base.html" %}`. Dadurch müssen
Navigation, Layout und Styling nicht in jeder Datei wiederholt werden.
### Vorbereitete Assets
Flask wurde mit folgendem Verzeichnis für statische Dateien eingerichtet:
```text
flask_app/assets/
```
Dateien aus diesem Verzeichnis wären unter `/assets/...` erreichbar. Die
Unterordner `css`, `img` und `js` sind bereits angelegt, derzeit aber noch
leer. Das CSS der Flask-App steht deshalb momentan direkt in `base.html`.
Das HTML-Grundgerüst, die Navigation und das blaue Stadion-Design werden direkt
in `app.py` erzeugt. Es gibt keine Jinja-Templates für die Flask-App.
## Statische Website
@ -161,8 +116,7 @@ anderem:
Die statische Website und die Flask-App sind derzeit getrennte Bereiche des
Projekts. Die Dateien in den Verzeichnissen `css/`, `img/` und `js/` gehören
zu den statischen HTML-Seiten. Für die Flask-App ist das Verzeichnis
`flask_app/assets/` vorgesehen.
zu den statischen HTML-Seiten.
### `index.html` Startseite
@ -284,10 +238,8 @@ verwenden `../img/logo.png`.
| Datei oder Ziel | Verlinkt beziehungsweise verwendet in | Zweck |
| --- | --- | --- |
| `flask_app/templates/index.html` | Flask-Routen `/` und `/app/` | Start des Stadion-Escape-Rooms |
| `flask_app/templates/raetsel.html` | Rätsel- und Frage-Routen | Formular und Auswertung des Stadiontor-Codes |
| `flask_app/templates/base.html` | Beide Flask-Seiten | Gemeinsames Layout, Navigation und Styling |
| `flask_app/assets/` | Flask-Konfiguration in `app.py` | Vorbereiteter Bereich für statische Flask-Dateien |
| `flask_app/app.py` | Flask-Routen `/`, `/app/` und `/app/raum/<raum_id>` | Logik, HTML-Ausgabe und Styling des Stadion-Escape-Rooms |
| `flask_app/init_db.py` | Lokale Vorbereitung der Flask-App | Erstellt die SQLite-Datenbank |
| `index.html` | Navigation aller statischen Seiten | Rückkehr zur statischen Startseite |
| `ueber_mich.html` | Navigation, Startseiten-Button und Startseiten-Card | Vorstellungsseite |
| `eis_projekt.html` | Navigation und Startseiten-Card | Projektseite |
@ -301,13 +253,12 @@ verwenden `../img/logo.png`.
| `js/textanalyse.js` | `uebungen/textanalyse.html` | Textanalyse-Logik |
| `img/Hintergrund.jpg` | `css/style.css` über die Klasse `.hero` | Hero-Hintergrundbild |
| `img/logo.png` | Alle statischen HTML-Seiten | Favicon |
| Google Fonts | Flask-Templates und statische Seiten | Schriftarten Bebas Neue und Barlow |
| Google Fonts | Flask-App und statische Seiten | Schriftarten Bebas Neue und Barlow |
## Verwendete Technologien
- Python 3
- Flask
- Jinja
- HTML5
- CSS3 mit Custom Properties, Grid, Flexbox und Animationen
- JavaScript mit DOM-Manipulation, Events und `localStorage`
@ -317,9 +268,7 @@ verwenden `../img/logo.png`.
- Es gibt derzeit keine `requirements.txt`; Flask muss lokal manuell
installiert werden.
- Die Styles der Flask-App befinden sich aktuell direkt in `base.html`.
- Die Unterordner in `flask_app/assets/` sind vorbereitet, enthalten aber
noch keine eigenen CSS-, Bild- oder JavaScript-Dateien.
- Die Styles der Flask-App befinden sich direkt in `flask_app/app.py`.
- Das Kontaktformular der statischen Website sendet keine Daten.
- Die Projektseite enthält noch keinen vollständig ausgearbeiteten Inhalt.
- Die Angaben im Impressum sind fiktive Platzhalterdaten und nicht für eine

View file

@ -9,39 +9,241 @@ app.secret_key = "stadion-escape-secret"
STYLE = """
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glück Auf Stadion Escape</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after {
box-sizing: border-box;
}
body {
background: #0f172a;
color: #e5e7eb;
font-family: system-ui, sans-serif;
max-width: 640px;
margin: 3rem auto;
padding: 0 1rem;
line-height: 1.6;
margin: 0;
min-height: 100vh;
font-family: "Barlow", sans-serif;
color: #f5f8ff;
background:
linear-gradient(rgba(0, 20, 60, 0.9), rgba(0, 31, 68, 0.96)),
radial-gradient(circle at 50% 120%, rgba(255, 255, 255, 0.16), transparent 34%),
#001f44;
}
a, button {
display: inline-block;
border: 1px solid #334155;
border-radius: 8px;
padding: .45em 1em;
margin: .3em .3em 0 0;
background: #1e293b;
color: #e5e7eb;
nav {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
padding: 1.2rem 2.5rem;
background: rgba(0, 20, 60, 0.88);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.logo {
color: #fff;
font-family: "Bebas Neue", sans-serif;
font-size: 1.7rem;
letter-spacing: 0.12em;
text-decoration: none;
font: inherit;
cursor: pointer;
}
nav a:not(.logo) {
color: rgba(255, 255, 255, 0.52);
font-size: 0.82rem;
letter-spacing: 0.12em;
text-decoration: none;
text-transform: uppercase;
}
nav a:hover {
color: #fff;
}
main {
position: relative;
min-height: calc(100vh - 76px);
display: grid;
align-items: center;
padding: 4rem 3rem;
overflow: hidden;
}
main::before {
content: "";
position: absolute;
inset: auto 8% 0;
height: 42%;
border: 1px solid rgba(122, 179, 255, 0.22);
border-bottom: 0;
border-radius: 50% 50% 0 0;
opacity: 0.75;
}
.scene,
.puzzle {
position: relative;
z-index: 1;
max-width: 760px;
}
.puzzle {
width: min(720px, 100%);
margin: 0 auto;
padding: 2rem;
border: 1px solid rgba(122, 179, 255, 0.28);
background: rgba(0, 31, 68, 0.72);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.28);
}
h1 {
margin-top: 0;
margin-bottom: 1.5rem;
font-family: "Bebas Neue", sans-serif;
font-size: clamp(3rem, 8vw, 7rem);
line-height: 0.92;
letter-spacing: 0.03em;
color: #fff;
text-shadow: 0 4px 40px rgba(0, 80, 200, 0.7);
}
h2 {
margin-top: 2rem;
font-family: "Bebas Neue", sans-serif;
font-size: 2.2rem;
letter-spacing: 0.06em;
}
h1 span {
color: #7ab3ff;
}
p {
max-width: 560px;
color: rgba(255, 255, 255, 0.68);
font-size: 1.05rem;
font-weight: 300;
line-height: 1.8;
}
.tag,
label {
color: rgba(255, 255, 255, 0.46);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.tag {
margin-bottom: 1.2rem;
}
.clue,
.result,
.highscore {
margin: 1.4rem 0;
padding: 1rem 1.2rem;
border-left: 3px solid #7ab3ff;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.82);
}
label {
display: block;
margin-bottom: 0.6rem;
}
form {
margin-top: 1.4rem;
}
input {
padding: .45em .7em;
border-radius: 8px;
border: 1px solid #334155;
background: #020617;
color: #e5e7eb;
width: 100%;
padding: 0.95rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.06);
color: #fff;
outline: none;
font-size: 16px;
}
input:focus {
border-color: rgba(122, 179, 255, 0.9);
box-shadow: 0 0 0 3px rgba(122, 179, 255, 0.12);
}
a.btn,
button {
display: inline-block;
margin-top: 1rem;
padding: 0.82rem 2.2rem;
color: #001f44;
background: #fff;
border: 0;
text-decoration: none;
font-family: "Barlow", sans-serif;
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.2s, color 0.2s, transform 0.15s;
}
a.btn:hover,
button:hover {
color: #fff;
background: #0057c2;
transform: translateY(-2px);
}
.secondary {
padding-inline: 1.6rem;
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
ol {
padding-left: 1.4rem;
}
li {
margin-bottom: 0.45rem;
color: rgba(255, 255, 255, 0.78);
}
@media (max-width: 640px) {
nav {
align-items: flex-start;
flex-direction: column;
padding: 1rem 1.5rem;
}
main {
padding: 3rem 1.5rem;
}
.puzzle {
padding: 1.4rem;
}
}
</style>
</head>
<body>
<nav>
<a class="logo" href="/app/">Glück Auf</a>
<a href="/app/raum/stadiontor">Stadiontor</a>
</nav>
<main>
"""
HTML_END = """
</main>
</body>
</html>
"""
@ -51,6 +253,10 @@ def get_db():
return db
def seite_html(inhalt, klasse="puzzle"):
return STYLE + '<section class="' + klasse + '">' + inhalt + "</section>" + HTML_END
def highscore_html(db):
eintraege = db.execute(
"""
@ -65,10 +271,10 @@ def highscore_html(db):
if not eintraege:
return ""
html = "<h2>Highscore</h2><ol>"
html = '<div class="highscore"><h2>Highscore</h2><ol>'
for eintrag in eintraege:
html += "<li>" + escape(eintrag["name"]) + " - " + str(round(eintrag["dauer"], 1)) + " Sekunden</li>"
html += "</ol>"
html += "</ol></div>"
return html
@ -92,19 +298,23 @@ def start():
session["spieler_id"] = spieler_id
session["spieler_name"] = name
return STYLE + (
return seite_html(
"<h1>Eintrittskarte gelöst</h1>"
"<p>Willkommen, " + escape(name) + ". Dein Weg durchs Stadion beginnt.</p>"
'<a href="/app/raum/stadiontor">Zum Stadiontor</a>'
'<a class="btn" href="/app/raum/stadiontor">Zum Stadiontor</a>'
)
return STYLE + (
"<h1>Stadion Escape</h1>"
return seite_html(
'<p class="tag">Escape Room im Stadiongang</p>'
"<h1>Glück Auf<br><span>im Stadion</span></h1>"
"<p>Trage deinen Namen auf der Eintrittskarte ein. Ab dann läuft deine Zeit.</p>"
'<form method="post">'
'<label for="name">Name auf der Eintrittskarte</label>'
'<input name="name" placeholder="Dein Name" autocomplete="off">'
"<button>Abenteuer starten</button>"
"</form>"
'<p><a class="btn secondary" href="/app/raum/stadiontor">Direkt zum Stadiontor</a></p>',
"scene",
)
@ -116,7 +326,9 @@ def raum(raum_id):
if r is None:
db.close()
return STYLE + '<h1>Raum nicht gefunden</h1><a href="/app/">Zurück zum Start</a>', 404
return seite_html(
'<h1>Raum nicht gefunden</h1><a class="btn" href="/app/">Zurück zum Start</a>'
), 404
if r["raetsel_frage"]:
if request.method == "POST":
@ -141,22 +353,23 @@ def raum(raum_id):
)
html += highscore_html(db)
db.close()
return STYLE + html
return STYLE + (
return seite_html(html)
return seite_html(
'<h1>Leider falsch</h1>'
'<p>Erwin schüttelt den Kopf. Die Botschaft stimmt noch nicht.</p>'
'<a href="/app/raum/' + raum_id + '">Nochmal versuchen</a>'
'<a class="btn" href="/app/raum/' + escape(raum_id) + '">Nochmal versuchen</a>'
)
html = "<h1>" + r["name"] + "</h1>"
html += "<p>" + r["beschreibung"] + "</p>"
html = "<h1>" + escape(r["name"]) + "</h1>"
html += "<p>" + escape(r["beschreibung"]) + "</p>"
html += '<form method="post">'
html += "<p>" + r["raetsel_frage"] + "</p>"
html += '<input name="antwort" autocomplete="off">'
html += '<p class="clue">' + escape(r["raetsel_frage"]) + "</p>"
html += '<label for="antwort">Deine Antwort</label>'
html += '<input id="antwort" name="antwort" autocomplete="off">'
html += "<button>OK</button>"
html += "</form>"
db.close()
return STYLE + html
return seite_html(html)
ausgaenge = db.execute(
"SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?",
@ -164,14 +377,14 @@ def raum(raum_id):
).fetchall()
db.close()
html = "<h1>" + r["name"] + "</h1>"
html += "<p>" + r["beschreibung"] + "</p>"
html = "<h1>" + escape(r["name"]) + "</h1>"
html += "<p>" + escape(r["beschreibung"]) + "</p>"
for a in ausgaenge:
html += '<a href="/app/raum/' + a["nach_raum"] + '">'
html += a["richtung"]
html += '<a class="btn" href="/app/raum/' + escape(a["nach_raum"]) + '">'
html += escape(a["richtung"])
html += "</a> "
return STYLE + html
return seite_html(html)
if __name__ == "__main__":

View file

@ -1,212 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Glück Auf Stadion Escape{% endblock %}</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:wght@300;400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Barlow", sans-serif;
color: #f5f8ff;
background:
linear-gradient(rgba(0, 20, 60, 0.9), rgba(0, 31, 68, 0.96)),
radial-gradient(circle at 50% 120%, rgba(255, 255, 255, 0.16), transparent 34%),
#001f44;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
padding: 1.2rem 2.5rem;
background: rgba(0, 20, 60, 0.88);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.logo {
color: #fff;
font-family: "Bebas Neue", sans-serif;
font-size: 1.7rem;
letter-spacing: 0.12em;
text-decoration: none;
}
nav a:not(.logo) {
color: rgba(255, 255, 255, 0.52);
font-size: 0.82rem;
letter-spacing: 0.12em;
text-decoration: none;
text-transform: uppercase;
}
nav a:hover {
color: #fff;
}
main {
position: relative;
min-height: calc(100vh - 76px);
display: grid;
align-items: center;
padding: 4rem 3rem;
overflow: hidden;
}
main::before {
content: "";
position: absolute;
inset: auto 8% 0;
height: 42%;
border: 1px solid rgba(122, 179, 255, 0.22);
border-bottom: 0;
border-radius: 50% 50% 0 0;
opacity: 0.75;
}
.scene,
.puzzle {
position: relative;
z-index: 1;
max-width: 760px;
}
.puzzle {
width: min(720px, 100%);
margin: 0 auto;
padding: 2rem;
border: 1px solid rgba(122, 179, 255, 0.28);
background: rgba(0, 31, 68, 0.72);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.28);
}
h1 {
margin-top: 0;
margin-bottom: 1.5rem;
font-family: "Bebas Neue", sans-serif;
font-size: clamp(3rem, 8vw, 7rem);
line-height: 0.92;
letter-spacing: 0.03em;
color: #fff;
text-shadow: 0 4px 40px rgba(0, 80, 200, 0.7);
}
h1 span {
color: #7ab3ff;
}
p {
max-width: 560px;
color: rgba(255, 255, 255, 0.68);
font-size: 1.05rem;
font-weight: 300;
line-height: 1.8;
}
.tag,
label {
color: rgba(255, 255, 255, 0.46);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.tag {
margin-bottom: 1.2rem;
}
.clue,
.result {
margin: 1.4rem 0;
padding: 1rem 1.2rem;
border-left: 3px solid #7ab3ff;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.82);
}
label {
display: block;
margin-bottom: 0.6rem;
}
input {
width: 100%;
padding: 0.95rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.06);
color: #fff;
outline: none;
font-size: 16px;
}
input:focus {
border-color: rgba(122, 179, 255, 0.9);
box-shadow: 0 0 0 3px rgba(122, 179, 255, 0.12);
}
.btn,
button {
display: inline-block;
margin-top: 1rem;
padding: 0.82rem 2.2rem;
color: #001f44;
background: #fff;
border: 0;
text-decoration: none;
font-family: "Barlow", sans-serif;
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
cursor: pointer;
transition: background 0.2s, color 0.2s, transform 0.15s;
}
.btn:hover,
button:hover {
color: #fff;
background: #0057c2;
transform: translateY(-2px);
}
.secondary {
padding-inline: 1.6rem;
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
@media (max-width: 640px) {
nav {
align-items: flex-start;
flex-direction: column;
padding: 1rem 1.5rem;
}
main {
padding: 3rem 1.5rem;
}
.puzzle {
padding: 1.4rem;
}
}
</style>
</head>
<body>
<nav>
<a class="logo" href="/app/">Glück Auf</a>
<a href="/app/raetsel">Stadiontor</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View file

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% block title %}Glück Auf Stadion Escape{% endblock %}
{% block content %}
<section class="scene">
<p class="tag">Escape Room im Stadiongang</p>
<h1>Glück Auf<br><span>im Stadion</span></h1>
<p>
Der Gang unter der Tribüne ist fast leer. Aus dem Innenraum
dringt Flutlicht, aber das Drehkreuz zum Spielfeld bleibt gesperrt.
</p>
<p>
Auf einer blauen Tafel steht ein Hinweis:
Der erste Code steckt in der Zahl, die jeder Fan sofort erkennt.
</p>
<a class="btn" href="/app/raetsel">Zum Stadiontor</a>
</section>
{% endblock %}

View file

@ -1,28 +0,0 @@
{% extends "base.html" %}
{% block title %}Rätsel am Stadiontor{% endblock %}
{% block content %}
<section class="puzzle">
<h1>Das Stadiontor</h1>
<p>
Das Drehkreuz ist verriegelt. Über dem Eingang leuchtet eine besonders blaue Markierung: Nordkurve.
</p>
<p class="clue">
Hinweis: Schau auf das Logo des Vereins. Ein wahrer Fan erkennt die
gesuchte Zahl sofort.
</p>
<form method="post">
<label for="antwort">Code am Stadiontor</label>
<input id="antwort" name="antwort" required>
<button type="submit">Tor öffnen</button>
</form>
{% if ergebnis %}
<p class="result">{{ ergebnis }}</p>
{% endif %}
<p><a class="btn secondary" href="/app/">Zurück in den Gang</a></p>
</section>
{% endblock %}