Add PunktFri Flask landing page

- Landing page with hero, problemet, målgruppe, idé, disclaimer sections
- Signup form → SQLite + file log (navn, email, domæner, egne_ns, kommentar)
- /tak confirmation page
- /admin with HTTP Basic Auth
- /health endpoint
- Mobile-first minimal CSS, Danish throughout
- Honest 'under undersøgelse' framing — no premature organisational claims
This commit is contained in:
Henrik Jess Nielsen
2026-04-26 18:20:54 +02:00
parent 6fce8985b4
commit 7fe85dfe2c
9 changed files with 1011 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.venv/
*.db
*.log
__pycache__/
*.pyc
.env

View File

@@ -0,0 +1,45 @@
# PunktFri.dk
Flask landing page for PunktFri — a Danish non-profit registrar initiative for self-hosters.
## Setup
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python app.py
```
Visit `http://localhost:5000`
## Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `5000` | Port to listen on |
| `DATABASE` | `punktfri.db` | SQLite database path |
| `LOG_FILE` | `signups.log` | Signup log file path |
| `ADMIN_USER` | `admin` | Admin username |
| `ADMIN_PASS` | `punktfri2024` | Admin password |
Change `ADMIN_USER` and `ADMIN_PASS` before deploying publicly.
## Routes
- `/` — Landing page + signup form
- `/tak` — Confirmation page after signup
- `/admin` — Signup table (HTTP Basic Auth)
- `/health` — Health check (returns `{"status": "ok"}`)
## Data
Signups are stored in two places:
- **SQLite** (`punktfri.db`) — queryable, used by `/admin`
- **Log file** (`signups.log`) — append-only plaintext log
Both are created automatically on first run.
## Admin
Visit `http://localhost:5000/admin` and enter the credentials above.

137
app.py Normal file
View File

@@ -0,0 +1,137 @@
import os
import logging
import sqlite3
from functools import wraps
from flask import Flask, render_template, request, redirect, url_for, g, Response
DATABASE = os.environ.get("DATABASE", "punktfri.db")
LOG_FILE = os.environ.get("LOG_FILE", "signups.log")
ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
ADMIN_PASS = os.environ.get("ADMIN_PASS", "punktfri2024")
PORT = int(os.environ.get("PORT", 5000))
app = Flask(__name__)
signup_logger = logging.getLogger("signups")
signup_logger.setLevel(logging.INFO)
_fh = logging.FileHandler(LOG_FILE)
_fh.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
signup_logger.addHandler(_fh)
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_db(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()
def init_db():
with app.app_context():
db = get_db()
db.execute(
"""
CREATE TABLE IF NOT EXISTS signups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
navn TEXT NOT NULL,
email TEXT NOT NULL,
domaener INTEGER NOT NULL,
egne_ns TEXT NOT NULL,
kommentar TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
)
db.commit()
def require_admin(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or auth.username != ADMIN_USER or auth.password != ADMIN_PASS:
return Response(
"Login krævet.",
401,
{"WWW-Authenticate": 'Basic realm="PunktFri Admin"'},
)
return f(*args, **kwargs)
return decorated
@app.route("/", methods=["GET", "POST"])
def index():
error = None
if request.method == "POST":
navn = request.form.get("navn", "").strip()
email = request.form.get("email", "").strip().lower()
domaener_raw = request.form.get("domaener", "").strip()
egne_ns = request.form.get("egne_ns", "").strip()
kommentar = request.form.get("kommentar", "").strip()
if not navn or not email or not domaener_raw or egne_ns not in ("ja", "nej"):
error = "Udfyld venligst alle påkrævede felter."
else:
try:
domaener = int(domaener_raw)
if domaener < 1:
raise ValueError
except ValueError:
error = "Angiv et gyldigt antal domæner (mindst 1)."
else:
db = get_db()
existing = db.execute(
"SELECT id FROM signups WHERE email = ?", (email,)
).fetchone()
if existing:
error = "Denne e-mail er allerede tilmeldt — tak for din interesse!"
else:
db.execute(
"INSERT INTO signups (navn, email, domaener, egne_ns, kommentar) VALUES (?, ?, ?, ?, ?)",
(navn, email, domaener, egne_ns, kommentar or None),
)
db.commit()
signup_logger.info(
f"SIGNUP navn={navn!r} email={email!r} domaener={domaener} "
f"egne_ns={egne_ns} kommentar={kommentar!r}"
)
return redirect(url_for("tak"))
return render_template("index.html", error=error)
@app.route("/tak")
def tak():
return render_template("tak.html")
@app.route("/admin")
@require_admin
def admin():
db = get_db()
signups = db.execute(
"SELECT * FROM signups ORDER BY timestamp DESC"
).fetchall()
count = db.execute("SELECT COUNT(*) FROM signups").fetchone()[0]
return render_template("admin.html", signups=signups, count=count)
@app.route("/health")
def health():
return {"status": "ok"}, 200
init_db()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=PORT, debug=False)

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
Flask==3.1.0
Werkzeug==3.1.3

563
static/style.css Normal file
View File

@@ -0,0 +1,563 @@
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
color: #1a1a1a;
line-height: 1.65;
background: #fff;
}
/* ── Nav ─────────────────────────────── */
header {
background: #0f172a;
padding: 1rem 1.5rem;
}
header nav {
max-width: 860px;
margin: 0 auto;
}
.logo {
color: #fff;
font-weight: 800;
font-size: 1.2rem;
letter-spacing: -0.02em;
text-decoration: none;
}
.logo span {
color: #818cf8;
}
/* ── Hero ────────────────────────────── */
.hero {
background: #0f172a;
color: #fff;
padding: 5rem 1.5rem 4.5rem;
text-align: center;
border-bottom: 1px solid #1e293b;
}
.hero h1 {
font-size: clamp(2.2rem, 6vw, 3.8rem);
font-weight: 800;
letter-spacing: -0.04em;
margin-bottom: 0.6rem;
line-height: 1.1;
}
.hero .tagline {
font-size: clamp(1rem, 2.5vw, 1.3rem);
color: #94a3b8;
margin-bottom: 2rem;
font-style: italic;
}
.hero .intro {
max-width: 580px;
margin: 0 auto;
font-size: 1.05rem;
color: #cbd5e1;
line-height: 1.75;
}
/* ── Sections ────────────────────────── */
.section {
padding: 3.5rem 1.5rem;
}
.section-inner {
max-width: 760px;
margin: 0 auto;
}
.section:nth-child(odd) {
background: #f8fafc;
}
.section:nth-child(even) {
background: #fff;
}
.section h2 {
font-size: 1.55rem;
font-weight: 700;
margin-bottom: 1.25rem;
letter-spacing: -0.02em;
line-height: 1.2;
}
.section p {
margin-bottom: 1rem;
font-size: 0.975rem;
color: #374151;
}
/* ── Problem list ────────────────────── */
.problem-list {
list-style: none;
margin: 1.25rem 0 0;
}
.problem-list li {
padding: 0.7rem 0 0.7rem 1.6rem;
position: relative;
border-bottom: 1px solid #e5e7eb;
font-size: 0.95rem;
color: #374151;
}
.problem-list li:last-child {
border-bottom: none;
}
.problem-list li::before {
content: "→";
position: absolute;
left: 0;
color: #dc2626;
font-weight: 700;
}
.problem-list li strong {
color: #111827;
}
/* ── Target grid ─────────────────────── */
.target-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
margin-top: 1.25rem;
}
@media (max-width: 580px) {
.target-grid {
grid-template-columns: 1fr;
}
}
.target-box {
padding: 1.25rem 1.25rem 1rem;
border-radius: 7px;
border: 1px solid #e5e7eb;
}
.target-box.not-for {
background: #fff5f5;
border-color: #fecaca;
}
.target-box.is-for {
background: #f0fdf4;
border-color: #bbf7d0;
}
.target-box h3 {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 0.85rem;
}
.target-box.not-for h3 {
color: #dc2626;
}
.target-box.is-for h3 {
color: #16a34a;
}
.target-box ul {
list-style: none;
}
.target-box ul li {
padding: 0.3rem 0 0.3rem 1.3rem;
font-size: 0.875rem;
color: #374151;
position: relative;
}
.target-box.not-for ul li::before {
content: "✕";
position: absolute;
left: 0;
color: #dc2626;
font-size: 0.8rem;
top: 0.35rem;
}
.target-box.is-for ul li::before {
content: "✓";
position: absolute;
left: 0;
color: #16a34a;
font-size: 0.85rem;
top: 0.3rem;
}
/* ── Idea list ───────────────────────── */
.idea-items {
list-style: none;
margin-top: 1.25rem;
}
.idea-items li {
padding: 0.75rem 0 0.75rem 1.6rem;
position: relative;
border-bottom: 1px solid #e5e7eb;
font-size: 0.95rem;
color: #374151;
}
.idea-items li:last-child {
border-bottom: none;
}
.idea-items li::before {
content: "·";
position: absolute;
left: 0.45rem;
top: 0.45rem;
color: #6366f1;
font-size: 1.6rem;
line-height: 1;
}
.status-badge {
display: inline-block;
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
border-radius: 4px;
padding: 0.1rem 0.5rem;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-left: 0.4rem;
vertical-align: middle;
position: relative;
top: -1px;
}
/* ── Not-list ────────────────────────── */
.not-list {
list-style: none;
margin-top: 1.25rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
@media (max-width: 480px) {
.not-list {
grid-template-columns: 1fr;
}
}
.not-list li {
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 0.7rem 1rem;
font-size: 0.875rem;
color: #475569;
display: flex;
align-items: center;
gap: 0.5rem;
}
.not-list li::before {
content: "✕";
color: #94a3b8;
font-size: 0.75rem;
flex-shrink: 0;
}
/* ── Signup form ─────────────────────── */
.form-section {
background: #0f172a !important;
color: #fff;
}
.form-section h2 {
color: #fff;
}
.form-section .lead {
color: #94a3b8 !important;
margin-bottom: 0.5rem;
}
.form-section .disclaimer {
color: #64748b !important;
font-size: 0.85rem !important;
margin-bottom: 1.5rem;
}
.signup-form {
margin-top: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.4rem;
color: #e2e8f0;
}
.form-group .hint {
display: block;
font-size: 0.78rem;
color: #64748b;
font-weight: 400;
margin-top: 0.2rem;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="number"],
.form-group textarea {
width: 100%;
padding: 0.65rem 0.875rem;
background: #1e293b;
border: 1px solid #334155;
border-radius: 5px;
color: #f1f5f9;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.15s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #6366f1;
background: #1e293b;
}
.form-group textarea {
min-height: 90px;
resize: vertical;
}
.radio-group {
display: flex;
gap: 1.5rem;
margin-top: 0.3rem;
}
.radio-group label {
display: flex;
align-items: center;
gap: 0.4rem;
font-weight: 400;
cursor: pointer;
color: #e2e8f0;
font-size: 1rem;
}
.radio-group input[type="radio"] {
width: 1rem;
height: 1rem;
accent-color: #6366f1;
flex-shrink: 0;
}
.error-msg {
background: #7f1d1d;
border: 1px solid #ef4444;
color: #fecaca;
padding: 0.75rem 1rem;
border-radius: 5px;
margin-bottom: 1.25rem;
font-size: 0.9rem;
}
.submit-btn {
background: #6366f1;
color: #fff;
border: none;
padding: 0.875rem 2rem;
font-size: 1rem;
font-weight: 600;
border-radius: 5px;
cursor: pointer;
width: 100%;
transition: background 0.15s;
font-family: inherit;
}
.submit-btn:hover {
background: #4f46e5;
}
/* ── Footer ──────────────────────────── */
footer {
background: #f1f5f9;
border-top: 1px solid #e2e8f0;
padding: 2rem 1.5rem;
}
.footer-inner {
max-width: 760px;
margin: 0 auto;
text-align: center;
}
.footer-inner p {
font-size: 0.85rem;
color: #64748b;
margin-bottom: 0.5rem;
}
.footer-inner a {
color: #6366f1;
text-decoration: none;
}
.footer-inner a:hover {
text-decoration: underline;
}
.footer-small {
font-size: 0.75rem !important;
color: #94a3b8 !important;
margin-top: 0.25rem;
}
/* ── Tak page ────────────────────────── */
.tak-section {
min-height: 65vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 1.5rem;
background: #f8fafc;
}
.tak-card {
max-width: 520px;
text-align: center;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 2.5rem 2rem;
}
.tak-icon {
font-size: 2.8rem;
margin-bottom: 1rem;
}
.tak-card h1 {
font-size: 1.9rem;
font-weight: 700;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
}
.tak-card p {
color: #6b7280;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.back-link {
display: inline-block;
margin-top: 1.25rem;
color: #6366f1;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
}
.back-link:hover {
text-decoration: underline;
}
/* ── Admin ───────────────────────────── */
.admin-section {
padding: 2rem 1.5rem;
max-width: 1100px;
margin: 0 auto;
}
.admin-section h1 {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 0.4rem;
}
.admin-meta {
color: #6b7280;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.table-wrapper {
overflow-x: auto;
}
.signups-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.signups-table th {
background: #f1f5f9;
padding: 0.6rem 0.75rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
color: #374151;
}
.signups-table td {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid #e5e7eb;
vertical-align: top;
}
.signups-table tr:hover td {
background: #f8fafc;
}
.badge-ja {
background: #dcfce7;
color: #166534;
padding: 0.1rem 0.45rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: 700;
}
.badge-nej {
background: #fee2e2;
color: #991b1b;
padding: 0.1rem 0.45rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: 700;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #9ca3af;
}

47
templates/admin.html Normal file
View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Admin — PunktFri{% endblock %}
{% block content %}
<div class="admin-section">
<h1>Tilmeldinger</h1>
<p class="admin-meta">{{ count }} tilmelding{{ 'er' if count != 1 else '' }} i alt</p>
{% if signups %}
<div class="table-wrapper">
<table class="signups-table">
<thead>
<tr>
<th>#</th>
<th>Navn</th>
<th>E-mail</th>
<th>Domæner</th>
<th>Egne NS</th>
<th>Kommentar</th>
<th>Tidspunkt</th>
</tr>
</thead>
<tbody>
{% for s in signups %}
<tr>
<td>{{ s.id }}</td>
<td>{{ s.navn }}</td>
<td>{{ s.email }}</td>
<td>{{ s.domaener }}</td>
<td>
<span class="badge-{{ s.egne_ns }}">{{ s.egne_ns | upper }}</span>
</td>
<td>{{ s.kommentar or '—' }}</td>
<td style="white-space:nowrap; color:#6b7280;">{{ s.timestamp }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<p>Ingen tilmeldinger endnu.</p>
</div>
{% endif %}
</div>
{% endblock %}

29
templates/base.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="PunktFri — non-profit registratorinitiativ for selvhostere med egne navneservere.">
<title>{% block title %}PunktFri{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<nav>
<a href="/" class="logo">Punkt<span>Fri</span></a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<div class="footer-inner">
<p><strong>PunktFri er ikke live endnu.</strong> Dette er en interessetilkendegivelse — ingen forpligtelse, ingen faktura, intet abonnement.</p>
<p>Kontakt: <a href="mailto:info@punktfri.dk">info@punktfri.dk</a></p>
<p class="footer-small">PunktFri er en idé under undersøgelse. Ingen forening er stiftet, ingen akkreditering er søgt.</p>
</div>
</footer>
</body>
</html>

161
templates/index.html Normal file
View File

@@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block title %}PunktFri — Din domæneaftale. Ingen forhandler.{% endblock %}
{% block content %}
<!-- 1. Hero -->
<section class="hero">
<h1>PunktFri</h1>
<p class="tagline">"Din domæneaftale. Ingen forhandler. Ingen bullshit."</p>
<p class="intro">
Punktum dk tvinger fra 2026 alle .dk-domæneejere til at bruge en kommerciel forhandler.
Det rammer dig, der kører egne navneservere og aldrig har haft brug for hjælp.
Vi undersøger om vi kan gøre noget ved det — på vores egne præmisser.
</p>
</section>
<!-- 2. Problemet -->
<section class="section">
<div class="section-inner">
<h2>Problemet</h2>
<p>
Punktum dk ændrer reglerne. Fra 2026 kan du ikke längre administrere dit .dk-domæne direkte
— du skal have en akkrediteret forhandler som mellemled. Uanset om du har brug for dem eller ej.
</p>
<ul class="problem-list">
<li><strong>1. juli 2026:</strong> Nye .dk-domæner kræver en forhandler. Ingen direkte registrering mere.</li>
<li><strong>1. juli 2028:</strong> Alle eksisterende .dk-domæner skal overføres til en forhandler.</li>
<li><strong>Selvhostere rammes direkte:</strong> Kører du egne navneservere og har altid klaret dig selv hos Punktum dk, skal du nu have et administrativt mellemled — mod din vilje.</li>
<li><strong>Prisstigningen er reel:</strong> Kommercielle forhandlere opkræver typisk 90150 kr/år per domæne. Du betalte ~50 kr direkte. Det er dobbelt til tredobbelt pris for et lag du ikke har bedt om.</li>
<li><strong>Ingen teknisk gevinst:</strong> Du mister ikke kontrol over dine navneservere — men du tvinges alligevel til at betale en forhandler for at "hjælpe" dig.</li>
</ul>
</div>
</section>
<!-- 3. Hvem er PunktFri for? -->
<section class="section">
<div class="section-inner">
<h2>Er det noget for dig?</h2>
<p>PunktFri er tænkt til en meget specifik gruppe. Vær ærlig med dig selv.</p>
<div class="target-grid">
<div class="target-box not-for">
<h3>Ikke for dig hvis</h3>
<ul>
<li>Du vil have webhosting</li>
<li>Du vil have DNS-hosting</li>
<li>Du har brug for support på "min hjemmeside virker ikke"</li>
<li>Du ikke ved hvad en navneserver er</li>
<li>Du leder efter den billigste registrar</li>
</ul>
</div>
<div class="target-box is-for">
<h3>For dig hvis</h3>
<ul>
<li>Du kører egne navneservere (BIND, PowerDNS, Knot osv.)</li>
<li>Du har administreret direkte hos Punktum dk i årevis</li>
<li>Du vil beholde fuld kontrol over din infrastruktur</li>
<li>Du bare har brug for et tyndt administrativt lag mod Punktum dk</li>
<li>Du er ligeglad med support, panels og features</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 4. Idéen bag PunktFri -->
<section class="section">
<div class="section-inner">
<h2>Idéen bag PunktFri <span class="status-badge">Under undersøgelse</span></h2>
<p>
PunktFri er på nuværende tidspunkt en idé — ikke en organisation.
Vi undersøger om det er muligt og realistisk at etablere følgende:
</p>
<ul class="idea-items">
<li>En non-profit forening der ansøger om akkreditering som registrator hos Punktum dk</li>
<li>Kostprismodel — ingen markup, ingen upsell, ingen abonnementsservice du ikke har bedt om</li>
<li>Demokratisk struktur — medlemmerne bestemmer, ikke investorer eller kommercielle interesser</li>
<li>Du beholder dine egne navneservere fuldstændigt — vi rører ikke din infrastruktur</li>
<li>Minimal administration: vi håndterer kun det formelle forhold til Punktum dk på dine vegne</li>
</ul>
<p style="margin-top: 1.25rem; font-size: 0.875rem; color: #6b7280;">
Intet er besluttet endnu. Denne side er til for at afdække om der er nok interesse til at
det kan bære sig — juridisk, økonomisk og praktisk.
</p>
</div>
</section>
<!-- 5. Hvad PunktFri IKKE er -->
<section class="section">
<div class="section-inner">
<h2>Hvad PunktFri ikke er</h2>
<p>For at undgå enhver tvivl:</p>
<ul class="not-list">
<li>En hostingudbyder</li>
<li>En DNS-udbyder</li>
<li>En supportlinje</li>
<li>En forretning</li>
<li>Et panel med features</li>
<li>Noget du betaler for nu</li>
</ul>
</div>
</section>
<!-- 6. Signup -->
<section class="section form-section">
<div class="section-inner">
<h2>Er du interesseret?</h2>
<p class="lead">Tilmeld dig som interesseret — det forpligter dig til ingenting.</p>
<p class="disclaimer">
Vi bruger dine oplysninger til at vurdere om der er tilstrækkelig interesse til at gå videre.
Ingen spam, ingen salg, ingen automatiske abonnementer. Kun en enkelt opdatering når vi ved mere.
</p>
{% if error %}
<div class="error-msg">{{ error }}</div>
{% endif %}
<form class="signup-form" method="post" action="/">
<div class="form-group">
<label for="navn">Navn <span style="color:#ef4444">*</span></label>
<input type="text" id="navn" name="navn" required autocomplete="name" placeholder="Dit navn">
</div>
<div class="form-group">
<label for="email">E-mail <span style="color:#ef4444">*</span></label>
<input type="email" id="email" name="email" required autocomplete="email" placeholder="du@eksempel.dk">
</div>
<div class="form-group">
<label for="domaener">
Antal .dk-domæner du administrerer <span style="color:#ef4444">*</span>
</label>
<input type="number" id="domaener" name="domaener" required min="1" placeholder="fx 3">
<span class="hint">Inkludér alle domæner du selv holder styr på</span>
</div>
<div class="form-group">
<label>Kører du egne navneservere? <span style="color:#ef4444">*</span></label>
<div class="radio-group">
<label>
<input type="radio" name="egne_ns" value="ja" required>
Ja
</label>
<label>
<input type="radio" name="egne_ns" value="nej">
Nej
</label>
</div>
</div>
<div class="form-group">
<label for="kommentar">Kommentarer eller spørgsmål <span style="color:#475569; font-weight:400">(valgfrit)</span></label>
<textarea id="kommentar" name="kommentar" placeholder="Har du tanker om modellen, bekymringer, eller noget vi bør tænke over?"></textarea>
</div>
<button type="submit" class="submit-btn">Ja, jeg er interesseret</button>
</form>
</div>
</section>
{% endblock %}

21
templates/tak.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Tak — PunktFri{% endblock %}
{% block content %}
<section class="tak-section">
<div class="tak-card">
<div class="tak-icon"></div>
<h1>Tak for din tilmelding</h1>
<p>Vi har noteret din interesse. Ingen forpligtelse, ingen faktura, intet abonnement.</p>
<p>
Vi vender tilbage med en enkelt opdatering, når vi ved om der er tilstrækkelig
interesse til at gå videre med idéen.
</p>
<p style="font-size: 0.8rem; color: #9ca3af;">
Spørgsmål? Skriv til <a href="mailto:info@punktfri.dk" style="color: #6366f1;">info@punktfri.dk</a>
</p>
<a href="/" class="back-link">← Tilbage til forsiden</a>
</div>
</section>
{% endblock %}