Initial commit: Astro scaffold, review template, voorstelformulier, screenshot/AI-automatisering

This commit is contained in:
2026-07-24 08:54:47 +02:00
commit 0913c99b9b
23 changed files with 6027 additions and 0 deletions

View File

View File

@@ -0,0 +1,64 @@
---
interface Props {
hue: "teal" | "amber" | "rust" | "slate" | "olive";
logo?: string;
company: string;
size?: "thumb" | "hero";
}
const { hue, logo, company, size = "thumb" } = Astro.props;
---
<div class={`cover ${size} hue-${hue}`}>
{
logo ? (
<img src={logo} alt={`${company} logo`} class="logo" />
) : (
<svg class="mark" viewBox="0 0 24 24" aria-hidden="true">
<use href="#robot-icon" />
</svg>
)
}
</div>
<style>
.cover {
--ink-c: var(--hue-teal-ink);
--wash-c: var(--hue-teal-wash);
position: relative;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
background:
radial-gradient(var(--ink-c) 1px, transparent 1.6px) 0 0 / 12px 12px,
var(--wash-c);
overflow: hidden;
flex-shrink: 0;
}
.cover.hue-amber { --ink-c: var(--hue-amber-ink); --wash-c: var(--hue-amber-wash); }
.cover.hue-rust { --ink-c: var(--hue-rust-ink); --wash-c: var(--hue-rust-wash); }
.cover.hue-slate { --ink-c: var(--hue-slate-ink); --wash-c: var(--hue-slate-wash); }
.cover.hue-olive { --ink-c: var(--hue-olive-ink); --wash-c: var(--hue-olive-wash); }
.cover .mark {
position: absolute;
fill: var(--ink-c);
opacity: 0.5;
}
.cover .logo {
position: relative;
max-width: 60%;
max-height: 60%;
object-fit: contain;
}
.cover.thumb { width: 88px; height: 88px; }
.cover.thumb .mark { width: 52px; height: 52px; right: -8px; bottom: -8px; }
.cover.hero { width: 100%; height: 168px; margin-bottom: 26px; }
.cover.hero .mark { width: 110px; height: 110px; right: 18px; bottom: -20px; }
@media (max-width: 620px) {
.cover.thumb { width: 64px; height: 64px; }
.cover.thumb .mark { width: 38px; height: 38px; }
}
</style>

View File

@@ -0,0 +1,41 @@
---
interface Props {
score?: number;
size?: "md" | "lg";
showNumber?: boolean;
}
// score is optioneel voor een nog-niet-gepubliceerd concept (draft: true) —
// dan tonen we gewoon 0/5 in plaats van te crashen.
const { score = 0, size = "md", showNumber = true } = Astro.props;
const cells = Array.from({ length: 5 }, (_, i) => i < score);
---
<span class={`robots ${size === "lg" ? "lg" : ""}`} aria-label={`${score} van de 5 robots`}>
{
cells.map((filled) => (
<svg class={filled ? "robot-fill" : "robot-off"} aria-hidden="true">
<use href="#robot-icon" />
</svg>
))
}
{showNumber && <span class="score-num tabnum">{score}/5</span>}
</span>
<style>
.robots {
display: inline-flex;
align-items: center;
gap: 3px;
vertical-align: middle;
}
.robots svg { width: 17px; height: 17px; display: block; }
.robots.lg svg { width: 24px; height: 24px; }
.robot-fill { fill: var(--accent); }
.robot-off { fill: var(--robot-off); }
.score-num {
font-family: var(--mono);
font-size: 13px;
color: var(--muted);
margin-left: 4px;
}
</style>

54
src/content.config.ts Normal file
View File

@@ -0,0 +1,54 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const reviews = defineCollection({
loader: glob({
pattern: ["*.md", "!_template.md"],
base: "./src/content/reviews",
}),
schema: z
.object({
title: z.string(),
company: z.string(),
website: z.string().url(),
location: z.string(),
category: z.string(),
date: z.coerce.date().optional(),
draft: z.boolean().default(true),
for_who: z.string(),
price_from: z.string(),
hosting: z.string(),
// Optioneel zolang draft: true — een concept mag nog geen score/oordeel
// hebben. Zodra draft: false wordt gezet, dwingt de refine hieronder af
// dat beide echt ingevuld zijn (voorkomt een leeg/0-sterren oordeel dat
// per ongeluk gepubliceerd wordt).
score: z.number().int().min(1).max(5).optional(),
verdict: z.string().optional().default(""),
logo: z.string().optional().default(""),
coverHue: z
.enum(["teal", "amber", "rust", "slate", "olive"])
.default("teal"),
})
.superRefine((data, ctx) => {
if (data.draft) return;
if (data.score === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["score"],
message: "score (1-5) is verplicht zodra draft: false staat",
});
}
if (!data.verdict) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["verdict"],
message: "verdict mag niet leeg zijn zodra draft: false staat",
});
}
}),
});
export const collections = { reviews };

View File

@@ -0,0 +1,45 @@
---
# Kopieer dit bestand naar <slug>.md (bv. watermelon.md) en vul in.
# Bestandsnaam = URL-slug, dus alleen kleine letters/cijfers/koppeltekens.
title: "" # bv. "Watermelon: AI-klantenservice die je zelf binnen een uur opzet"
company: "" # bv. "Watermelon"
website: "" # bv. "https://watermelon.ai"
location: "" # bv. "Utrecht"
category: "" # bv. "Klantenservice" — 1 hoofdcategorie, wordt ook gebruikt voor de coverkleur
date: "" # publicatiedatum, bv. 2026-07-23 — leeg laten zolang draft: true
draft: true # false zetten bij publiceren (bepaalt "gepubliceerd"/"binnenkort" op de homepage)
# --- Specs-balkje bovenaan de review ---
for_who: "" # korte typering, bv. "MKB met veel herhalende vragen"
price_from: "" # bv. "€39 /maand" — "Gratis" of "Op aanvraag" mag ook
hosting: "" # bv. "EU, GDPR-verklaring"
# --- Eindoordeel ---
score: 0 # geheel getal 1-5 (aantal robots)
verdict: "" # één zin, komt in het verdict-kader bovenaan
# --- Cover ---
logo: "" # bv. "/logos/watermelon.svg" — bestand in public/logos/ zetten, anders leeg laten
coverHue: "teal" # teal | amber | rust | slate | olive — alleen gebruikt zolang er geen logo is
---
## Wat is het
<!-- Wat doet de tool, in gewone-mensen-taal. Eén tot twee alinea's. -->
## Voor wie
<!-- Voor welk type gebruiker/bedrijf is dit relevant, en voor wie juist niet. -->
## Wie zit erachter
<!-- Team, locatie, funding/omzet indien bekend. Onbekend? Dat gewoon zo benoemen. -->
## Prijs
<!-- Prijsmodel iets uitgebreider dan het specs-balkje: tiers, gratis laag, addertjes onder het gras. -->
## Hands-on indruk
<!-- De eigen ervaring: wat werkte goed, wat viel tegen. Dit is de kern van de review. -->

View File

@@ -0,0 +1,39 @@
---
title: "Watermelon: AI-klantenservice die je zelf binnen een uur opzet"
company: "Watermelon"
website: "https://watermelon.ai"
location: "Utrecht"
category: "Klantenservice"
date: 2026-07-23
draft: false
for_who: "MKB met veel herhalende vragen"
price_from: "€39 /maand"
hosting: "EU, GDPR-verklaring"
score: 4
verdict: "Sterk, zelfstandig te testen product met een geloofwaardig GDPR-verhaal — mist alleen wat diepgang in de rapportages."
logo: ""
coverHue: "teal"
---
## Wat is het
Watermelon is een AI-klantenservice-agent die vragen via chat, WhatsApp, e-mail en telefoon beantwoordt, getraind op de kennisbank van het eigen bedrijf. Geen los gesprek met sales nodig: je maakt een account, wijst een paar pagina's of documenten aan, en de bot beantwoordt daarna zelfstandig binnenkomende vragen.
## Voor wie
Vooral relevant voor MKB-bedrijven met een voorspelbare stroom herhalende vragen (verzendstatus, openingstijden, retourbeleid) die nu telefoon of mailbox belasten.
## Wie zit erachter
Opgericht in Utrecht. GDPR/EU-hosted, ISO-gecertificeerd — verdere teamdetails nog na te trekken voor publicatie.
## Prijs
Instapplan vanaf €39 per maand, oplopend met volume en kanalen. Geen gratis laag, wel een proefperiode zonder creditcard.
## Hands-on indruk
<!-- TODO: eigen testervaring invullen zodra getest -->

188
src/layouts/Base.astro Normal file
View File

@@ -0,0 +1,188 @@
---
interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
const path = Astro.url.pathname;
const isHome = path === "/";
const isVoorstellen = path.startsWith("/voorstellen");
---
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} · AITestBank</title>
<meta name="description" content={description} />
<link rel="icon" href="data:," />
</head>
<body>
<svg width="0" height="0" style="position:absolute">
<symbol id="robot-icon" viewBox="0 0 24 24">
<rect x="4" y="7" width="16" height="14" rx="3"></rect>
<rect x="10" y="2" width="4" height="4" rx="1"></rect>
<circle class="robot-eye" cx="9" cy="14" r="1.6"></circle>
<circle class="robot-eye" cx="15" cy="14" r="1.6"></circle>
<rect class="robot-eye" x="9" y="17.5" width="6" height="1.6" rx="0.8"></rect>
</symbol>
</svg>
<div class="shell">
<header class="site">
<a class="wordmark" href="/">
<span class="name">AITestBank</span>
<span class="tag">aitestbank.nl</span>
</a>
<nav class="site">
<a href="/" aria-current={isHome ? "page" : undefined}>Reviews</a>
<a href="/voorstellen" aria-current={isVoorstellen ? "page" : undefined}>Tool voorstellen</a>
</nav>
</header>
<slot />
</div>
</body>
</html>
<style is:global>
:root {
--paper: #faf9f6;
--paper-raised: #f2f0ea;
--ink: #1c1b19;
--muted: #6b6a64;
--line: #dedcd5;
--accent: #3c6e63;
--accent-soft: #e4ece9;
--robot-off: #d6d4cc;
--radius: 4px;
--sans: ui-sans-serif, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Consolas, monospace;
--hue-teal-ink: #3c6e63;
--hue-teal-wash: #e4ece9;
--hue-amber-ink: #9c6b23;
--hue-amber-wash: #f3e8d3;
--hue-rust-ink: #a85039;
--hue-rust-wash: #f5e0da;
--hue-slate-ink: #3f5a7d;
--hue-slate-wash: #dfe6ef;
--hue-olive-ink: #6b7a3a;
--hue-olive-wash: #e9edd9;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #17181a;
--paper-raised: #1e2022;
--ink: #eae8e2;
--muted: #94928a;
--line: #2b2d2d;
--accent: #74ab9d;
--accent-soft: #1c2926;
--robot-off: #35373a;
--hue-teal-ink: #74ab9d;
--hue-teal-wash: #1c2926;
--hue-amber-ink: #d9a75c;
--hue-amber-wash: #2e2617;
--hue-rust-ink: #d98267;
--hue-rust-wash: #33211c;
--hue-slate-ink: #7fa0c4;
--hue-slate-wash: #1c2530;
--hue-olive-ink: #a3b56a;
--hue-olive-wash: #262c18;
}
}
:root[data-theme="dark"] {
--paper: #17181a;
--paper-raised: #1e2022;
--ink: #eae8e2;
--muted: #94928a;
--line: #2b2d2d;
--accent: #74ab9d;
--accent-soft: #1c2926;
--robot-off: #35373a;
--hue-teal-ink: #74ab9d;
--hue-teal-wash: #1c2926;
--hue-amber-ink: #d9a75c;
--hue-amber-wash: #2e2617;
--hue-rust-ink: #d98267;
--hue-rust-wash: #33211c;
--hue-slate-ink: #7fa0c4;
--hue-slate-wash: #1c2530;
--hue-olive-ink: #a3b56a;
--hue-olive-wash: #262c18;
}
:root[data-theme="light"] {
--paper: #faf9f6;
--paper-raised: #f2f0ea;
--ink: #1c1b19;
--muted: #6b6a64;
--line: #dedcd5;
--accent: #3c6e63;
--accent-soft: #e4ece9;
--robot-off: #d6d4cc;
--hue-teal-ink: #3c6e63;
--hue-teal-wash: #e4ece9;
--hue-amber-ink: #9c6b23;
--hue-amber-wash: #f3e8d3;
--hue-rust-ink: #a85039;
--hue-rust-wash: #f5e0da;
--hue-slate-ink: #3f5a7d;
--hue-slate-wash: #dfe6ef;
--hue-olive-ink: #6b7a3a;
--hue-olive-wash: #e9edd9;
}
* { box-sizing: border-box; }
html,
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
font-size: 18px;
line-height: 1.6;
}
a {
color: var(--accent);
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
a:focus-visible,
button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.tabnum { font-variant-numeric: tabular-nums; }
.shell { max-width: 820px; margin: 0 auto; padding: 0 24px 100px; }
header.site {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
padding: 30px 0 24px;
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.wordmark {
display: flex;
align-items: baseline;
gap: 10px;
text-decoration: none;
color: var(--ink);
}
.wordmark .name { font-size: 21px; font-weight: 700; letter-spacing: -0.01em; }
.wordmark .tag { font-family: var(--mono); font-size: 13px; color: var(--muted); }
nav.site { display: flex; gap: 22px; }
nav.site a { color: var(--muted); text-decoration: none; font-size: 15px; }
nav.site a:hover,
nav.site a[aria-current="page"] { color: var(--ink); }
/* shared by RobotScore and Cover: the eyes of the #robot-icon symbol
always need to punch through to the page background */
.robot-eye { fill: var(--paper); }
</style>

17
src/pages/bedankt.astro Normal file
View File

@@ -0,0 +1,17 @@
---
import Base from "../layouts/Base.astro";
---
<Base title="Bedankt" description="Je tool-suggestie is ontvangen.">
<section class="intro">
<h1>Bedankt!</h1>
<p>Je suggestie is binnen. Ik lees alles, maar kan niet beloven dat elke tool een review krijgt.</p>
<p><a href="/">← Terug naar de reviews</a></p>
</section>
</Base>
<style>
section.intro { padding: 44px 0 10px; margin-bottom: 8px; }
section.intro h1 { font-size: 34px; margin: 0 0 12px; text-wrap: balance; letter-spacing: -0.01em; }
section.intro p { margin: 0 0 16px; color: var(--muted); max-width: 58ch; font-size: 18px; }
</style>

138
src/pages/index.astro Normal file
View File

@@ -0,0 +1,138 @@
---
import { getCollection } from "astro:content";
import Base from "../layouts/Base.astro";
import Cover from "../components/Cover.astro";
import RobotScore from "../components/RobotScore.astro";
const all = await getCollection("reviews");
const published = all
.filter((r) => !r.data.draft)
.sort((a, b) => (b.data.date?.valueOf() ?? 0) - (a.data.date?.valueOf() ?? 0));
const upcoming = all.filter((r) => r.data.draft);
const posts = [...published, ...upcoming];
const dateFormatter = new Intl.DateTimeFormat("nl-NL", {
day: "numeric",
month: "short",
year: "numeric",
});
---
<Base
title="Reviews"
description="Wekelijkse, eerlijke tests van Nederlandse AI-tools. Score van 1 tot 5 robots."
>
<section class="intro">
<h1>Nederlandse AI-tools, eerlijk getest.</h1>
<p>
Elke week (of vaker) een AI-tool onder de loep: hoe het werkt, voor wie
het is, wat het kost. Score van 1 tot 5 robots, geen sponsored fluff.
</p>
</section>
<ul class="postlist">
{
posts.map((post) => (
<li class="post-row">
<Cover
hue={post.data.coverHue}
logo={post.data.logo}
company={post.data.company}
size="thumb"
/>
<div class="meta">
<span class="tabnum">
{post.data.draft ? "—" : dateFormatter.format(post.data.date)}
</span>
<span>·</span>
<span>{post.data.category}</span>
<span class={`status ${post.data.draft ? "pending" : ""}`}>
{post.data.draft ? "binnenkort" : "gepubliceerd"}
</span>
</div>
<h2>
<a href={post.data.draft ? undefined : `/reviews/${post.id}`}>
{post.data.title}
</a>
</h2>
<p class="dek">
{post.data.location} · {post.data.verdict}
</p>
<div class="score">
{!post.data.draft && <RobotScore score={post.data.score} />}
</div>
</li>
))
}
</ul>
</Base>
<style>
section.intro {
padding: 44px 0 10px;
border-bottom: 1px solid var(--line);
margin-bottom: 8px;
}
section.intro h1 {
font-size: 34px;
margin: 0 0 12px;
text-wrap: balance;
letter-spacing: -0.01em;
}
section.intro p {
margin: 0 0 30px;
color: var(--muted);
max-width: 58ch;
font-size: 18px;
}
ul.postlist { list-style: none; margin: 0; padding: 0; }
.post-row {
display: grid;
grid-template-columns: 88px 1fr auto;
gap: 6px 20px;
padding: 28px 0;
border-bottom: 1px solid var(--line);
align-items: start;
}
.post-row :global(.cover) { grid-row: 1 / 3; }
.post-row .meta {
grid-column: 2 / 3;
font-family: var(--mono);
font-size: 13px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
display: flex;
gap: 10px;
align-items: center;
}
.post-row .meta .status {
padding: 1px 7px;
border-radius: var(--radius);
background: var(--accent-soft);
color: var(--accent);
text-transform: none;
letter-spacing: 0;
}
.post-row .meta .status.pending {
background: transparent;
border: 1px solid var(--line);
color: var(--muted);
}
.post-row h2 { grid-column: 2 / 3; margin: 0; font-size: 20px; line-height: 1.35; }
.post-row h2 a { color: var(--ink); text-decoration: none; }
.post-row h2 a:hover { color: var(--accent); }
.post-row p.dek {
grid-column: 2 / 3;
margin: 4px 0 0;
color: var(--muted);
font-size: 15.5px;
max-width: 54ch;
}
.post-row .score { grid-column: 3 / 4; grid-row: 1 / 2; justify-self: end; white-space: nowrap; }
@media (max-width: 620px) {
.post-row { grid-template-columns: 64px 1fr auto; }
}
</style>

View File

@@ -0,0 +1,157 @@
---
import { getCollection, render } from "astro:content";
import Base from "../../layouts/Base.astro";
import Cover from "../../components/Cover.astro";
import RobotScore from "../../components/RobotScore.astro";
export async function getStaticPaths() {
const all = await getCollection("reviews");
return all.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
const d = entry.data;
const dateFormatter = new Intl.DateTimeFormat("nl-NL", {
day: "numeric",
month: "long",
year: "numeric",
});
---
<Base title={d.title} description={d.verdict}>
<article class="review">
<p class="breadcrumb"><a href="/">← Alle reviews</a></p>
<Cover hue={d.coverHue} logo={d.logo} company={d.company} size="hero" />
<h1>{d.title}</h1>
<div class="review-meta">
{d.date && <span class="tabnum">{dateFormatter.format(d.date)}</span>}
{d.date && <span>·</span>}
<span>{d.category}</span>
<span>·</span>
<span>{d.location}</span>
<span>·</span>
<a href={d.website} target="_blank" rel="noopener">{d.company} →</a>
</div>
<div class="verdict">
<div>
<div class="label">Eindoordeel</div>
<p class="oneliner">{d.verdict}</p>
</div>
<div class="score-block">
<RobotScore score={d.score} size="lg" showNumber={false} />
<span class="score-num tabnum">{d.score} / 5</span>
</div>
</div>
<dl class="specs">
<div>
<dt>Voor wie</dt>
<dd>{d.for_who}</dd>
</div>
<div>
<dt>Vanaf</dt>
<dd class="tabnum">{d.price_from}</dd>
</div>
<div>
<dt>Hosting</dt>
<dd>{d.hosting}</dd>
</div>
</dl>
<div class="body">
<Content />
</div>
</article>
</Base>
<style>
.breadcrumb {
font-family: var(--mono);
font-size: 13px;
color: var(--muted);
margin: 30px 0 20px;
}
.breadcrumb a { color: var(--muted); }
article.review h1 {
font-size: 40px;
margin: 0 0 10px;
text-wrap: balance;
letter-spacing: -0.01em;
}
.review-meta {
font-family: var(--mono);
font-size: 13.5px;
color: var(--muted);
margin-bottom: 22px;
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.verdict {
background: var(--paper-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 22px 24px;
display: grid;
grid-template-columns: 1fr auto;
gap: 12px 24px;
align-items: center;
margin-bottom: 36px;
}
.verdict .label {
font-family: var(--mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 6px;
}
.verdict .oneliner { margin: 0; font-size: 17px; max-width: 46ch; }
.verdict .score-block { text-align: right; }
.verdict .score-block .score-num {
font-family: var(--mono);
font-size: 16px;
color: var(--muted);
display: block;
margin: 6px 0 0;
}
.specs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin: 0 0 36px;
padding: 18px 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
.specs dt {
font-family: var(--mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
margin: 0 0 4px;
}
.specs dd { margin: 0; font-size: 16px; }
.body { max-width: 64ch; }
.body :global(h2) { font-size: 22px; margin: 42px 0 12px; letter-spacing: -0.005em; }
.body :global(p) { margin: 0 0 17px; }
@media (max-width: 620px) {
.specs { grid-template-columns: 1fr 1fr; }
.verdict { grid-template-columns: 1fr; }
.verdict .score-block { text-align: left; }
}
</style>

130
src/pages/voorstellen.astro Normal file
View File

@@ -0,0 +1,130 @@
---
import Base from "../layouts/Base.astro";
---
<Base
title="Tool voorstellen"
description="Stel een Nederlandse AI-tool voor om te reviewen op AITestBank."
>
<section class="intro">
<h1>Stel een tool voor</h1>
<p>
Ken je een (bij voorkeur Nederlandse) AI-tool die een review verdient?
Laat het weten, ik lees alles.
</p>
</section>
<form method="post" action="/submit-tool" class="suggest-form">
<div class="field">
<label for="tool_naam">Naam van de tool <span class="req">*</span></label>
<input type="text" id="tool_naam" name="tool_naam" required maxlength="120" />
</div>
<div class="field">
<label for="tool_url">Website van de tool <span class="req">*</span></label>
<input type="url" id="tool_url" name="tool_url" required placeholder="https://" maxlength="300" />
</div>
<div class="field">
<label for="toelichting">Waarom deze tool?</label>
<textarea id="toelichting" name="toelichting" rows="4" maxlength="2000"></textarea>
</div>
<div class="field-row">
<div class="field">
<label for="naam">Je naam <span class="opt">(optioneel)</span></label>
<input type="text" id="naam" name="naam" maxlength="120" />
</div>
<div class="field">
<label for="email">Je e-mail <span class="opt">(optioneel, voor een reactie)</span></label>
<input type="email" id="email" name="email" maxlength="200" />
</div>
</div>
<!-- honeypot: verborgen voor mensen via CSS, bots vullen dit vaak automatisch in -->
<div class="hp" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
</div>
<button type="submit">Versturen</button>
</form>
</Base>
<style>
section.intro {
padding: 44px 0 10px;
border-bottom: 1px solid var(--line);
margin-bottom: 32px;
}
section.intro h1 {
font-size: 34px;
margin: 0 0 12px;
text-wrap: balance;
letter-spacing: -0.01em;
}
section.intro p {
margin: 0 0 30px;
color: var(--muted);
max-width: 58ch;
font-size: 18px;
}
.suggest-form {
max-width: 56ch;
display: flex;
flex-direction: column;
gap: 22px;
margin-bottom: 80px;
}
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 22px;
}
.field { display: flex; flex-direction: column; gap: 6px; }
label {
font-family: var(--mono);
font-size: 12.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.req { color: var(--accent); }
.opt { text-transform: none; letter-spacing: 0; }
input, textarea {
font: inherit;
font-size: 16px;
color: var(--ink);
background: var(--paper-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 10px 12px;
}
textarea { resize: vertical; }
input:focus-visible, textarea:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
button {
align-self: start;
font: inherit;
font-size: 15px;
font-weight: 600;
color: var(--paper);
background: var(--accent);
border: none;
border-radius: var(--radius);
padding: 12px 22px;
cursor: pointer;
}
button:hover { opacity: 0.9; }
@media (max-width: 560px) {
.field-row { grid-template-columns: 1fr; }
}
</style>