From 448a166e3ad195b3eab9012c1576dd047471dc8a Mon Sep 17 00:00:00 2001 From: Mohamed Ibrahim Date: Sun, 19 Apr 2026 00:00:56 +0200 Subject: [PATCH] feat(site): redesign Renvoo as bilingual funnel --- README.md | 3 + config/generate-site-pages.mjs | 17 + config/site-postbuild.mjs | 41 +- config/vite.site.config.mjs | 22 +- docs/website/deployment.md | 10 + package.json | 7 +- src/site/404.html | 138 ++- src/site/en/404.html | 93 ++ src/site/en/index.html | 202 ++++ src/site/en/patient-notice.html | 116 +++ src/site/en/pilot.html | 306 ++++++ src/site/en/privacy.html | 120 +++ src/site/en/product.html | 210 ++++ src/site/en/trust.html | 226 +++++ src/site/index.html | 774 ++++----------- src/site/lib/booking.d.ts | 59 ++ src/site/lib/booking.js | 159 +++ src/site/lib/site-content.js | 1376 ++++++++++++++++++++++++++ src/site/lib/site-render.js | 834 ++++++++++++++++ src/site/main.js | 462 ++++++++- src/site/patient-notice.html | 218 ++--- src/site/pilot.html | 306 ++++++ src/site/privacy.html | 232 +++-- src/site/product.html | 210 ++++ src/site/styles.css | 1632 ++++++++++++++++++------------- src/site/trust.html | 226 +++++ tests/site-booking.test.ts | 100 ++ 27 files changed, 6498 insertions(+), 1601 deletions(-) create mode 100644 config/generate-site-pages.mjs create mode 100644 src/site/en/404.html create mode 100644 src/site/en/index.html create mode 100644 src/site/en/patient-notice.html create mode 100644 src/site/en/pilot.html create mode 100644 src/site/en/privacy.html create mode 100644 src/site/en/product.html create mode 100644 src/site/en/trust.html create mode 100644 src/site/lib/booking.d.ts create mode 100644 src/site/lib/booking.js create mode 100644 src/site/lib/site-content.js create mode 100644 src/site/lib/site-render.js create mode 100644 src/site/pilot.html create mode 100644 src/site/product.html create mode 100644 src/site/trust.html create mode 100644 tests/site-booking.test.ts diff --git a/README.md b/README.md index f267edc..33b41dc 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ memory/ The repo now also includes a clinic-facing marketing site under `src/site/`. +- Regenerate the static page set from the shared bilingual content source: + - `npm run site:generate` - Start local website dev: - `npm run site:dev` - Build the static website: @@ -69,6 +71,7 @@ The repo now also includes a clinic-facing marketing site under `src/site/`. - `npm run site:preview` Website build output goes to `dist/site/`. +The public funnel is Dutch-first at the root and mirrored in English under `src/site/en/`. If you set `SITE_URL=https://your-domain.example` when running `npm run site:build`, the build also generates `sitemap.xml` and a `robots.txt` file that includes the sitemap location. Deployment notes live in `docs/website/deployment.md`. diff --git a/config/generate-site-pages.mjs b/config/generate-site-pages.mjs new file mode 100644 index 0000000..8b5eac8 --- /dev/null +++ b/config/generate-site-pages.mjs @@ -0,0 +1,17 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { renderAllPages } from "../src/site/lib/site-render.js"; + +const configDir = dirname(fileURLToPath(import.meta.url)); +const projectRoot = resolve(configDir, ".."); +const siteRoot = resolve(projectRoot, "src/site"); + +const pages = renderAllPages(); + +for (const page of pages) { + const outputPath = resolve(siteRoot, page.path); + await mkdir(dirname(outputPath), { recursive: true }); + await writeFile(outputPath, page.html, "utf8"); +} diff --git a/config/site-postbuild.mjs b/config/site-postbuild.mjs index 1259b01..2c0138e 100644 --- a/config/site-postbuild.mjs +++ b/config/site-postbuild.mjs @@ -2,17 +2,31 @@ import { readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import fg from "fast-glob"; + const configDir = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(configDir, ".."); const distDir = resolve(projectRoot, "dist/site"); const rawSiteUrl = process.env.SITE_URL?.trim(); const siteUrl = rawSiteUrl ? rawSiteUrl.replace(/\/+$/, "") : ""; -const socialImageUrl = siteUrl ? `${siteUrl}/social-preview.png` : "./social-preview.png"; - const robotsLines = ["User-agent: *", "Allow: /"]; -const htmlFiles = ["index.html", "privacy.html", "patient-notice.html", "404.html"]; -const sitemapPaths = ["/", "/privacy.html", "/patient-notice.html"]; + +const htmlFiles = fg.sync(["**/*.html"], { + cwd: distDir, + onlyFiles: true, +}); + +const sitemapPaths = htmlFiles + .filter((file) => !file.endsWith("404.html")) + .map((file) => { + const normalized = file.replace(/\\/g, "/"); + return normalized === "index.html" + ? "/" + : normalized.endsWith("/index.html") + ? `/${normalized.slice(0, -"index.html".length)}` + : `/${normalized}`; + }); if (siteUrl) { robotsLines.push("", `Sitemap: ${siteUrl}/sitemap.xml`); @@ -35,16 +49,27 @@ if (siteUrl) { for (const htmlFile of htmlFiles) { const filePath = resolve(distDir, htmlFile); let html = await readFile(filePath, "utf8"); + const normalized = htmlFile.replace(/\\/g, "/"); + const pagePath = + normalized === "index.html" + ? "/" + : normalized.endsWith("/index.html") + ? `/${normalized.slice(0, -"index.html".length)}` + : `/${normalized}`; + const depth = normalized.split("/").length - 1; + const socialImageRelative = `${depth ? "../".repeat(depth) : "./"}social-preview.png`; + const canonicalUrl = siteUrl ? `${siteUrl}${pagePath}` : ""; + const socialImageUrl = siteUrl ? `${siteUrl}/social-preview.png` : socialImageRelative; if (siteUrl) { html = html - .replaceAll("__SITE_URL__", siteUrl) + .replaceAll("__CANONICAL_URL__", canonicalUrl) .replaceAll("__SOCIAL_IMAGE__", socialImageUrl); } else { html = html - .replace(/^\s*\n?/m, "") - .replace(/^\s*\n?/m, "") - .replaceAll("__SOCIAL_IMAGE__", "./social-preview.png"); + .replace(/^\s*\n?/m, "") + .replace(/^\s*\n?/m, "") + .replaceAll("__SOCIAL_IMAGE__", socialImageRelative); } await writeFile(filePath, html, "utf8"); diff --git a/config/vite.site.config.mjs b/config/vite.site.config.mjs index 100cc5a..e152586 100644 --- a/config/vite.site.config.mjs +++ b/config/vite.site.config.mjs @@ -1,25 +1,31 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import fg from "fast-glob"; import { defineConfig } from "vite"; const configDir = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(configDir, ".."); +const siteRoot = resolve(projectRoot, "src/site"); + +const htmlEntries = fg.sync(["src/site/**/*.html"], { + cwd: projectRoot, + onlyFiles: true, +}); + +const input = Object.fromEntries( + htmlEntries.map((entry) => [entry.replace(/^src\/site\//, "").replaceAll("/", "__"), resolve(projectRoot, entry)]), +); export default defineConfig({ base: "./", - root: resolve(projectRoot, "src/site"), - publicDir: resolve(projectRoot, "src/site/public"), + root: siteRoot, + publicDir: resolve(siteRoot, "public"), build: { emptyOutDir: true, outDir: resolve(projectRoot, "dist/site"), rollupOptions: { - input: { - main: resolve(projectRoot, "src/site/index.html"), - privacy: resolve(projectRoot, "src/site/privacy.html"), - patientNotice: resolve(projectRoot, "src/site/patient-notice.html"), - notFound: resolve(projectRoot, "src/site/404.html"), - }, + input, }, }, }); diff --git a/docs/website/deployment.md b/docs/website/deployment.md index b280775..eb3d133 100644 --- a/docs/website/deployment.md +++ b/docs/website/deployment.md @@ -1,6 +1,7 @@ # Renvoo Website Deployment The clinic website is a static Vite build. +The root site is Dutch-first, with mirrored English routes under `/en/`. ## Build @@ -10,11 +11,19 @@ SITE_URL=https://your-domain.example npm run site:build ``` The static output goes to `dist/site/`. +`npm run site:build` regenerates the bilingual HTML pages before Vite builds them. ## What the build includes - `index.html` +- `product.html` +- `pilot.html` +- `trust.html` - `404.html` +- `en/index.html` +- `en/product.html` +- `en/pilot.html` +- `en/trust.html` - `robots.txt` - `site.webmanifest` - `sitemap.xml` when `SITE_URL` is set @@ -46,6 +55,7 @@ This repo now includes: ## Verification Checklist - Homepage loads without broken assets +- Product, pilot, and trust pages load in Dutch and English - `downloads/renvoo-clinic-one-pager.pdf` opens - `downloads/renvoo-clinic-deck.pptx` downloads - `404.html` renders correctly diff --git a/package.json b/package.json index adb11a6..110da9f 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "scripts": { "build": "tsc -p tsconfig.json", "dev": "tsx src/cli.ts", - "site:dev": "vite --config config/vite.site.config.mjs", - "site:build": "vite build --config config/vite.site.config.mjs && node config/site-postbuild.mjs", - "site:preview": "vite preview --config config/vite.site.config.mjs", + "site:generate": "node config/generate-site-pages.mjs", + "site:dev": "npm run site:generate && vite --config config/vite.site.config.mjs", + "site:build": "npm run site:generate && vite build --config config/vite.site.config.mjs && node config/site-postbuild.mjs", + "site:preview": "npm run site:generate && vite preview --config config/vite.site.config.mjs", "typecheck": "tsc --noEmit -p tsconfig.json", "test": "vitest run", "memory:init": "npm run build && node dist/src/cli.js init", diff --git a/src/site/404.html b/src/site/404.html index 709cde7..ccb81af 100644 --- a/src/site/404.html +++ b/src/site/404.html @@ -1,63 +1,93 @@ - + - - Renvoo | Page not found - - - - - - + + Renvoo | Pagina niet gevonden + + + + + + + + + + + + + + + + + - -
-
- -
-
- -

Page not found

-

The page you were looking for is not here.

-

- The safest next step is to go back to the clinic site and continue from the core Renvoo - overview. -

-
- Return to the homepage - - Download the one-pager - + + + + + +
+ +
+
+
+

404

+

Deze pagina bestaat niet meer.

+

Ga terug naar de funnel en kies opnieuw of u eerst product, vertrouwen of direct het validatiegesprek wilt bekijken.

+
+
+
+ + diff --git a/src/site/en/404.html b/src/site/en/404.html new file mode 100644 index 0000000..c461631 --- /dev/null +++ b/src/site/en/404.html @@ -0,0 +1,93 @@ + + + + + + Renvoo | Page not found + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+

404

+

This page no longer exists.

+

Go back into the funnel and choose whether you want to review the product, the trust layer, or the meeting planner next.

+ +
+
+
+ +
+
+ + +
+ + + diff --git a/src/site/en/index.html b/src/site/en/index.html new file mode 100644 index 0000000..738f476 --- /dev/null +++ b/src/site/en/index.html @@ -0,0 +1,202 @@ + + + + + + Renvoo | Reduce no-shows. Recover lost appointments. + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+

For Dutch dental clinics

+

Reduce no-shows. Recover lost appointments.

+

Renvoo helps Dutch dental clinics spot no-show risk earlier, handle late cancellations sooner, and recover empty chair time without adding front-desk work.

+ +
    +
  • No patient accounts in v1
  • Administrative-data boundary
  • Pilot-first, not platform-first
  • +
+
+ +
+
+ +
+
+

The real problem

+

The damage is not only the no-show itself.

+

Loss compounds when empty chair time, late signals, and recovery work all hit the same day. That is why reminders alone are not enough.

+
+
+
+

Empty chair time becomes immediate revenue loss

+

A scheduled appointment only creates value if the patient actually shows up.

+
+

Late cancellations can still be expensive even when polite

+

A patient may warn the clinic, and the slot can still be too late to save.

+
+

Admin work grows around uncertainty

+

Confirmations, chasing replies, rescheduling, and backfill all create extra operational load.

+
+
+
+ +
+
+

How Renvoo thinks

+

From generic reminders to schedule resilience

+

The pilot is designed as a lightweight operations layer. First surface risk, then confirm or reschedule more intentionally, then recover capacity faster.

+
+
+
+ Connect +

Start with scheduling and communication data rather than clinical data.

+
+ Detect +

Surface higher-risk appointments earlier than a generic reminder flow does.

+
+ Confirm +

Give staff a clearer moment to confirm, reschedule, or intervene earlier.

+
+ Replace +

Help the clinic recover lost capacity while keeping schedule control inside the practice.

+
+
+
+ +
+
+

Why this feels credible

+

Built to validate with clinics, not to claim too much too early.

+ +
+
+
+

Proof posture

+

The current case is grounded in clear operating logic and scenario math. The pilot exists to prove real reduction, recovery, and admin relief with clinic workflows.

+
+

Data boundary

+

Renvoo is deliberately framed around non-clinical scheduling and communication data, not diagnoses, notes, or treatment decisions.

+
+
+
+ +
+
+

Choose your route

+

Not every buyer wants the same thing first.

+ +
+
+
+

Understand the product first

+

See the full Connect → Detect → Confirm → Replace workflow and why it is different from reminder software.

+ Go to Product +
+

See the pilot path first

+

Review the validation meeting, pilot framing, and local booking flow.

+ Go to Pilot +
+

Check trust and data posture first

+

Review controller/processor boundaries, FAQs, materials, and legal notes.

+ Go to Trust +
+
+
+ +
+
+
+

Next step

+

The next step is small and practical.

+

We are looking for a short validation meeting with a dental clinic owner or practice manager to review the current no-show and cancellation workflow.

+
+ +
+
+ +
+
+ + +
+ + + diff --git a/src/site/en/patient-notice.html b/src/site/en/patient-notice.html new file mode 100644 index 0000000..372b4ad --- /dev/null +++ b/src/site/en/patient-notice.html @@ -0,0 +1,116 @@ + + + + + + Renvoo | Patient message note + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ + +
+ + + diff --git a/src/site/en/pilot.html b/src/site/en/pilot.html new file mode 100644 index 0000000..285b895 --- /dev/null +++ b/src/site/en/pilot.html @@ -0,0 +1,306 @@ + + + + + + Renvoo | Request a validation meeting + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+

Pilot

+

Request a validation meeting

+

The first conversation is not a heavy product demo. It is a short operator review of the current no-show, confirmation, and recovery workflow to see whether a lightweight pilot is worth it.

+ +
    +
  • 15 minutes
  • Founder-led
  • No live calendar sync yet
  • +
+
+
+
+ +
+
+

What the first meeting should do

+

Practical enough for a real yes or no

+ +
+
+
+

Workflow review

+

Walk through how no-shows, late cancellations, and recovery currently get handled.

+
+

Signals and constraints

+

Review where the clinic gets visibility too late and what data or tooling already exists.

+
+

Pilot fit

+

Decide whether the operational pain is strong enough for a measurable pilot.

+
+
+
+ +
+
+
+

Local booking flow

+

Pick preferred time blocks in a lightweight Calendly-style flow

+

This planner currently works as a request flow backed by local availability blocks. It stores a clean handoff while live routing is still being connected.

+
+
    +
  • Name and business contact details
  • Short context about clinic and current workflow
  • Preferred meeting format and time blocks
  • +
+
+ +
+ +
+
+

Book Meeting

+

Request a validation meeting

+

Live routing is not connected yet. The planner therefore stores a clean local handoff for launch prep and demo use.

+
+
+
+
+
    +
  1. Step 1Context
  2. +
  3. Step 2Preferences
  4. +
  5. Step 3Review
  6. +
+

Add the clinic context first to continue.

+ +
+
+
+ + +

+
+
+ + +

+
+
+ + +

+
+
+ + +

+
+
+ + +

+
+
+ + +

+
+
+ + +

+
+
+
+ + +

Briefly describe the current system or process… For example Exquise with manual reminders and a separate backfill list.

+

+
+
+ + + + + +
+ + + +
+ +

Live routing is not connected yet. The planner therefore stores a clean local handoff for launch prep and demo use.

+
+ + +
+ +
+
+ +
+
+
+

Not ready to book yet?

+

Review the trust page first for the data boundary, proof posture, FAQ, materials, and legal notes before returning to the planner.

+
+ See Trust First +
+
+ +
+
+ + +
+ + + diff --git a/src/site/en/privacy.html b/src/site/en/privacy.html new file mode 100644 index 0000000..8ce65a2 --- /dev/null +++ b/src/site/en/privacy.html @@ -0,0 +1,120 @@ + + + + + + Renvoo | Website privacy note + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ + +
+ + + diff --git a/src/site/en/product.html b/src/site/en/product.html new file mode 100644 index 0000000..74e9469 --- /dev/null +++ b/src/site/en/product.html @@ -0,0 +1,210 @@ + + + + + + Renvoo | Product workflow for no-show prevention + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+

Product

+

Not another reminder tool.

+

Renvoo is meant to be an operations layer around attendance uncertainty. The point is not more messages. The point is earlier visibility, better intervention timing, and calmer schedule recovery.

+ +
+
+
+ +
+
+

Before and after

+

What changes operationally

+ +
+
+
+

Without Renvoo

+
    +
  • Most appointments pass through roughly the same reminder logic.
  • Risk often becomes obvious only after the slot is already fragile.
  • Front-desk teams react manually to gaps, no-response, and late notice.
  • Backfill relies on memory, lists, and time pressure.
  • +
+
+
+

With Renvoo

+
    +
  • Staff can see earlier which appointments need more attention.
  • Confirming and rescheduling can happen more intentionally, not only generically.
  • Recovery work becomes a clearer workflow instead of scattered firefighting.
  • The clinic keeps final control over schedule changes and patient touchpoints.
  • +
+
+
+
+ +
+
+

The four steps

+

Connect. Detect. Confirm. Replace.

+

The architecture is deliberately small for early pilots: lightweight intake, operational visibility, and no patient-platform behavior in v1.

+
+
+
+
+ 01 +

Connect

+
+

Start with scheduling, appointment, and communication fields that are operationally useful enough to surface risk.

+
    +
  • Scheduling and status fields first
  • No diagnoses or clinical notes required
  • Lower-friction path into pilot validation
  • +
+
+
+ 02 +

Detect

+
+

Surface higher-risk appointments earlier than a standard reminder program would.

+
    +
  • Appointment-level risk, not one generic flow
  • Operator-facing, not clinical
  • Built for explainable use in practice
  • +
+
+
+ 03 +

Confirm

+
+

Give the team a clearer moment to confirm, reschedule, or intervene before chair time is lost.

+
    +
  • Supports practical staff follow-up
  • Fits beside existing systems
  • Helps address no-response sooner
  • +
+
+
+ 04 +

Replace

+
+

When a slot opens up, the workflow helps the clinic think about recovery earlier while keeping actual schedule mutation under clinic or EHR control.

+
    +
  • No autonomous schedule mutations in v1
  • Recovery over theater
  • Practice or EHR keeps final control
  • +
+
+
+
+ +
+
+

Low friction by design

+

Deliberately simple for the first clinic pilots

+ +
+
+
+

Non-clinical data boundary

+

Administrative scheduling and communication data comes first. That keeps the scope narrow and operational.

+
+

CSV-first or read-only intake

+

The first step does not need to become a heavy integration project before the pilot case is clear.

+
+

No patient accounts in v1

+

Patient interaction stays in clinic-triggered messages and secure links, not in a new consumer product.

+
+
+
+ +
+
+

Why dental first

+

The wedge is narrow, but sharp.

+ +
+
+
    +
  1. Empty chair time is economically visible immediately.
  2. Schedules are dense and hard to recover once notice is late.
  3. Owners and practice managers feel the pain directly in daily operations.
  4. +
+
+
+ +
+
+
+

Next step

+

Does this look like a realistic workflow for your clinic?

+

If yes, the best next step is a short meeting where we do not demo first. We review the current no-show, cancellation, and recovery workflow.

+
+ +
+
+ +
+
+ + +
+ + + diff --git a/src/site/en/trust.html b/src/site/en/trust.html new file mode 100644 index 0000000..c802841 --- /dev/null +++ b/src/site/en/trust.html @@ -0,0 +1,226 @@ + + + + + + Renvoo | Trust, data boundary, and clinic materials + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+

Trust

+

Built to validate with clinics, not to overclaim.

+

This page is for the skeptical route: what Renvoo does and does not claim, what data is and is not in scope, and what clinic materials already exist for real conversations.

+ +
+
+
+ +
+
+

Data boundary & role split

+

Narrow enough to stay workable

+ +
+
+
+

Non-clinical scope

+

Renvoo is designed around administrative scheduling and communication data, not diagnoses, treatment decisions, or clinical notes.

+
+

Clinic stays controller

+

For patient and appointment operational data, the clinic stays controller and Renvoo acts as processor under clinic instruction.

+
+

No patient platform in v1

+

The first pilots stay away from patient accounts, marketplace behavior, and unnecessary consumer-facing product sprawl.

+
+
+
+ +
+
+

Proof posture

+

What can be said honestly today

+ +
+
+
+
    +
  • The current case is grounded in operating logic and scenario math, not invented traction.
  • The pilot exists to validate real reduction, recovery, and admin relief with live clinic workflows.
  • The founder-led motion is meant to create workflow proof, not a generic self-serve funnel.
  • +
+
+
+

Founder

+

Renvoo is being built by Mohamed Ibrahim, former Electrical Subteam Lead for Team Polar at TU/e and part of the NVIDIA 6G Developer Program.

+
+
+
+ +
+
+

Frequent questions

+

The main objections without startup theater

+ +
+
+
+ We already send reminders. What changes then? +

That makes sense. Reminders are part of the baseline. The gap Renvoo is focused on is what happens when reminders are not enough: no response, late response, and manual recovery work around lost capacity.

+
+ We do not want another heavy system. +

That is exactly why the current direction is low friction: CSV-first or read-only onboarding, no patient accounts in v1, and a short workflow validation before anything heavier.

+
+ We are careful with patient data. +

That caution is right. Renvoo is designed around non-clinical administrative scheduling and communication data, not diagnoses, clinical notes, or treatment decisions.

+
+ Do patients need to create an account? +

No. The v1 posture is clinic SaaS, not a patient platform. Patient interaction should happen through clinic-triggered reminders, confirmations, and secure links.

+
+ We are too busy to change workflow now. +

That is part of the problem Renvoo is trying to solve. The first meeting is not a heavy rollout. It is there to see whether the operational pain is strong enough for a lightweight pilot.

+
+ How do we know this is worth it? +

That is the central pilot question. The current case is logical, but the pilot should validate whether no-show reduction, recovery, and admin relief become visible in your clinic.

+
+
+
+ +
+
+

Clinic materials

+

The outreach assets are already concrete enough for real conversations

+

The website is not the only artifact. This funnel sits alongside the same one-pager, deck, and operator framing already used in founder-led outreach.

+
+
+
+
+
+

Clinic one-pager

+

Compact leave-behind for operators after a short intro or follow-up email.

+ Download the one-pager +
+

PDF leave-behind

+

Ready-to-forward PDF version of the one-pager.

+ Download the PDF +
+
+
+
+
+ Preview of the Renvoo clinic one-pager +
One-pager preview
+
+ Preview of the first Renvoo clinic deck slide +
Deck cover slide
+
+ Preview of the Renvoo clinic deck solution slide +
Deck solution slide
+
+
+
+
+ + + +
+
+
+

Next step

+

If the posture feels right, request the short meeting.

+

The funnel is intentionally simple: understand the workflow, validate the trust layer, and only then request the conversation.

+
+ +
+
+ +
+
+ + +
+ + + diff --git a/src/site/index.html b/src/site/index.html index a4f0ae5..3093303 100644 --- a/src/site/index.html +++ b/src/site/index.html @@ -1,604 +1,202 @@ - + - - Renvoo | Reduce no-shows. Recover lost appointments. - - - - - - - - - - - - - - - - - - - - + + Renvoo | Minder no-shows. Meer grip op de agenda. + + + + + + + + + + + + + + + + + - - -
-
- + + + + - -
-
-
-

B2B clinic SaaS for Dutch dental clinics

-

Reduce no-shows. Recover lost appointments.

-

- Renvoo is a B2B clinic SaaS for Dutch dental clinics that helps reduce no-shows, - handle late cancellations earlier, and recover lost appointment capacity without - adding front-desk work. -

+ +
+
+ +
+ +
+
+
+

Voor Nederlandse tandartspraktijken

+

Minder no-shows. Meer grip op de agenda.

+

Renvoo helpt tandartspraktijken no-shows eerder signaleren, late uitval rustiger opvangen en verloren afspraken sneller herstellen zonder extra front-desk werk.

-
    -
  • Operational workflow SaaS
  • -
  • No patient accounts in v1
  • -
  • Admin-data-only onboarding
  • +
      +
    • Geen patiëntaccounts in v1
    • Niet-klinische datagrens
    • Pilot-first, niet platform-first
- -
-
-
- Clinic operations view - Pilot-facing workflow -
-
-
- Detect -

Chair time at risk

-

High-risk appointments surface before the slot is lost.

-
    -
  • Tuesday 14:00 implant consult
  • -
  • Thursday 09:30 hygiene block
  • -
  • Friday 16:15 follow-up visit
  • -
-
-
- Confirm -

Act earlier

-

Escalate confirmation and rescheduling before uncertainty becomes waste.

-
- SMS sent - Reply pending - Phone fallback queued -
-
-
- Replace -

Recover capacity

-

Use waitlists and earlier-slot offers to backfill otherwise empty time.

-
- -
- Backfill remains under clinic control. -
-
-
-
-
- -
-
-

The problem

-

Reminders exist. Prevention does not.

-

- Sending the same reminder to everyone still leaves clinics reacting too late. -

-
-
-
-

No-shows turn booked time into lost revenue

-

- A booked slot only creates value if the patient actually shows up. Empty chair time - is immediate lost capacity. -

-
-
-

Late cancellations still leave chair time empty

-

- Even when patients tell you, late notice often arrives too late to save the slot. -

-
-
-

Admin work grows around uncertainty

-

- Confirmations, rescheduling, and recovery work all increase when attendance is - unclear. -

-
-
-
- -
-
-

The workflow

-

Connect. Detect. Confirm. Replace.

-

- Renvoo is designed as a clinic operations layer, not just a reminder tool. -

-
-
-
- 01 -

Connect

-

- Start with lightweight exports, read-only inputs, or local connector flows instead of - heavy enterprise integration. -

-
-
- 02 -

Detect

-

- Highlight higher-risk appointments using operational scheduling and communication - signals. -

-
-
- 03 -

Confirm

-

- Support earlier outreach, clearer status tracking, and lower uncertainty before the - slot is lost. -

-
-
- 04 -

Replace

-

- Help clinics recover otherwise empty time through waitlists or earlier-slot movement - while keeping operational control with staff. -

-
-
-
- -
-
-

How a pilot starts

-

Start with a short workflow review, not a heavy software project

-

- The point of the first conversation is to understand where uncertainty creates empty - chair time and whether a lightweight pilot is justified. -

-
-
-
- Step 1 -

Validation meeting

-

- Review current no-show, late-cancellation, confirmation, and rescheduling workflow - with the clinic owner or practice manager. -

-
-
- Step 2 -

Lightweight intake

-

- Start with operational scheduling and communication data through CSV-first or - read-only inputs instead of pushing for deep integration on day one. -

-
-
- Step 3 -

Measure real outcomes

-

- Use the pilot to learn whether no-show reduction, recovered revenue, and lower - front-desk load show up clearly enough to earn a broader rollout. -

-
-
-
- -
-
-
-

Proof posture

-

Built to validate with clinics, not just describe the problem

-

- The case today is based on operating logic and scenario math. The point of a pilot is - to prove the outcome on real clinic workflows without overreaching into clinical data. -

-
-
- 15-25% - pilot target for no-show reduction -
-
- 30%+ - backfill target within 72 hours -
-
- 20%+ - admin time reduction target -
-
-
- -
-

Data boundary

-

Operational AI with a narrower data surface

-
    -
  • Non-clinical administrative scheduling data only
  • -
  • No diagnoses, clinical notes, or treatment decisions required
  • -
  • Auditability and staff visibility are part of the product skeleton
  • +
-
-
- -
-
-

Compliance posture

-

Kept deliberately simple for the first clinic pilots

-

- Renvoo is being launched as clinic SaaS, not as a patient platform or public - marketplace. The data and contract model stay narrow on purpose. -

-
-
-
-

Clinic controls patient data

-

- For appointment and patient operational data, the clinic stays controller and Renvoo - acts as processor under clinic instruction. -

-
-
-

v1 stays out of the patient-platform trap

-

- No public marketplace, no user-generated content, no patient subscriptions, and no - patient accounts in the first release. -

-
-
-

Non-clinical boundary by default

-

- The default rollout excludes BSN, diagnosis, medication, lab data, treatment - decisions, and clinical free text. -

-
-
- -
- -
-
-

Preliminary pilot

-

Low-friction by design

-

- Enough to frame budget fit now, while keeping the commercial commitment honest. -

-
-
-
-

What clinics should expect

-

- Founder-led onboarding, lightweight data intake, and a workflow focused on - confirmation, rescheduling, and capacity recovery without asking patients to create a - new account. -

-
    -
  • Dental-first positioning
  • -
  • Read-only or CSV-first rollout
  • -
  • Staff control over schedule changes
  • -
-
-
-

Preliminary pilot pricing

-

- Preliminary pilot pricing starts at EUR 2.49 per appointment, plus a fixed fee - depending on the clinic. Final pricing is still being shaped with pilot partners. -

- - Pricing is intentionally presented as preliminary because it still needs shaping with - real pilot partners. - -
-
-
- -
-
-
-

Founder credibility

-

Built to have practical conversations with operators

-

- Renvoo is being built by Mohamed Ibrahim, former Electrical Subteam Lead for Team - Polar at TU/e and part of the NVIDIA 6G Developer Program. -

- Renvoo mark -
-
- -
-
-

Clinic materials

-

Built to support real outreach, not just sit on a homepage

-

- The current website pass ships with the same clinic-facing materials already used in - founder-led outreach. -

-
-
-
-
-

What is ready now

-
    -
  • Clinic one-pager for leave-behinds and email follow-up
  • -
  • Clinic deck for validation calls and short pilot discussions
  • -
  • Messaging aligned with the current dental-first B2B SaaS wedge
  • -
  • Pre-launch privacy and patient message notes for the simple pilot model
  • -
- -
-
- -
-
- Preview of the Renvoo clinic one-pager -
Clinic one-pager preview
-
-
- Preview of the Renvoo clinic deck cover slide -
Deck opening slide
-
-
- Preview of the Renvoo clinic deck solution slide -
Operational workflow slide
-
-
-
-
- -
-
-

Questions clinics ask first

-

Objection handling without startup theater

-

- The first conversations are usually practical: workflow disruption, data boundaries, - and whether the outcome is worth changing anything at all. -

-
-
-
- We already send reminders. -

- That makes sense. The gap Renvoo focuses on is what happens when reminders are not - enough: no response, late notice, and the manual work needed to recover capacity - before the chair time is lost. -

-
-
- We do not want another system. -

- That is exactly why the current direction is low-friction. The goal is not to force a - heavy system change, but to fit around current scheduling operations with lightweight - onboarding and operational visibility. -

-
-
- We are careful with patient data. -

- That caution is right. Renvoo is designed around non-clinical administrative - scheduling and communication data, not diagnoses, clinical notes, or treatment - decisions. -

-
-
- Do patients need a Renvoo account? -

- No. The current v1 posture is clinic SaaS, not a patient platform. The intention is - to use clinic-triggered reminders, confirmations, and secure links without adding a - separate patient account unless a later phase clearly requires it. -

-
-
- We are too busy to change workflow. -

- The point of a short validation meeting is not to introduce a full rollout. It is to - understand the current workflow and see whether there is enough operational pain to - justify a lightweight pilot. -

-
-
- How do we know this is worth it? -

- That is the key question. The current case is based on clear operational logic and - scenario math, and the pilot stage exists to validate real no-show reduction, - recovered revenue, and reduced admin burden with actual clinic workflows. -

-
-
-
- -
-
-

Current ask

-

We are looking for a short validation meeting.

-

- We are looking for a short validation meeting with a dental clinic owner or practice - manager to review your current no-show and rescheduling workflow. -

- -
- Dental clinics first - Operational value, not AI theater - Lightweight onboarding path -
-
-
+ +
+ + +
+
+

Het echte probleem

+

De schade zit niet alleen in de no-show.

+

Het verlies ontstaat in de combinatie van lege stoeluren, te late signalen en extra herstelwerk rond bevestigen, verplaatsen en opvullen.

+
+
+
+

Lege stoeluren raken direct de omzet

+

Een geplande afspraak levert pas iets op als de patiënt ook echt verschijnt.

+
+

Late uitval is vaak netjes gemeld, maar nog steeds duur

+

Zelfs een keurige afmelding kan te laat komen om de stoel nog te redden.

+
+

Herstelwerk groeit rond onzekerheid

+

Bevestigen, nabellen, schuiven en terugvullen belanden als extra werk bij het team.

+
+
+
+ +
+
+

Hoe Renvoo denkt

+

Van losse reminders naar planningszekerheid

+

De pilot is ontworpen als operationele workflowlaag. Eerst zien waar risico zit, daarna gerichter bevestigen of verplaatsen en pas dan herstellen.

+
+
+
+ Connect +

Start met plannings- en communicatiedata, niet met klinische inhoud.

+
+ Detect +

Maak risicovolle afspraken eerder zichtbaar dan de standaard reminderflow doet.

+
+ Confirm +

Geef het team een duidelijker moment om te bevestigen of te verplaatsen.

+
+ Replace +

Help verloren capaciteit sneller terug te winnen terwijl de praktijk de controle houdt.

+
+
+
+ +
+
+

Waarom dit geloofwaardig voelt

+

Gebouwd om met praktijken te valideren, niet om alvast te overbeloven.

+ +
+
+
+

Bewijspositie

+

De huidige case rust op duidelijke operationele logica en scenario-denken. De pilot is er juist om echte reductie, herstel en tijdswinst te bewijzen.

+
+

Datagrens

+

Renvoo is bewust gepositioneerd rond administratieve planning- en communicatiegegevens, niet rond diagnoses, notities of behandelbeslissingen.

+
+
+
+ +
+
+

Kies uw route

+

Niet iedereen wil hetzelfde eerst zien.

+ +
+
+
+

Eerst begrijpen wat het product doet

+

Bekijk de volledige Connect → Detect → Confirm → Replace workflow.

+ Naar product +
+

Eerst zien hoe de pilot werkt

+

Bekijk de validatie-opzet, de meetingstructuur en de planner.

+ Naar pilot +
+

Eerst vertrouwen en datagrens controleren

+

Zie de controller/processor-posture, FAQ en clinic-materialen.

+ Naar vertrouwen +
+
+
+ +
+
+
+

Volgende stap

+

De volgende stap is klein en praktisch.

+

We zoeken een kort validatiegesprek met een praktijkhouder of praktijkmanager om de huidige no-show- en uitvalworkflow te begrijpen.

+
+ +
+
+
- - - + diff --git a/src/site/lib/booking.d.ts b/src/site/lib/booking.d.ts new file mode 100644 index 0000000..c9005e9 --- /dev/null +++ b/src/site/lib/booking.d.ts @@ -0,0 +1,59 @@ +export interface BookingValues { + contactName: string; + contactEmail: string; + role: string; + clinicName: string; + city: string; + clinicSize: string; + primaryPain: string; + workflowNotes: string; + meetingFormat: string; + availability: string[]; +} + +export interface BookingPayload extends BookingValues { + submittedAt: string; + locale: string; + page: string; + mode: string; +} + +export declare const BOOKING_STORAGE_KEY: string; +export declare const bookingFieldOrder: string[]; + +export declare function collectBookingValues(form: HTMLFormElement): BookingValues; +export declare function validateBookingStep(step: number, values: BookingValues): Record; +export declare function getFirstInvalidField(errors: Record): string | null; +export declare function createBookingPayload( + values: BookingValues, + meta?: Partial>, +): BookingPayload; +export declare function persistBookingRequest(payload: BookingPayload): void; +export declare function readStoredBookingRequests(): BookingPayload[]; +export declare function createPlainTextSummary( + payload: BookingPayload, + labels: { + summaryTitle: string; + submittedAt: string; + mode: string; + contactName: string; + contactEmail: string; + role: string; + clinicName: string; + city: string; + clinicSize: string; + primaryPain: string; + workflowNotes: string; + meetingFormat: string; + availabilityTitle: string; + roles: Record; + clinicSizes: Record; + primaryPains: Record; + meetingFormats: Record; + availability: Record; + }, +): string; +export declare function createDownloadFile(summary: string, fileName: string): { + fileName: string; + href: string; +}; diff --git a/src/site/lib/booking.js b/src/site/lib/booking.js new file mode 100644 index 0000000..d02f3cc --- /dev/null +++ b/src/site/lib/booking.js @@ -0,0 +1,159 @@ +export const BOOKING_STORAGE_KEY = "renvoo-booking-requests"; + +const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export const bookingFieldOrder = [ + "contactName", + "contactEmail", + "role", + "clinicName", + "city", + "clinicSize", + "primaryPain", + "workflowNotes", + "meetingFormat", + "availability", +]; + +export function collectBookingValues(form) { + const formData = new FormData(form); + + return { + contactName: formData.get("contactName")?.toString().trim() ?? "", + contactEmail: formData.get("contactEmail")?.toString().trim() ?? "", + role: formData.get("role")?.toString().trim() ?? "", + clinicName: formData.get("clinicName")?.toString().trim() ?? "", + city: formData.get("city")?.toString().trim() ?? "", + clinicSize: formData.get("clinicSize")?.toString().trim() ?? "", + primaryPain: formData.get("primaryPain")?.toString().trim() ?? "", + workflowNotes: formData.get("workflowNotes")?.toString().trim() ?? "", + meetingFormat: formData.get("meetingFormat")?.toString().trim() ?? "", + availability: formData + .getAll("availability") + .map((value) => value.toString().trim()) + .filter(Boolean), + }; +} + +export function validateBookingStep(step, values) { + const errors = {}; + + if (step === 1) { + if (!values.contactName) { + errors.contactName = "required"; + } + + if (!values.contactEmail) { + errors.contactEmail = "required"; + } else if (!EMAIL_PATTERN.test(values.contactEmail)) { + errors.contactEmail = "email"; + } + + if (!values.role) { + errors.role = "required"; + } + + if (!values.clinicName) { + errors.clinicName = "required"; + } + + if (!values.city) { + errors.city = "required"; + } + + if (!values.clinicSize) { + errors.clinicSize = "required"; + } + + if (!values.primaryPain) { + errors.primaryPain = "required"; + } + + if (!values.workflowNotes) { + errors.workflowNotes = "required"; + } else if (values.workflowNotes.length < 16) { + errors.workflowNotes = "tooShort"; + } + } + + if (step === 2) { + if (!values.meetingFormat) { + errors.meetingFormat = "required"; + } + + if (!values.availability.length) { + errors.availability = "required"; + } + } + + return errors; +} + +export function getFirstInvalidField(errors) { + return bookingFieldOrder.find((field) => field in errors) ?? null; +} + +export function createBookingPayload(values, meta = {}) { + return { + ...values, + submittedAt: meta.submittedAt ?? new Date().toISOString(), + locale: meta.locale ?? "en", + page: meta.page ?? "pilot", + mode: meta.mode ?? "preview", + }; +} + +export function persistBookingRequest(payload) { + const existing = readStoredBookingRequests(); + const updated = [payload, ...existing].slice(0, 20); + localStorage.setItem(BOOKING_STORAGE_KEY, JSON.stringify(updated)); +} + +export function readStoredBookingRequests() { + if (typeof localStorage === "undefined") { + return []; + } + + try { + const raw = localStorage.getItem(BOOKING_STORAGE_KEY); + if (!raw) { + return []; + } + + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +export function createPlainTextSummary(payload, labels) { + const availabilitySummary = payload.availability + .map((value) => labels.availability[value] ?? value) + .join(", "); + + return [ + `${labels.summaryTitle}`, + "", + `${labels.contactName}: ${payload.contactName}`, + `${labels.contactEmail}: ${payload.contactEmail}`, + `${labels.role}: ${labels.roles[payload.role] ?? payload.role}`, + `${labels.clinicName}: ${payload.clinicName}`, + `${labels.city}: ${payload.city}`, + `${labels.clinicSize}: ${labels.clinicSizes[payload.clinicSize] ?? payload.clinicSize}`, + `${labels.primaryPain}: ${labels.primaryPains[payload.primaryPain] ?? payload.primaryPain}`, + `${labels.workflowNotes}: ${payload.workflowNotes}`, + `${labels.meetingFormat}: ${labels.meetingFormats[payload.meetingFormat] ?? payload.meetingFormat}`, + `${labels.availabilityTitle}: ${availabilitySummary}`, + `${labels.submittedAt}: ${payload.submittedAt}`, + `${labels.mode}: ${payload.mode}`, + ].join("\n"); +} + +export function createDownloadFile(summary, fileName) { + const blob = new Blob([summary], { type: "text/plain;charset=utf-8" }); + return { + fileName, + href: URL.createObjectURL(blob), + }; +} diff --git a/src/site/lib/site-content.js b/src/site/lib/site-content.js new file mode 100644 index 0000000..118bfe4 --- /dev/null +++ b/src/site/lib/site-content.js @@ -0,0 +1,1376 @@ +const sharedFounderLine = + "Renvoo is being built by Mohamed Ibrahim, former Electrical Subteam Lead for Team Polar at TU/e and part of the NVIDIA 6G Developer Program."; + +const sharedPricingLine = + "Preliminary pilot pricing starts at EUR 2.49 per appointment, plus a fixed fee depending on the clinic. Final pricing is still being shaped with pilot partners."; + +const sharedDownloads = [ + { + title: "Clinic one-pager", + description: "Compact leave-behind for operators after a short intro or follow-up email.", + href: "downloads/renvoo-clinic-one-pager.pptx", + label: "Download the one-pager", + }, + { + title: "Clinic deck", + description: "Clinic-facing deck for workflow reviews and pilot conversations.", + href: "downloads/renvoo-clinic-deck.pptx", + label: "Download the clinic deck", + track: "deck_downloaded", + }, + { + title: "PDF leave-behind", + description: "Ready-to-forward PDF version of the one-pager.", + href: "downloads/renvoo-clinic-one-pager.pdf", + label: "Download the PDF", + }, +]; + +export const siteContent = { + nl: { + htmlLang: "nl", + locale: "nl-NL", + languageLabel: "Nederlands", + switchLabel: "EN", + brandLine: "Operationele SaaS voor rustigere agenda's", + nav: { + home: "Start", + product: "Product", + pilot: "Pilot", + trust: "Vertrouwen", + cta: "Plan gesprek", + }, + footer: { + summary: + "Renvoo is een vroege B2B clinic SaaS voor Nederlandse tandartspraktijken die no-shows, late uitval en leeg stoeluur rustiger wil opvangen.", + quickLinksTitle: "Funnel", + legalTitle: "Juridisch", + languageTitle: "Taal", + legalLinks: [ + { page: "privacy", label: "Privacyverklaring website" }, + { page: "patientNotice", label: "Notitie patiëntbericht" }, + ], + languageLinks: [{ lang: "en", page: "home", label: "English" }], + proofNote: "Gebouwd rond pilot-proof, niet rond opgeblazen claims.", + }, + pageNames: { + home: "Start", + product: "Product", + pilot: "Pilot", + trust: "Vertrouwen", + privacy: "Privacy", + patientNotice: "Patiëntbericht", + notFound: "Niet gevonden", + }, + pages: { + home: { + seo: { + title: "Renvoo | Minder no-shows. Meer grip op de agenda.", + description: + "Renvoo helpt Nederlandse tandartspraktijken no-shows eerder signaleren, late uitval rustiger opvangen en verloren afspraken beter herstellen zonder extra front-desk werk.", + }, + hero: { + eyebrow: "Voor Nederlandse tandartspraktijken", + title: "Minder no-shows. Meer grip op de agenda.", + body: + "Renvoo helpt tandartspraktijken no-shows eerder signaleren, late uitval rustiger opvangen en verloren afspraken sneller herstellen zonder extra front-desk werk.", + primaryCta: "Vraag een validatiegesprek aan", + secondaryCta: "Bekijk hoe het werkt", + badges: [ + "Geen patiëntaccounts in v1", + "Niet-klinische datagrens", + "Pilot-first, niet platform-first", + ], + operatorCard: { + title: "Waar operators direct last van hebben", + items: [ + "Boekingen lijken gevuld, maar stoeltijd blijft kwetsbaar.", + "Late afzeggingen geven te weinig tijd om slim te herstellen.", + "Front-desk teams vangen de onzekerheid handmatig op.", + ], + }, + }, + pains: { + eyebrow: "Het echte probleem", + title: "De schade zit niet alleen in de no-show.", + intro: + "Het verlies ontstaat in de combinatie van lege stoeluren, te late signalen en extra herstelwerk rond bevestigen, verplaatsen en opvullen.", + items: [ + { + title: "Lege stoeluren raken direct de omzet", + body: "Een geplande afspraak levert pas iets op als de patiënt ook echt verschijnt.", + }, + { + title: "Late uitval is vaak netjes gemeld, maar nog steeds duur", + body: "Zelfs een keurige afmelding kan te laat komen om de stoel nog te redden.", + }, + { + title: "Herstelwerk groeit rond onzekerheid", + body: "Bevestigen, nabellen, schuiven en terugvullen belanden als extra werk bij het team.", + }, + ], + }, + workflowTeaser: { + eyebrow: "Hoe Renvoo denkt", + title: "Van losse reminders naar planningszekerheid", + intro: + "De pilot is ontworpen als operationele workflowlaag. Eerst zien waar risico zit, daarna gerichter bevestigen of verplaatsen en pas dan herstellen.", + steps: [ + { + label: "Connect", + body: "Start met plannings- en communicatiedata, niet met klinische inhoud.", + }, + { + label: "Detect", + body: "Maak risicovolle afspraken eerder zichtbaar dan de standaard reminderflow doet.", + }, + { + label: "Confirm", + body: "Geef het team een duidelijker moment om te bevestigen of te verplaatsen.", + }, + { + label: "Replace", + body: "Help verloren capaciteit sneller terug te winnen terwijl de praktijk de controle houdt.", + }, + ], + }, + proof: { + eyebrow: "Waarom dit geloofwaardig voelt", + title: "Gebouwd om met praktijken te valideren, niet om alvast te overbeloven.", + cards: [ + { + title: "Bewijspositie", + body: "De huidige case rust op duidelijke operationele logica en scenario-denken. De pilot is er juist om echte reductie, herstel en tijdswinst te bewijzen.", + }, + { + title: "Datagrens", + body: "Renvoo is bewust gepositioneerd rond administratieve planning- en communicatiegegevens, niet rond diagnoses, notities of behandelbeslissingen.", + }, + ], + }, + paths: { + eyebrow: "Kies uw route", + title: "Niet iedereen wil hetzelfde eerst zien.", + items: [ + { + title: "Eerst begrijpen wat het product doet", + body: "Bekijk de volledige Connect → Detect → Confirm → Replace workflow.", + page: "product", + label: "Naar product", + }, + { + title: "Eerst zien hoe de pilot werkt", + body: "Bekijk de validatie-opzet, de meetingstructuur en de planner.", + page: "pilot", + label: "Naar pilot", + }, + { + title: "Eerst vertrouwen en datagrens controleren", + body: "Zie de controller/processor-posture, FAQ en clinic-materialen.", + page: "trust", + label: "Naar vertrouwen", + }, + ], + }, + closing: { + eyebrow: "Volgende stap", + title: "De volgende stap is klein en praktisch.", + body: + "We zoeken een kort validatiegesprek met een praktijkhouder of praktijkmanager om de huidige no-show- en uitvalworkflow te begrijpen.", + primaryCta: "Plan het gesprek", + secondaryCta: "Bekijk eerst de pilot", + }, + }, + product: { + seo: { + title: "Renvoo | Productflow voor no-showpreventie", + description: + "Bekijk hoe Renvoo Nederlandse tandartspraktijken helpt met een operationele workflow voor signaleren, bevestigen, verplaatsen en herstellen.", + }, + hero: { + eyebrow: "Product", + title: "Niet nog een reminder-tool.", + body: + "Renvoo is bedoeld als operationele laag rond planningsonzekerheid. Het doel is niet meer berichten sturen, maar eerder weten waar uitval dreigt en het team betere herstelmomenten geven.", + primaryCta: "Plan een validatiegesprek", + secondaryCta: "Bekijk vertrouwen & data", + }, + comparison: { + eyebrow: "Voor en na", + title: "Wat verandert er operationeel", + beforeTitle: "Zonder Renvoo", + afterTitle: "Met Renvoo", + before: [ + "Iedere afspraak krijgt ongeveer dezelfde reminderflow.", + "Risico wordt vaak pas duidelijk als de stoel al kwetsbaar is.", + "Front-desk teams reageren handmatig op gaten en no-response.", + "Terugvullen gebeurt via losse lijsten, geheugen en tijdsdruk.", + ], + after: [ + "Het team ziet eerder welke afspraken extra aandacht nodig hebben.", + "Bevestigen en verplaatsen kan gerichter gebeuren, niet alleen generiek.", + "Herstelwerk wordt onderdeel van een duidelijke workflow in plaats van losse brandjes.", + "De praktijk houdt controle over agenda-mutaties en patiëntcontactmomenten.", + ], + }, + workflow: { + eyebrow: "De vier stappen", + title: "Connect. Detect. Confirm. Replace.", + intro: + "De architectuur is bewust klein gehouden voor de eerste pilots: laagdrempelige intake, heldere teamzichtbaarheid en geen patiëntplatform in v1.", + steps: [ + { + title: "Connect", + body: "CSV-first of read-only onboarding rond agenda-, afspraak- en communicatievelden die operationeel genoeg zijn om risico te zien.", + bullets: [ + "Focus op scheduling en statusvelden", + "Geen diagnoses of klinische notities nodig", + "Lage frictie voor een eerste pilot", + ], + }, + { + title: "Detect", + body: "Afspraken met verhoogd no-show- of late-uitvalrisico worden eerder zichtbaar dan in een standaard reminderprogramma.", + bullets: [ + "Risico per afspraak in plaats van één generieke flow", + "Operatorgericht, niet klinisch", + "Gebouwd voor uitlegbaar gebruik in een praktijk", + ], + }, + { + title: "Confirm", + body: "Het team kan sneller zien waar een bevestiging, verplaatsing of extra check zinvol is voordat de stoel verloren raakt.", + bullets: [ + "Ondersteunt praktische opvolging", + "Past naast bestaande systemen", + "Helpt no-response eerder adresseren", + ], + }, + { + title: "Replace", + body: "Wanneer een stoel vrijkomt, helpt de workflow de praktijk eerder denken in herstel en backfill terwijl daadwerkelijke agenda-acties onder praktijkcontrole blijven.", + bullets: [ + "Geen autonome mutaties in v1", + "Herstel boven theater", + "Praktijk of EPD houdt de uiteindelijke controle", + ], + }, + ], + }, + fit: { + eyebrow: "Lage frictie als ontwerpkeuze", + title: "Bewust eenvoudig gehouden voor de eerste kliniekpilots", + items: [ + { + title: "Niet-klinische datagrens", + body: "Administratieve planning- en communicatiegegevens eerst. Dat houdt de scope smal en operationeel.", + }, + { + title: "CSV-first of read-only intake", + body: "De eerste stap hoeft geen zwaar integratieproject te zijn om te zien of de pilot de moeite waard is.", + }, + { + title: "Geen patiëntaccounts in v1", + body: "Patiëntinteractie blijft via praktijkgestuurde berichten en veilige links, niet via een nieuw consumentproduct.", + }, + ], + }, + whyDental: { + eyebrow: "Waarom tandartspraktijken eerst", + title: "De wedge is klein, maar scherp.", + items: [ + "Lege stoeluren zijn direct economisch voelbaar.", + "Agenda's zijn dicht gepland en laten weinig herstelruimte over.", + "Praktijkhouders en praktijkmanagers voelen dit probleem zelf in de operatie.", + ], + }, + closing: { + eyebrow: "Volgende stap", + title: "Ziet dit eruit als een realistische workflow voor uw praktijk?", + body: + "Dan is de beste volgende stap een kort gesprek waarin we niet demo'en, maar uw huidige no-show- en uitvalproces scherp krijgen.", + primaryCta: "Vraag het validatiegesprek aan", + secondaryCta: "Bekijk de pilot", + }, + }, + pilot: { + seo: { + title: "Renvoo | Vraag een validatiegesprek aan", + description: + "Vraag een kort validatiegesprek aan en deel wanneer uw tandartspraktijk het liefst wil praten over no-shows, late uitval en herstelcapaciteit.", + }, + hero: { + eyebrow: "Pilot", + title: "Vraag een validatiegesprek aan", + body: + "Het eerste gesprek is geen zware salesdemo. Het is een korte operator-review van uw no-show-, bevestigings- en herstelworkflow om te zien of een lichte pilot logisch is.", + primaryCta: "Start de planner", + secondaryCta: "Bekijk eerst het product", + chips: ["15 minuten", "Founder-led", "Geen live kalender-sync nodig"], + }, + agenda: { + eyebrow: "Wat het gesprek oplevert", + title: "Praktisch genoeg voor een eerste ja of nee", + items: [ + { + title: "Workflow-scan", + body: "We lopen kort door hoe no-shows, late uitval en herstel nu worden afgehandeld.", + }, + { + title: "Signalen & beperkingen", + body: "We bespreken waar de praktijk te laat zicht krijgt en welke data of tooling er nu al beschikbaar is.", + }, + { + title: "Pilot-fit", + body: "We bepalen of er genoeg operationele pijn is voor een kleine, meetbare pilot.", + }, + ], + }, + bookingIntro: { + eyebrow: "Lokale planner", + title: "Kies uw voorkeurstijden zoals in een lichte Calendly-flow", + body: + "Deze planner werkt nu als request-flow op basis van lokale beschikbaarheidsblokken. De aanvraag wordt als nette handoff opgeslagen zolang live routing nog niet is gekoppeld.", + support: [ + "Naam en zakelijke contactgegevens", + "Korte context over praktijk en huidige workflow", + "Voorkeur voor gesprekstype en tijdsblokken", + ], + }, + pricing: { + eyebrow: "Prijsstelling blijft secundair", + title: "Preliminary pilot pricing", + body: sharedPricingLine, + note: + "De prijsregel is er om budgetfit te framen, niet om een zwaar commitment te forceren vóór validatie.", + }, + plannerAside: { + title: "Wat u na versturen ziet", + items: [ + "Een lokale successtatus met samenvatting", + "Een kopieerbare handoff voor de live routing", + "Een schone samenvatting voor interne opvolging", + ], + responseExpectation: + "Doelrespons: een bevestiging van het gesprek of een korte follow-up om een definitieve tijd vast te zetten.", + }, + fallback: { + title: "Nog niet klaar om te boeken?", + body: + "Bekijk dan eerst de trust-pagina met datagrens, proof posture, materialen en juridische notities voordat u terugkomt naar de planner.", + label: "Eerst vertrouwen bekijken", + }, + }, + trust: { + seo: { + title: "Renvoo | Vertrouwen, datagrens en clinic-materialen", + description: + "Bekijk de trust-laag van Renvoo: proof posture, controller/processor-rolverdeling, FAQ, materialen en juridische notities voor de eerste tandartspraktijkpilots.", + }, + hero: { + eyebrow: "Vertrouwen", + title: "Gebouwd om met praktijken te valideren, niet om teveel te beloven.", + body: + "Deze pagina is voor de kritische route: wat Renvoo wel en niet claimt, welke data wel en niet in scope zijn, en welke clinic-materialen al klaar liggen voor een echt gesprek.", + primaryCta: "Ga naar de planner", + secondaryCta: "Bekijk het product", + }, + boundary: { + eyebrow: "Datagrens & rolverdeling", + title: "Nauw genoeg om werkbaar te blijven", + items: [ + { + title: "Niet-klinische scope", + body: "Renvoo is ontworpen rond administratieve planning- en communicatiegegevens, niet rond diagnose, behandeladvies of klinische notities.", + }, + { + title: "Praktijk als controller", + body: "Voor patiënt- en afspraakgegevens in de operatie blijft de praktijk controller en werkt Renvoo als processor onder instructie.", + }, + { + title: "Geen patiëntplatform in v1", + body: "De eerste pilots blijven weg van patiëntaccounts, openbare marktplaatsdynamiek en onnodige consumer-features.", + }, + ], + }, + proof: { + eyebrow: "Bewijspositie", + title: "Wat vandaag eerlijk gezegd kan worden", + items: [ + "De kerncase rust op operationele logica en scenario-math, niet op verzonnen traction.", + "Het doel van de pilot is echte reductie, herstel en admin-tijdswinst valideren met praktijkdata.", + "De founder-led motion is gericht op workflow-validatie, niet op een generieke self-serve funnel.", + ], + founderLine: sharedFounderLine, + }, + faq: { + eyebrow: "Veelgehoorde vragen", + title: "De belangrijkste bezwaren zonder startup-theater", + items: [ + { + question: "We sturen al reminders. Wat verandert er dan?", + answer: + "Reminders zijn het startpunt, niet de oplossing. Renvoo richt zich op wat er gebeurt wanneer de reminder niet genoeg is: geen reactie, te late reactie en extra herstelwerk voor het team.", + }, + { + question: "We willen niet nog een zwaar systeem erbij.", + answer: + "Dat is precies waarom de huidige richting laagdrempelig is: CSV-first of read-only onboarding, geen patiëntaccounts in v1 en eerst een korte workflowvalidatie in plaats van een grote rollout.", + }, + { + question: "Hoe zit het met patiëntdata?", + answer: + "Die voorzichtigheid is terecht. Renvoo is bewust ontworpen rond niet-klinische administratieve planning- en communicatiegegevens. Het doel is operationele verbetering met smallere data-exposure.", + }, + { + question: "Moeten patiënten een account aanmaken?", + answer: + "Nee. De v1-posture is clinic SaaS, geen patiëntplatform. Patiëntinteractie loopt via praktijkgestuurde berichten en veilige links.", + }, + { + question: "We hebben nu geen tijd voor een grote verandering.", + answer: + "Het validatiegesprek is juist bedoeld om te zien of er genoeg operationele pijn is voor een kleine pilot, niet om meteen een zwaar verandertraject te starten.", + }, + { + question: "Hoe weten we of het echt de moeite waard is?", + answer: + "Dat is precies de pilotvraag. De huidige case is logisch, maar de pilot moet bewijzen of no-show-reductie, herstel en admin-tijdswinst in uw praktijk ook echt zichtbaar worden.", + }, + ], + }, + materials: { + eyebrow: "Clinic-materialen", + title: "De outreach-assets zijn al concreet genoeg voor echte gesprekken", + intro: + "De website is niet het enige artefact. De huidige funnel hangt samen met dezelfde one-pager, deck en operatorverhalen die ook in founder-led outreach worden gebruikt.", + downloads: sharedDownloads, + previews: [ + { + src: "assets/previews/one-pager-preview.png", + alt: "Preview van de Renvoo clinic one-pager", + caption: "One-pager preview", + width: 1080, + height: 1528, + }, + { + src: "assets/previews/deck-slide-01.png", + alt: "Preview van de eerste slide van de Renvoo clinic deck", + caption: "Deck cover slide", + width: 1280, + height: 720, + }, + { + src: "assets/previews/deck-slide-07.png", + alt: "Preview van de oplossingsslide van de Renvoo clinic deck", + caption: "Deck solution slide", + width: 1280, + height: 720, + }, + ], + }, + legal: { + eyebrow: "Juridische basis", + title: "Smalle launch-posture", + body: + "De juridische pagina's blijven bewust eenvoudig: website privacy, patiëntberichtnotitie en verwijzingen naar de bredere legal pack voor controller/processor, DPIA-lite en contracttemplates.", + links: [ + { page: "privacy", label: "Privacyverklaring website" }, + { page: "patientNotice", label: "Notitie patiëntbericht" }, + ], + }, + closing: { + eyebrow: "Volgende stap", + title: "Als de posture klopt, plan dan het korte gesprek.", + body: + "De funnel is bewust simpel: eerst begrijpen, dan vertrouwen, dan pas een gesprek aanvragen. Geen theatrale claim nodig om te zien of de workflow pijn echt genoeg is.", + primaryCta: "Plan het validatiegesprek", + secondaryCta: "Bekijk de pilotflow", + }, + }, + privacy: { + seo: { + title: "Renvoo | Privacyverklaring website", + description: + "Pre-launch privacy note voor Renvoo's clinic-SaaS website, lead capture en pilot outreach flow.", + }, + hero: { + eyebrow: "Pre-launch note", + title: "Privacyverklaring website", + body: + "Deze pagina beschrijft de smalle privacy-posture die nu is bedoeld voor Renvoo's eigen website, clinic lead capture en pilot outreach. Voor live launch moeten de definitieve `Renvoo B.V.` contactgegevens en keuzes nog worden ingevuld.", + }, + cards: [ + { + title: "Wat deze pagina dekt", + body: + "Deze note gaat over Renvoo's eigen website, lead capture en business-developmentcommunicatie. Het is geen vervanging voor de privacy notice van een kliniek richting patiënten.", + }, + { + title: "Huidige productposture", + list: [ + "Renvoo wordt gelanceerd als B2B clinic SaaS voor Nederlandse private klinieken, gestart met tandartspraktijken.", + "De site is informatief en gericht op lead capture en pilotgesprekken.", + "De site is geen patiëntportaal en geen patiëntaccountproduct.", + ], + }, + { + title: "Welke data Renvoo direct kan verzamelen", + list: [ + "naam, praktijk en zakelijke contactgegevens die u zelf verstuurt", + "berichten, meeting requests en notities uit pilotgesprekken", + "basis technische en security-informatie die nodig is om de site te laten werken", + ], + }, + { + title: "Cookies", + body: + "De beoogde launch-posture is geen niet-noodzakelijke cookies standaard. Als dat verandert, hoort daar eerst een expliciete consent-flow bij.", + }, + { + title: "Waarom die data wordt gebruikt", + list: [ + "om praktijkvragen te beantwoorden", + "om validatie- of pilotgesprekken in te plannen", + "om de website te beveiligen en beheren", + "om normale bedrijfsadministratie uit te voeren", + ], + }, + { + title: "Implementatienotitie", + body: + "Vervang deze pre-launch note vóór publieke livegang door de definitieve website privacy notice uit de legal pack, met contactgegevens, bewaartermijnen en eventuele goedgekeurde vendors.", + }, + ], + }, + patientNotice: { + seo: { + title: "Renvoo | Notitie patiëntbericht", + description: + "Pre-launch patient message note voor praktijkgestuurde herinneringen, bevestigingen en veilige verplaatslinks in de eerste Renvoo-pilotfase.", + }, + hero: { + eyebrow: "Pre-launch note", + title: "Notitie patiëntbericht", + body: + "Deze pagina laat de korte patiëntnotitie zien die Renvoo wil gebruiken voor praktijkgestuurde reminders, bevestigingen en veilige verplaatslinks in de eerste pilotfase.", + }, + cards: [ + { + title: "Korte versie", + body: + "Uw praktijk kan Renvoo gebruiken om afspraakbevestigingen, verplaatsingen en herstel van anders ongebruikte tijd te ondersteunen. Renvoo werkt daarbij namens de praktijk voor deze operationele berichten.", + }, + { + title: "Rolverdeling", + list: [ + "uw praktijk blijft verantwoordelijk voor de zorgrelatie en uw patiëntdata", + "Renvoo levert de operationele workflow als serviceprovider voor de praktijk", + "vragen over uw afspraak of privacyrechten horen in eerste instantie bij uw praktijk", + ], + }, + { + title: "Welke data hier bedoeld is", + list: [ + "uw naam en contactgegevens", + "uw afspraakmoment en afspraakstatus", + "bericht- en responsstatus die nodig is voor planningsoperatie", + ], + }, + { + title: "Wat dit niet is", + list: [ + "geen patiëntaccount of patiëntportaal", + "geen diagnosetool", + "geen behandeladvies of medische dienst", + ], + }, + { + title: "Implementatienotitie", + body: + "Vóór live pilotgebruik moeten praktijkspecifieke formuleringen, contactgegevens en goedgekeurde datacategorieën definitief worden gemaakt in de formele legal notice.", + }, + ], + }, + notFound: { + seo: { + title: "Renvoo | Pagina niet gevonden", + description: "De gevraagde Renvoo-pagina bestaat niet of is verplaatst.", + }, + hero: { + eyebrow: "404", + title: "Deze pagina bestaat niet meer.", + body: + "Ga terug naar de funnel en kies opnieuw of u eerst product, vertrouwen of direct het validatiegesprek wilt bekijken.", + }, + actions: [ + { page: "home", label: "Terug naar start" }, + { page: "pilot", label: "Naar planner" }, + ], + }, + }, + booking: { + labels: { + stepLabel: "Stap", + step1: "Context", + step2: "Voorkeuren", + step3: "Controleren", + statusReady: "Vul de praktijkcontext in om verder te gaan.", + statusStep2: "Kies uw voorkeursmomenten voor het gesprek.", + statusReview: "Controleer uw aanvraag en verstuur wanneer alles klopt.", + statusSuccess: "Uw aanvraag is lokaal opgeslagen voor de live handoff.", + summaryTitle: "Renvoo booking request", + submittedAt: "Ingediend op", + mode: "Modus", + contactName: "Naam", + contactEmail: "Zakelijk e-mailadres", + role: "Rol", + clinicName: "Praktijknaam", + city: "Stad", + clinicSize: "Praktijkgrootte", + primaryPain: "Grootste pijnpunt", + workflowNotes: "Huidige workflow of tooling", + meetingFormat: "Gesprekstype", + availabilityTitle: "Voorkeurstijden", + buttons: { + next: "Verder", + back: "Terug", + submit: "Verstuur aanvraag", + restart: "Nieuwe aanvraag", + copy: "Kopieer samenvatting", + download: "Download samenvatting", + }, + help: { + workflowNotes: + "Beschrijf kort uw huidige systeem of werkwijze… Bijvoorbeeld Exquise met handmatige reminders en losse terugbellijst.", + availability: "Kies minimaal één blok. Meer mag ook.", + }, + errors: { + required: "Dit veld is nodig om verder te gaan.", + email: "Gebruik een geldig zakelijk e-mailadres.", + tooShort: "Voeg iets meer context toe zodat de praktijkworkflow duidelijk wordt.", + availability: "Kies minimaal één voorkeurstijd.", + }, + previewMode: + "Live routing is nog niet gekoppeld. De planner slaat daarom een nette handoff lokaal op voor demo- en launchvoorbereiding.", + responseExpectation: + "Doel: na deze aanvraag volgt een bevestiging of korte afstemming om het gesprek definitief vast te zetten.", + successTitle: "Aanvraag klaar voor opvolging", + successBody: + "De planner heeft een schone handoff gemaakt. U kunt de samenvatting kopiëren of downloaden zolang live routing nog niet actief is.", + copySuccess: "Samenvatting gekopieerd.", + copyFallback: "Kopiëren lukte niet automatisch. Gebruik de downloadknop als fallback.", + downloadReady: "Samenvatting gedownload.", + }, + roles: [ + { value: "owner", label: "Praktijkhouder / eigenaar" }, + { value: "manager", label: "Praktijkmanager" }, + { value: "operations", label: "Operations / front-desk lead" }, + { value: "other", label: "Anders, maar betrokken bij planning" }, + ], + clinicSizes: [ + { value: "solo", label: "1 behandelkamer of solo-praktijk" }, + { value: "small", label: "2-4 behandelkamers" }, + { value: "mid", label: "5-8 behandelkamers" }, + { value: "group", label: "Meerdere locaties of grotere groep" }, + ], + primaryPains: [ + { value: "no-shows", label: "No-shows" }, + { value: "late-cancellations", label: "Late afzeggingen" }, + { value: "backfill", label: "Terugvullen van lege plekken" }, + { value: "admin-load", label: "Te veel handmatig front-desk werk" }, + ], + meetingFormats: [ + { value: "video", label: "Video call" }, + { value: "phone", label: "Telefonisch" }, + { value: "onsite", label: "Op locatie als het logisch is" }, + ], + availability: [ + { value: "tue-morning", label: "Dinsdag ochtend", detail: "09:00-11:30 CET" }, + { value: "tue-afternoon", label: "Dinsdag middag", detail: "13:00-16:00 CET" }, + { value: "wed-morning", label: "Woensdag ochtend", detail: "09:00-11:30 CET" }, + { value: "thu-afternoon", label: "Donderdag middag", detail: "13:30-16:30 CET" }, + { value: "fri-morning", label: "Vrijdag ochtend", detail: "09:00-11:00 CET" }, + ], + }, + }, + en: { + htmlLang: "en", + locale: "en-US", + languageLabel: "English", + switchLabel: "NL", + brandLine: "Operational SaaS for calmer schedule recovery", + nav: { + home: "Home", + product: "Product", + pilot: "Pilot", + trust: "Trust", + cta: "Book Meeting", + }, + footer: { + summary: + "Renvoo is an early-stage B2B clinic SaaS for Dutch dental clinics that aims to reduce no-shows, handle late cancellations earlier, and recover lost chair time without extra front-desk work.", + quickLinksTitle: "Funnel", + legalTitle: "Legal", + languageTitle: "Language", + legalLinks: [ + { page: "privacy", label: "Website privacy note" }, + { page: "patientNotice", label: "Patient message note" }, + ], + languageLinks: [{ lang: "nl", page: "home", label: "Nederlandse versie" }], + proofNote: "Built around pilot proof, not inflated claims.", + }, + pageNames: { + home: "Home", + product: "Product", + pilot: "Pilot", + trust: "Trust", + privacy: "Privacy", + patientNotice: "Patient note", + notFound: "Not found", + }, + pages: { + home: { + seo: { + title: "Renvoo | Reduce no-shows. Recover lost appointments.", + description: + "Renvoo helps Dutch dental clinics spot no-show risk earlier, handle late cancellations sooner, and recover empty chair time without adding front-desk work.", + }, + hero: { + eyebrow: "For Dutch dental clinics", + title: "Reduce no-shows. Recover lost appointments.", + body: + "Renvoo helps Dutch dental clinics spot no-show risk earlier, handle late cancellations sooner, and recover empty chair time without adding front-desk work.", + primaryCta: "Request a Validation Meeting", + secondaryCta: "See How It Works", + badges: [ + "No patient accounts in v1", + "Administrative-data boundary", + "Pilot-first, not platform-first", + ], + operatorCard: { + title: "What operators feel first", + items: [ + "The schedule looks full, but chair time still feels fragile.", + "Late notice leaves too little time to recover the slot well.", + "Front-desk teams absorb the uncertainty manually.", + ], + }, + }, + pains: { + eyebrow: "The real problem", + title: "The damage is not only the no-show itself.", + intro: + "Loss compounds when empty chair time, late signals, and recovery work all hit the same day. That is why reminders alone are not enough.", + items: [ + { + title: "Empty chair time becomes immediate revenue loss", + body: "A scheduled appointment only creates value if the patient actually shows up.", + }, + { + title: "Late cancellations can still be expensive even when polite", + body: "A patient may warn the clinic, and the slot can still be too late to save.", + }, + { + title: "Admin work grows around uncertainty", + body: "Confirmations, chasing replies, rescheduling, and backfill all create extra operational load.", + }, + ], + }, + workflowTeaser: { + eyebrow: "How Renvoo thinks", + title: "From generic reminders to schedule resilience", + intro: + "The pilot is designed as a lightweight operations layer. First surface risk, then confirm or reschedule more intentionally, then recover capacity faster.", + steps: [ + { + label: "Connect", + body: "Start with scheduling and communication data rather than clinical data.", + }, + { + label: "Detect", + body: "Surface higher-risk appointments earlier than a generic reminder flow does.", + }, + { + label: "Confirm", + body: "Give staff a clearer moment to confirm, reschedule, or intervene earlier.", + }, + { + label: "Replace", + body: "Help the clinic recover lost capacity while keeping schedule control inside the practice.", + }, + ], + }, + proof: { + eyebrow: "Why this feels credible", + title: "Built to validate with clinics, not to claim too much too early.", + cards: [ + { + title: "Proof posture", + body: "The current case is grounded in clear operating logic and scenario math. The pilot exists to prove real reduction, recovery, and admin relief with clinic workflows.", + }, + { + title: "Data boundary", + body: "Renvoo is deliberately framed around non-clinical scheduling and communication data, not diagnoses, notes, or treatment decisions.", + }, + ], + }, + paths: { + eyebrow: "Choose your route", + title: "Not every buyer wants the same thing first.", + items: [ + { + title: "Understand the product first", + body: "See the full Connect → Detect → Confirm → Replace workflow and why it is different from reminder software.", + page: "product", + label: "Go to Product", + }, + { + title: "See the pilot path first", + body: "Review the validation meeting, pilot framing, and local booking flow.", + page: "pilot", + label: "Go to Pilot", + }, + { + title: "Check trust and data posture first", + body: "Review controller/processor boundaries, FAQs, materials, and legal notes.", + page: "trust", + label: "Go to Trust", + }, + ], + }, + closing: { + eyebrow: "Next step", + title: "The next step is small and practical.", + body: + "We are looking for a short validation meeting with a dental clinic owner or practice manager to review the current no-show and cancellation workflow.", + primaryCta: "Book the Meeting", + secondaryCta: "See the Pilot First", + }, + }, + product: { + seo: { + title: "Renvoo | Product workflow for no-show prevention", + description: + "See how Renvoo helps Dutch dental clinics with an operational workflow for detecting risk, confirming earlier, and recovering empty chair time.", + }, + hero: { + eyebrow: "Product", + title: "Not another reminder tool.", + body: + "Renvoo is meant to be an operations layer around attendance uncertainty. The point is not more messages. The point is earlier visibility, better intervention timing, and calmer schedule recovery.", + primaryCta: "Book a Validation Meeting", + secondaryCta: "See Trust & Data", + }, + comparison: { + eyebrow: "Before and after", + title: "What changes operationally", + beforeTitle: "Without Renvoo", + afterTitle: "With Renvoo", + before: [ + "Most appointments pass through roughly the same reminder logic.", + "Risk often becomes obvious only after the slot is already fragile.", + "Front-desk teams react manually to gaps, no-response, and late notice.", + "Backfill relies on memory, lists, and time pressure.", + ], + after: [ + "Staff can see earlier which appointments need more attention.", + "Confirming and rescheduling can happen more intentionally, not only generically.", + "Recovery work becomes a clearer workflow instead of scattered firefighting.", + "The clinic keeps final control over schedule changes and patient touchpoints.", + ], + }, + workflow: { + eyebrow: "The four steps", + title: "Connect. Detect. Confirm. Replace.", + intro: + "The architecture is deliberately small for early pilots: lightweight intake, operational visibility, and no patient-platform behavior in v1.", + steps: [ + { + title: "Connect", + body: "Start with scheduling, appointment, and communication fields that are operationally useful enough to surface risk.", + bullets: [ + "Scheduling and status fields first", + "No diagnoses or clinical notes required", + "Lower-friction path into pilot validation", + ], + }, + { + title: "Detect", + body: "Surface higher-risk appointments earlier than a standard reminder program would.", + bullets: [ + "Appointment-level risk, not one generic flow", + "Operator-facing, not clinical", + "Built for explainable use in practice", + ], + }, + { + title: "Confirm", + body: "Give the team a clearer moment to confirm, reschedule, or intervene before chair time is lost.", + bullets: [ + "Supports practical staff follow-up", + "Fits beside existing systems", + "Helps address no-response sooner", + ], + }, + { + title: "Replace", + body: "When a slot opens up, the workflow helps the clinic think about recovery earlier while keeping actual schedule mutation under clinic or EHR control.", + bullets: [ + "No autonomous schedule mutations in v1", + "Recovery over theater", + "Practice or EHR keeps final control", + ], + }, + ], + }, + fit: { + eyebrow: "Low friction by design", + title: "Deliberately simple for the first clinic pilots", + items: [ + { + title: "Non-clinical data boundary", + body: "Administrative scheduling and communication data comes first. That keeps the scope narrow and operational.", + }, + { + title: "CSV-first or read-only intake", + body: "The first step does not need to become a heavy integration project before the pilot case is clear.", + }, + { + title: "No patient accounts in v1", + body: "Patient interaction stays in clinic-triggered messages and secure links, not in a new consumer product.", + }, + ], + }, + whyDental: { + eyebrow: "Why dental first", + title: "The wedge is narrow, but sharp.", + items: [ + "Empty chair time is economically visible immediately.", + "Schedules are dense and hard to recover once notice is late.", + "Owners and practice managers feel the pain directly in daily operations.", + ], + }, + closing: { + eyebrow: "Next step", + title: "Does this look like a realistic workflow for your clinic?", + body: + "If yes, the best next step is a short meeting where we do not demo first. We review the current no-show, cancellation, and recovery workflow.", + primaryCta: "Request the Meeting", + secondaryCta: "See the Pilot", + }, + }, + pilot: { + seo: { + title: "Renvoo | Request a validation meeting", + description: + "Request a short validation meeting and share when your dental clinic would prefer to talk about no-shows, late cancellations, and capacity recovery.", + }, + hero: { + eyebrow: "Pilot", + title: "Request a validation meeting", + body: + "The first conversation is not a heavy product demo. It is a short operator review of the current no-show, confirmation, and recovery workflow to see whether a lightweight pilot is worth it.", + primaryCta: "Start the Planner", + secondaryCta: "See the Product First", + chips: ["15 minutes", "Founder-led", "No live calendar sync yet"], + }, + agenda: { + eyebrow: "What the first meeting should do", + title: "Practical enough for a real yes or no", + items: [ + { + title: "Workflow review", + body: "Walk through how no-shows, late cancellations, and recovery currently get handled.", + }, + { + title: "Signals and constraints", + body: "Review where the clinic gets visibility too late and what data or tooling already exists.", + }, + { + title: "Pilot fit", + body: "Decide whether the operational pain is strong enough for a measurable pilot.", + }, + ], + }, + bookingIntro: { + eyebrow: "Local booking flow", + title: "Pick preferred time blocks in a lightweight Calendly-style flow", + body: + "This planner currently works as a request flow backed by local availability blocks. It stores a clean handoff while live routing is still being connected.", + support: [ + "Name and business contact details", + "Short context about clinic and current workflow", + "Preferred meeting format and time blocks", + ], + }, + pricing: { + eyebrow: "Pricing stays secondary", + title: "Preliminary pilot pricing", + body: sharedPricingLine, + note: + "The pricing line is here to frame budget fit, not to force a heavy commitment before validation.", + }, + plannerAside: { + title: "What happens after submit", + items: [ + "A local success state with a clean request summary", + "A copyable handoff while live routing is not yet active", + "A tidy summary for internal follow-up or launch prep", + ], + responseExpectation: + "Target response: confirmation of the meeting or a short follow-up to lock the final time.", + }, + fallback: { + title: "Not ready to book yet?", + body: + "Review the trust page first for the data boundary, proof posture, FAQ, materials, and legal notes before returning to the planner.", + label: "See Trust First", + }, + }, + trust: { + seo: { + title: "Renvoo | Trust, data boundary, and clinic materials", + description: + "Review Renvoo's trust layer: proof posture, controller/processor split, FAQ, clinic materials, and legal notes for the first dental pilots.", + }, + hero: { + eyebrow: "Trust", + title: "Built to validate with clinics, not to overclaim.", + body: + "This page is for the skeptical route: what Renvoo does and does not claim, what data is and is not in scope, and what clinic materials already exist for real conversations.", + primaryCta: "Go to the Planner", + secondaryCta: "See the Product", + }, + boundary: { + eyebrow: "Data boundary & role split", + title: "Narrow enough to stay workable", + items: [ + { + title: "Non-clinical scope", + body: "Renvoo is designed around administrative scheduling and communication data, not diagnoses, treatment decisions, or clinical notes.", + }, + { + title: "Clinic stays controller", + body: "For patient and appointment operational data, the clinic stays controller and Renvoo acts as processor under clinic instruction.", + }, + { + title: "No patient platform in v1", + body: "The first pilots stay away from patient accounts, marketplace behavior, and unnecessary consumer-facing product sprawl.", + }, + ], + }, + proof: { + eyebrow: "Proof posture", + title: "What can be said honestly today", + items: [ + "The current case is grounded in operating logic and scenario math, not invented traction.", + "The pilot exists to validate real reduction, recovery, and admin relief with live clinic workflows.", + "The founder-led motion is meant to create workflow proof, not a generic self-serve funnel.", + ], + founderLine: sharedFounderLine, + }, + faq: { + eyebrow: "Frequent questions", + title: "The main objections without startup theater", + items: [ + { + question: "We already send reminders. What changes then?", + answer: + "That makes sense. Reminders are part of the baseline. The gap Renvoo is focused on is what happens when reminders are not enough: no response, late response, and manual recovery work around lost capacity.", + }, + { + question: "We do not want another heavy system.", + answer: + "That is exactly why the current direction is low friction: CSV-first or read-only onboarding, no patient accounts in v1, and a short workflow validation before anything heavier.", + }, + { + question: "We are careful with patient data.", + answer: + "That caution is right. Renvoo is designed around non-clinical administrative scheduling and communication data, not diagnoses, clinical notes, or treatment decisions.", + }, + { + question: "Do patients need to create an account?", + answer: + "No. The v1 posture is clinic SaaS, not a patient platform. Patient interaction should happen through clinic-triggered reminders, confirmations, and secure links.", + }, + { + question: "We are too busy to change workflow now.", + answer: + "That is part of the problem Renvoo is trying to solve. The first meeting is not a heavy rollout. It is there to see whether the operational pain is strong enough for a lightweight pilot.", + }, + { + question: "How do we know this is worth it?", + answer: + "That is the central pilot question. The current case is logical, but the pilot should validate whether no-show reduction, recovery, and admin relief become visible in your clinic.", + }, + ], + }, + materials: { + eyebrow: "Clinic materials", + title: "The outreach assets are already concrete enough for real conversations", + intro: + "The website is not the only artifact. This funnel sits alongside the same one-pager, deck, and operator framing already used in founder-led outreach.", + downloads: sharedDownloads, + previews: [ + { + src: "assets/previews/one-pager-preview.png", + alt: "Preview of the Renvoo clinic one-pager", + caption: "One-pager preview", + width: 1080, + height: 1528, + }, + { + src: "assets/previews/deck-slide-01.png", + alt: "Preview of the first Renvoo clinic deck slide", + caption: "Deck cover slide", + width: 1280, + height: 720, + }, + { + src: "assets/previews/deck-slide-07.png", + alt: "Preview of the Renvoo clinic deck solution slide", + caption: "Deck solution slide", + width: 1280, + height: 720, + }, + ], + }, + legal: { + eyebrow: "Legal base layer", + title: "Narrow launch posture", + body: + "The legal pages stay deliberately simple: website privacy, patient message note, and pointers into the broader legal pack for controller/processor posture, DPIA-lite, and contract templates.", + links: [ + { page: "privacy", label: "Website privacy note" }, + { page: "patientNotice", label: "Patient message note" }, + ], + }, + closing: { + eyebrow: "Next step", + title: "If the posture feels right, request the short meeting.", + body: + "The funnel is intentionally simple: understand the workflow, validate the trust layer, and only then request the conversation.", + primaryCta: "Book the Validation Meeting", + secondaryCta: "See the Pilot Flow", + }, + }, + privacy: { + seo: { + title: "Renvoo | Website privacy note", + description: + "Pre-launch privacy note for Renvoo's clinic-SaaS website, lead capture, and pilot outreach workflow.", + }, + hero: { + eyebrow: "Pre-launch note", + title: "Website privacy note", + body: + "This page describes the narrow privacy posture currently intended for Renvoo's own website, clinic lead capture, and pilot outreach flow. The final `Renvoo B.V.` contact details and retention choices still need to be added before launch.", + }, + cards: [ + { + title: "What this page covers", + body: + "This note covers Renvoo's own website, clinic lead capture, and business-development communication. It does not replace a clinic's own patient privacy notice.", + }, + { + title: "Current product posture", + list: [ + "Renvoo is being launched as B2B clinic SaaS for Dutch private clinics, starting with dental clinics.", + "The site is informational and lead-generation oriented.", + "The site is not a patient portal or patient account product.", + ], + }, + { + title: "Data Renvoo may collect directly", + list: [ + "name, clinic, and business contact details you send directly", + "messages, meeting requests, and notes from pilot conversations", + "basic technical and security information needed to operate the site", + ], + }, + { + title: "Cookies", + body: + "The intended default launch posture is no non-essential cookies by default. If that changes, an explicit consent flow should be added first.", + }, + { + title: "Why the data is used", + list: [ + "to answer clinic inquiries", + "to schedule validation or pilot conversations", + "to secure and operate the website", + "to handle ordinary business administration", + ], + }, + { + title: "Implementation note", + body: + "Before public launch, replace this pre-launch note with the finalized website privacy notice from the legal pack, including legal contact details, retention choices, and any approved vendor list.", + }, + ], + }, + patientNotice: { + seo: { + title: "Renvoo | Patient message note", + description: + "Pre-launch patient message note for clinic-triggered reminders, confirmations, and secure rescheduling links in the first Renvoo pilot phase.", + }, + hero: { + eyebrow: "Pre-launch note", + title: "Patient message note", + body: + "This page shows the short-form patient message note Renvoo plans to use for clinic-triggered reminders, confirmations, and secure rescheduling links during the first pilot phase.", + }, + cards: [ + { + title: "Short version", + body: + "Your clinic may use Renvoo to help manage appointment confirmations, rescheduling, and recovery of otherwise unused appointment time. Renvoo acts on behalf of the clinic for those operational messages.", + }, + { + title: "Role split", + list: [ + "your clinic stays responsible for your care relationship and patient data", + "Renvoo provides the operational workflow as a service provider to the clinic", + "questions about your appointment or privacy rights should first go to your clinic", + ], + }, + { + title: "What data this is meant to use", + list: [ + "your name and contact details", + "your appointment time and appointment status", + "message and response status needed for scheduling operations", + ], + }, + { + title: "What this is not", + list: [ + "not a patient account or patient portal", + "not a diagnosis tool", + "not a treatment recommendation or medical advice service", + ], + }, + { + title: "Implementation note", + body: + "Before live pilot use, clinic-specific wording, contact details, and approved data categories should be finalized in the formal legal notice.", + }, + ], + }, + notFound: { + seo: { + title: "Renvoo | Page not found", + description: "The requested Renvoo page does not exist or has moved.", + }, + hero: { + eyebrow: "404", + title: "This page no longer exists.", + body: + "Go back into the funnel and choose whether you want to review the product, the trust layer, or the meeting planner next.", + }, + actions: [ + { page: "home", label: "Back to Home" }, + { page: "pilot", label: "Go to Planner" }, + ], + }, + }, + booking: { + labels: { + stepLabel: "Step", + step1: "Context", + step2: "Preferences", + step3: "Review", + statusReady: "Add the clinic context first to continue.", + statusStep2: "Choose preferred meeting timing next.", + statusReview: "Review the request and submit when everything looks right.", + statusSuccess: "Your request has been saved locally for the live handoff.", + summaryTitle: "Renvoo booking request", + submittedAt: "Submitted at", + mode: "Mode", + contactName: "Name", + contactEmail: "Business email", + role: "Role", + clinicName: "Clinic name", + city: "City", + clinicSize: "Clinic size", + primaryPain: "Primary pain point", + workflowNotes: "Current workflow or tooling", + meetingFormat: "Meeting format", + availabilityTitle: "Preferred time blocks", + buttons: { + next: "Continue", + back: "Back", + submit: "Submit Request", + restart: "Start Again", + copy: "Copy Summary", + download: "Download Summary", + }, + help: { + workflowNotes: + "Briefly describe the current system or process… For example Exquise with manual reminders and a separate backfill list.", + availability: "Choose at least one time block. More is fine.", + }, + errors: { + required: "This field is needed to continue.", + email: "Use a valid business email address.", + tooShort: "Add a bit more context so the clinic workflow is clear.", + availability: "Choose at least one preferred time block.", + }, + previewMode: + "Live routing is not connected yet. The planner therefore stores a clean local handoff for launch prep and demo use.", + responseExpectation: + "Target response: confirmation of the meeting or a short follow-up to lock the final time.", + successTitle: "Request ready for follow-up", + successBody: + "The planner has created a clean handoff. You can copy or download the summary while live routing is still inactive.", + copySuccess: "Summary copied.", + copyFallback: "Automatic copy did not work. Use the download button as fallback.", + downloadReady: "Summary downloaded.", + }, + roles: [ + { value: "owner", label: "Clinic owner" }, + { value: "manager", label: "Practice manager" }, + { value: "operations", label: "Operations or front-desk lead" }, + { value: "other", label: "Other planning stakeholder" }, + ], + clinicSizes: [ + { value: "solo", label: "1 chair or solo clinic" }, + { value: "small", label: "2-4 chairs" }, + { value: "mid", label: "5-8 chairs" }, + { value: "group", label: "Multi-location or larger group" }, + ], + primaryPains: [ + { value: "no-shows", label: "No-shows" }, + { value: "late-cancellations", label: "Late cancellations" }, + { value: "backfill", label: "Backfilling open slots" }, + { value: "admin-load", label: "Too much manual front-desk work" }, + ], + meetingFormats: [ + { value: "video", label: "Video call" }, + { value: "phone", label: "Phone call" }, + { value: "onsite", label: "On-site if it makes sense" }, + ], + availability: [ + { value: "tue-morning", label: "Tuesday morning", detail: "09:00-11:30 CET" }, + { value: "tue-afternoon", label: "Tuesday afternoon", detail: "13:00-16:00 CET" }, + { value: "wed-morning", label: "Wednesday morning", detail: "09:00-11:30 CET" }, + { value: "thu-afternoon", label: "Thursday afternoon", detail: "13:30-16:30 CET" }, + { value: "fri-morning", label: "Friday morning", detail: "09:00-11:00 CET" }, + ], + }, + }, +}; + +export const pageOrder = ["home", "product", "pilot", "trust", "privacy", "patientNotice", "notFound"]; + +export const pageFileNames = { + home: "index.html", + product: "product.html", + pilot: "pilot.html", + trust: "trust.html", + privacy: "privacy.html", + patientNotice: "patient-notice.html", + notFound: "404.html", +}; diff --git a/src/site/lib/site-render.js b/src/site/lib/site-render.js new file mode 100644 index 0000000..ec0673d --- /dev/null +++ b/src/site/lib/site-render.js @@ -0,0 +1,834 @@ +import { pageFileNames, pageOrder, siteContent } from "./site-content.js"; + +const pageSequence = [ + { lang: "nl", page: "home" }, + { lang: "nl", page: "product" }, + { lang: "nl", page: "pilot" }, + { lang: "nl", page: "trust" }, + { lang: "nl", page: "privacy" }, + { lang: "nl", page: "patientNotice" }, + { lang: "nl", page: "notFound" }, + { lang: "en", page: "home" }, + { lang: "en", page: "product" }, + { lang: "en", page: "pilot" }, + { lang: "en", page: "trust" }, + { lang: "en", page: "privacy" }, + { lang: "en", page: "patientNotice" }, + { lang: "en", page: "notFound" }, +]; + +function prefixForLang(lang) { + return lang === "en" ? ".." : "."; +} + +function outputPathFor(lang, page) { + const fileName = pageFileNames[page]; + return lang === "en" ? `en/${fileName}` : fileName; +} + +function routeFor(currentLang, targetLang, page, hash = "") { + const fileName = pageFileNames[page]; + const filePath = + currentLang === targetLang + ? `./${fileName}` + : currentLang === "en" + ? `../${fileName}` + : `./en/${fileName}`; + + return `${filePath}${hash}`; +} + +function assetPath(prefix, relativePath) { + return `${prefix}/${relativePath}`; +} + +function renderDocument({ lang, page, content }) { + const prefix = prefixForLang(lang); + const routes = { + home: routeFor(lang, lang, "home"), + product: routeFor(lang, lang, "product"), + pilot: routeFor(lang, lang, "pilot"), + trust: routeFor(lang, lang, "trust"), + booking: routeFor(lang, lang, "pilot", "#booking"), + privacy: routeFor(lang, lang, "privacy"), + patientNotice: routeFor(lang, lang, "patientNotice"), + switch: routeFor(lang, lang === "nl" ? "en" : "nl", page), + }; + const pageCopy = content.pages[page]; + const switchLabel = content.switchLabel; + const switchLang = lang === "nl" ? "en" : "nl"; + const bodyClass = `page-${page} lang-${lang}`; + + return ` + + + + + ${pageCopy.seo.title} + + + + + + + + + + + + + + + + + + + + + + + ${renderHeader(content, page, routes)} +
+ ${renderPageBody({ lang, page, content, routes, prefix })} +
+ ${renderFooter(content, lang, page)} + + + +`; +} + +function renderHeader(content, page, routes) { + const navItems = ["home", "product", "pilot", "trust"] + .map((navPage) => { + const isActive = page === navPage ? "is-active" : ""; + return `${content.nav[navPage]}`; + }) + .join(""); + + const menuLabel = content.htmlLang === "nl" ? "Open navigatie" : "Open navigation"; + + return ``; +} + +function renderFooter(content, lang, page) { + const quickLinks = ["home", "product", "pilot", "trust"] + .map( + (footerPage) => + `
  • ${content.pageNames[footerPage]}
  • `, + ) + .join(""); + + const legalLinks = content.footer.legalLinks + .map( + (link) => + `
  • ${link.label}
  • `, + ) + .join(""); + + const languageLinks = content.footer.languageLinks + .map((link) => { + const targetPage = page === "notFound" ? "notFound" : link.page; + return `
  • ${link.label}
  • `; + }) + .join(""); + + return `
    + + +
    `; +} + +function renderPageBody({ lang, page, content, routes, prefix }) { + switch (page) { + case "home": + return renderHomePage(content, routes); + case "product": + return renderProductPage(content, routes); + case "pilot": + return renderPilotPage(content, routes); + case "trust": + return renderTrustPage(content, routes, prefix); + case "privacy": + return renderLegalPage(content.pages.privacy); + case "patientNotice": + return renderLegalPage(content.pages.patientNotice); + case "notFound": + return renderNotFoundPage(content, routes); + default: + return ""; + } +} + +function renderSectionHeading(eyebrow, title, intro = "") { + return `
    +

    ${eyebrow}

    +

    ${title}

    + ${intro ? `

    ${intro}

    ` : ""} +
    `; +} + +function renderHomePage(content, routes) { + const copy = content.pages.home; + + return ` +
    +
    +
    +

    ${copy.hero.eyebrow}

    +

    ${copy.hero.title}

    +

    ${copy.hero.body}

    + +
      + ${copy.hero.badges.map((badge) => `
    • ${badge}
    • `).join("")} +
    +
    + +
    +
    + +
    + ${renderSectionHeading(copy.pains.eyebrow, copy.pains.title, copy.pains.intro)} +
    + ${copy.pains.items + .map( + (item, index) => `
    +

    ${item.title}

    +

    ${item.body}

    +
    `, + ) + .join("")} +
    +
    + +
    + ${renderSectionHeading(copy.workflowTeaser.eyebrow, copy.workflowTeaser.title, copy.workflowTeaser.intro)} +
    + ${copy.workflowTeaser.steps + .map( + (step, index) => `
    + ${step.label} +

    ${step.body}

    +
    `, + ) + .join("")} +
    +
    + +
    + ${renderSectionHeading(copy.proof.eyebrow, copy.proof.title)} +
    + ${copy.proof.cards + .map( + (card, index) => `
    +

    ${card.title}

    +

    ${card.body}

    +
    `, + ) + .join("")} +
    +
    + +
    + ${renderSectionHeading(copy.paths.eyebrow, copy.paths.title)} +
    + ${copy.paths.items + .map( + (item, index) => ``, + ) + .join("")} +
    +
    + + ${renderClosingBand(copy.closing, routes.booking, routes.pilot)} + `; +} + +function renderProductPage(content, routes) { + const copy = content.pages.product; + + return ` +
    +
    +
    +

    ${copy.hero.eyebrow}

    +

    ${copy.hero.title}

    +

    ${copy.hero.body}

    + +
    +
    +
    + +
    + ${renderSectionHeading(copy.comparison.eyebrow, copy.comparison.title)} +
    +
    +

    ${copy.comparison.beforeTitle}

    +
      + ${copy.comparison.before.map((item) => `
    • ${item}
    • `).join("")} +
    +
    +
    +

    ${copy.comparison.afterTitle}

    +
      + ${copy.comparison.after.map((item) => `
    • ${item}
    • `).join("")} +
    +
    +
    +
    + +
    + ${renderSectionHeading(copy.workflow.eyebrow, copy.workflow.title, copy.workflow.intro)} +
    + ${copy.workflow.steps + .map( + (step, index) => `
    +
    + 0${index + 1} +

    ${step.title}

    +
    +

    ${step.body}

    +
      + ${step.bullets.map((bullet) => `
    • ${bullet}
    • `).join("")} +
    +
    `, + ) + .join("")} +
    +
    + +
    + ${renderSectionHeading(copy.fit.eyebrow, copy.fit.title)} +
    + ${copy.fit.items + .map( + (item, index) => `
    +

    ${item.title}

    +

    ${item.body}

    +
    `, + ) + .join("")} +
    +
    + +
    + ${renderSectionHeading(copy.whyDental.eyebrow, copy.whyDental.title)} +
    +
      + ${copy.whyDental.items.map((item) => `
    1. ${item}
    2. `).join("")} +
    +
    +
    + + ${renderClosingBand(copy.closing, routes.booking, routes.pilot)} + `; +} + +function renderPilotPage(content, routes) { + const copy = content.pages.pilot; + const booking = content.booking; + + return ` +
    +
    +
    +

    ${copy.hero.eyebrow}

    +

    ${copy.hero.title}

    +

    ${copy.hero.body}

    + +
      + ${copy.hero.chips.map((chip) => `
    • ${chip}
    • `).join("")} +
    +
    +
    +
    + +
    + ${renderSectionHeading(copy.agenda.eyebrow, copy.agenda.title)} +
    + ${copy.agenda.items + .map( + (item, index) => `
    +

    ${item.title}

    +

    ${item.body}

    +
    `, + ) + .join("")} +
    +
    + +
    +
    + ${renderSectionHeading(copy.bookingIntro.eyebrow, copy.bookingIntro.title, copy.bookingIntro.body)} +
      + ${copy.bookingIntro.support.map((item) => `
    • ${item}
    • `).join("")} +
    +
    + +
    + +
    + ${renderSectionHeading(content.nav.cta, copy.hero.title, booking.labels.previewMode)} +
    + ${renderBookingForm(content)} + +
    +
    + +
    +
    +
    +

    ${copy.fallback.title}

    +

    ${copy.fallback.body}

    +
    + ${copy.fallback.label} +
    +
    + `; +} + +function renderBookingForm(content) { + const booking = content.booking; + const labels = booking.labels; + + return `
    +
    +
      +
    1. ${labels.stepLabel} 1${labels.step1}
    2. +
    3. ${labels.stepLabel} 2${labels.step2}
    4. +
    5. ${labels.stepLabel} 3${labels.step3}
    6. +
    +

    ${labels.statusReady}

    + +
    +
    + ${renderInputField({ + label: labels.contactName, + id: "contactName", + name: "contactName", + type: "text", + autocomplete: "name", + placeholder: content.htmlLang === "nl" ? "Bijvoorbeeld Mohamed Ibrahim…" : "For example Mohamed Ibrahim…", + })} + ${renderInputField({ + label: labels.contactEmail, + id: "contactEmail", + name: "contactEmail", + type: "email", + autocomplete: "email", + placeholder: content.htmlLang === "nl" ? "naam@praktijk.nl…" : "name@clinic.com…", + spellcheck: false, + inputmode: "email", + })} + ${renderSelectField({ + label: labels.role, + id: "role", + name: "role", + placeholder: content.htmlLang === "nl" ? "Kies uw rol…" : "Choose your role…", + options: booking.roles, + })} + ${renderInputField({ + label: labels.clinicName, + id: "clinicName", + name: "clinicName", + type: "text", + autocomplete: "organization", + placeholder: content.htmlLang === "nl" ? "Naam van de praktijk…" : "Clinic name…", + })} + ${renderInputField({ + label: labels.city, + id: "city", + name: "city", + type: "text", + autocomplete: "address-level2", + placeholder: content.htmlLang === "nl" ? "Bijvoorbeeld Eindhoven…" : "For example Eindhoven…", + })} + ${renderSelectField({ + label: labels.clinicSize, + id: "clinicSize", + name: "clinicSize", + placeholder: content.htmlLang === "nl" ? "Kies de schaal…" : "Choose the clinic size…", + options: booking.clinicSizes, + })} + ${renderSelectField({ + label: labels.primaryPain, + id: "primaryPain", + name: "primaryPain", + placeholder: content.htmlLang === "nl" ? "Kies het grootste pijnpunt…" : "Choose the main pain point…", + options: booking.primaryPains, + })} +
    + ${renderTextareaField({ + label: labels.workflowNotes, + id: "workflowNotes", + name: "workflowNotes", + placeholder: labels.help.workflowNotes, + rows: 4, + })} +
    + + + + + +
    + + + +
    + +

    ${labels.previewMode}

    +
    + + +
    `; +} + +function renderInputField({ + label, + id, + name, + type, + autocomplete, + placeholder, + spellcheck, + inputmode, +}) { + return `
    + + +

    +
    `; +} + +function renderSelectField({ label, id, name, placeholder, options }) { + return `
    + + +

    +
    `; +} + +function renderTextareaField({ label, id, name, placeholder, rows }) { + return `
    + + +

    ${placeholder}

    +

    +
    `; +} + +function renderTrustPage(content, routes, prefix) { + const copy = content.pages.trust; + + return ` +
    +
    +
    +

    ${copy.hero.eyebrow}

    +

    ${copy.hero.title}

    +

    ${copy.hero.body}

    + +
    +
    +
    + +
    + ${renderSectionHeading(copy.boundary.eyebrow, copy.boundary.title)} +
    + ${copy.boundary.items + .map( + (item, index) => `
    +

    ${item.title}

    +

    ${item.body}

    +
    `, + ) + .join("")} +
    +
    + +
    + ${renderSectionHeading(copy.proof.eyebrow, copy.proof.title)} +
    +
    +
      + ${copy.proof.items.map((item) => `
    • ${item}
    • `).join("")} +
    +
    +
    +

    Founder

    +

    ${copy.proof.founderLine}

    +
    +
    +
    + +
    + ${renderSectionHeading(copy.faq.eyebrow, copy.faq.title)} +
    + ${copy.faq.items + .map( + (item) => `
    + ${item.question} +

    ${item.answer}

    +
    `, + ) + .join("")} +
    +
    + +
    + ${renderSectionHeading(copy.materials.eyebrow, copy.materials.title, copy.materials.intro)} +
    +
    +
    + ${copy.materials.downloads + .map( + (download) => ``, + ) + .join("")} +
    +
    +
    + ${copy.materials.previews + .map( + (preview) => `
    + ${preview.alt} +
    ${preview.caption}
    +
    `, + ) + .join("")} +
    +
    +
    + + + + ${renderClosingBand(copy.closing, routes.booking, routes.pilot)} + `; +} + +function renderLegalPage(copy) { + return ` + + + + `; +} + +function renderNotFoundPage(content, routes) { + const copy = content.pages.notFound; + return ` +
    +
    +
    +

    ${copy.hero.eyebrow}

    +

    ${copy.hero.title}

    +

    ${copy.hero.body}

    +
    + ${copy.actions + .map( + (action, index) => `${action.label}`, + ) + .join("")} +
    +
    +
    +
    + `; +} + +function renderClosingBand(copy, primaryHref, secondaryHref) { + return `
    +
    +
    +

    ${copy.eyebrow ?? "Next step"}

    +

    ${copy.title}

    +

    ${copy.body}

    +
    + +
    +
    `; +} + +export function renderAllPages() { + return pageSequence.map(({ lang, page }) => ({ + path: outputPathFor(lang, page), + html: renderDocument({ + lang, + page, + content: siteContent[lang], + }), + })); +} + +export function getPagePaths() { + return pageSequence.map(({ lang, page }) => outputPathFor(lang, page)); +} + +export { outputPathFor, pageOrder, routeFor }; diff --git a/src/site/main.js b/src/site/main.js index 0db31ef..3bae0d0 100644 --- a/src/site/main.js +++ b/src/site/main.js @@ -1,55 +1,90 @@ -const header = document.querySelector("[data-header]"); -const navToggle = document.querySelector("[data-nav-toggle]"); -const siteNav = document.querySelector("[data-site-nav]"); -const revealElements = document.querySelectorAll(".reveal"); -const yearSlot = document.querySelector("[data-year]"); -const navLinks = siteNav?.querySelectorAll("a") ?? []; +import { + BOOKING_STORAGE_KEY, + collectBookingValues, + createBookingPayload, + createDownloadFile, + createPlainTextSummary, + getFirstInvalidField, + persistBookingRequest, + validateBookingStep, +} from "./lib/booking.js"; +import { siteContent } from "./lib/site-content.js"; -const syncHeaderState = () => { - if (!header) { - return; - } - - header.classList.toggle("is-scrolled", window.scrollY > 12); -}; +const body = document.body; +const currentLang = body.dataset.lang === "en" ? "en" : "nl"; +const copy = siteContent[currentLang]; +const yearSlot = document.querySelector("[data-year]"); if (yearSlot) { yearSlot.textContent = String(new Date().getFullYear()); } -syncHeaderState(); -window.addEventListener("scroll", syncHeaderState, { passive: true }); +body.classList.add("js-ready"); +requestAnimationFrame(() => { + body.classList.add("is-loaded"); +}); + +setupHeader(); +setupRevealMotion(); +setupTracking(); +setupBookingFlow(); + +function setupHeader() { + const header = document.querySelector("[data-header]"); + const navToggle = document.querySelector("[data-nav-toggle]"); + const navShell = document.querySelector("[data-site-nav-shell]"); + + const syncHeaderState = () => { + if (!header) { + return; + } + + header.classList.toggle("is-scrolled", window.scrollY > 18); + }; -const setNavState = (isOpen) => { - if (!(navToggle instanceof HTMLButtonElement) || !(siteNav instanceof HTMLElement)) { + syncHeaderState(); + window.addEventListener("scroll", syncHeaderState, { passive: true }); + + if (!(navToggle instanceof HTMLButtonElement) || !(navShell instanceof HTMLElement)) { return; } - navToggle.setAttribute("aria-expanded", String(isOpen)); - siteNav.classList.toggle("is-open", isOpen); - document.body.classList.toggle("nav-open", isOpen); -}; + const setNavState = (isOpen) => { + navToggle.setAttribute("aria-expanded", String(isOpen)); + navShell.classList.toggle("is-open", isOpen); + body.classList.toggle("nav-open", isOpen); + }; -if (navToggle instanceof HTMLButtonElement && siteNav instanceof HTMLElement) { navToggle.addEventListener("click", () => { - const isOpen = navToggle.getAttribute("aria-expanded") === "true"; - setNavState(!isOpen); + const nextState = navToggle.getAttribute("aria-expanded") !== "true"; + setNavState(nextState); }); - navLinks.forEach((link) => { - link.addEventListener("click", () => { - setNavState(false); - }); + navShell.querySelectorAll("a").forEach((link) => { + link.addEventListener("click", () => setNavState(false)); }); window.addEventListener("resize", () => { - if (window.innerWidth > 1080) { + if (window.innerWidth > 980) { + setNavState(false); + } + }); + + window.addEventListener("keydown", (event) => { + if (event.key === "Escape") { setNavState(false); } }); } -if ("IntersectionObserver" in window) { +function setupRevealMotion() { + const revealElements = document.querySelectorAll("[data-reveal]"); + + if (!("IntersectionObserver" in window)) { + revealElements.forEach((element) => element.classList.add("is-visible")); + return; + } + const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { @@ -61,11 +96,372 @@ if ("IntersectionObserver" in window) { }, { threshold: 0.16, - rootMargin: "0px 0px -8% 0px", + rootMargin: "0px 0px -10% 0px", }, ); revealElements.forEach((element) => observer.observe(element)); -} else { - revealElements.forEach((element) => element.classList.add("is-visible")); +} + +function setupTracking() { + document.querySelectorAll("[data-track]").forEach((element) => { + element.addEventListener("click", () => { + emitFunnelEvent(element.getAttribute("data-track") ?? "interaction", { + label: element.textContent?.trim() ?? "", + href: element instanceof HTMLAnchorElement ? element.getAttribute("href") ?? "" : "", + }); + }); + }); +} + +function emitFunnelEvent(eventName, detail = {}) { + const payload = { + event: eventName, + lang: currentLang, + page: body.dataset.page ?? "", + timestamp: new Date().toISOString(), + ...detail, + }; + + window.dispatchEvent(new CustomEvent("renvoo:funnel", { detail: payload })); + + if (Array.isArray(window.dataLayer)) { + window.dataLayer.push(payload); + } + + if (typeof window.renvooTrack === "function") { + window.renvooTrack(payload); + } +} + +function setupBookingFlow() { + const form = document.querySelector("[data-booking-form]"); + if (!(form instanceof HTMLFormElement)) { + return; + } + + const booking = copy.booking; + const labels = booking.labels; + const status = form.querySelector("[data-form-status]"); + const backButton = form.querySelector("[data-booking-back]"); + const nextButton = form.querySelector("[data-booking-next]"); + const submitButton = form.querySelector("[data-booking-submit]"); + const successPanel = document.querySelector("[data-booking-success]"); + const successSummary = successPanel?.querySelector("[data-success-summary]"); + const successStatus = successPanel?.querySelector("[data-success-status]"); + const copyButton = successPanel?.querySelector("[data-booking-copy]"); + const downloadButton = successPanel?.querySelector("[data-booking-download]"); + const restartButton = successPanel?.querySelector("[data-booking-restart]"); + const panels = Array.from(form.querySelectorAll("[data-step-panel]")); + const markers = Array.from(form.querySelectorAll("[data-step-marker]")); + const reviewList = form.querySelector("[data-review-list]"); + const labelMaps = createLabelMaps(booking, labels); + + let currentStep = 1; + let started = false; + let latestSummary = ""; + + setStep(1); + + form.addEventListener("input", (event) => { + const target = event.target; + if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) { + return; + } + + clearFieldError(form, target.name); + + if (currentStep === 3 && reviewList instanceof HTMLElement) { + renderReviewList(reviewList, collectBookingValues(form), labelMaps); + } + }); + + form.addEventListener("change", (event) => { + const target = event.target; + if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement)) { + return; + } + + clearFieldError(form, target.name); + }); + + nextButton?.addEventListener("click", () => { + const values = collectBookingValues(form); + const errors = validateBookingStep(currentStep, values); + + if (Object.keys(errors).length) { + applyErrors(form, errors, labels.errors); + focusFirstField(form, getFirstInvalidField(errors)); + return; + } + + if (currentStep === 1 && !started) { + started = true; + emitFunnelEvent("booking_started", { + mode: "preview", + storageKey: BOOKING_STORAGE_KEY, + }); + } + + setStep(currentStep + 1); + }); + + backButton?.addEventListener("click", () => { + setStep(currentStep - 1); + }); + + form.addEventListener("submit", async (event) => { + event.preventDefault(); + + const values = collectBookingValues(form); + const step1Errors = validateBookingStep(1, values); + const step2Errors = validateBookingStep(2, values); + const combinedErrors = { ...step1Errors, ...step2Errors }; + + if (Object.keys(combinedErrors).length) { + const firstField = getFirstInvalidField(combinedErrors); + setStep(["meetingFormat", "availability"].includes(firstField ?? "") ? 2 : 1); + applyErrors(form, combinedErrors, labels.errors); + focusFirstField(form, firstField); + return; + } + + setSubmitting(true); + + const payload = createBookingPayload(values, { + locale: copy.locale, + page: body.dataset.page ?? "pilot", + mode: "preview", + }); + + await new Promise((resolve) => window.setTimeout(resolve, 520)); + + persistBookingRequest(payload); + latestSummary = createPlainTextSummary(payload, labelMaps); + + if (successSummary instanceof HTMLElement) { + successSummary.textContent = latestSummary; + } + + form.hidden = true; + if (successPanel instanceof HTMLElement) { + successPanel.hidden = false; + } + + if (status instanceof HTMLElement) { + status.textContent = labels.statusSuccess; + } + + emitFunnelEvent("booking_submitted", { + mode: "preview", + availabilityCount: payload.availability.length, + meetingFormat: payload.meetingFormat, + }); + + setSubmitting(false); + }); + + copyButton?.addEventListener("click", async () => { + if (!latestSummary) { + return; + } + + try { + await navigator.clipboard.writeText(latestSummary); + setSuccessStatus(labels.copySuccess); + } catch { + setSuccessStatus(labels.copyFallback); + } + }); + + downloadButton?.addEventListener("click", () => { + if (!latestSummary) { + return; + } + + const fileName = + currentLang === "nl" ? "renvoo-gespreksaanvraag.txt" : "renvoo-meeting-request.txt"; + const download = createDownloadFile(latestSummary, fileName); + const link = document.createElement("a"); + link.href = download.href; + link.download = download.fileName; + document.body.append(link); + link.click(); + link.remove(); + window.setTimeout(() => URL.revokeObjectURL(download.href), 500); + setSuccessStatus(labels.downloadReady); + }); + + restartButton?.addEventListener("click", () => { + form.reset(); + clearAllErrors(form); + form.hidden = false; + if (successPanel instanceof HTMLElement) { + successPanel.hidden = true; + } + latestSummary = ""; + setSuccessStatus(""); + setStep(1); + const firstInput = form.querySelector("input, select, textarea"); + if (firstInput instanceof HTMLElement) { + firstInput.focus(); + } + }); + + function setStep(step) { + currentStep = Math.max(1, Math.min(3, step)); + + panels.forEach((panel) => { + const panelStep = Number(panel.getAttribute("data-step-panel")); + panel.hidden = panelStep !== currentStep; + }); + + markers.forEach((marker) => { + const markerStep = Number(marker.getAttribute("data-step-marker")); + marker.classList.toggle("is-active", markerStep === currentStep); + marker.classList.toggle("is-complete", markerStep < currentStep); + }); + + if (backButton instanceof HTMLElement) { + backButton.hidden = currentStep === 1; + } + + if (nextButton instanceof HTMLElement) { + nextButton.hidden = currentStep === 3; + } + + if (submitButton instanceof HTMLElement) { + submitButton.hidden = currentStep !== 3; + } + + if (status instanceof HTMLElement) { + status.textContent = + currentStep === 1 ? labels.statusReady : currentStep === 2 ? labels.statusStep2 : labels.statusReview; + } + + if (currentStep === 3 && reviewList instanceof HTMLElement) { + renderReviewList(reviewList, collectBookingValues(form), labelMaps); + } + } + + function setSubmitting(isSubmitting) { + if (!(submitButton instanceof HTMLButtonElement)) { + return; + } + + submitButton.disabled = isSubmitting; + submitButton.textContent = isSubmitting + ? currentLang === "nl" + ? "Aanvraag versturen…" + : "Submitting request…" + : labels.buttons.submit; + } + + function setSuccessStatus(message) { + if (successStatus instanceof HTMLElement) { + successStatus.textContent = message; + } + } +} + +function createLabelMaps(booking, labels) { + return { + summaryTitle: labels.summaryTitle, + submittedAt: labels.submittedAt, + mode: labels.mode, + contactName: labels.contactName, + contactEmail: labels.contactEmail, + role: labels.role, + clinicName: labels.clinicName, + city: labels.city, + clinicSize: labels.clinicSize, + primaryPain: labels.primaryPain, + workflowNotes: labels.workflowNotes, + meetingFormat: labels.meetingFormat, + availabilityTitle: labels.availabilityTitle, + roles: Object.fromEntries(booking.roles.map((item) => [item.value, item.label])), + clinicSizes: Object.fromEntries(booking.clinicSizes.map((item) => [item.value, item.label])), + primaryPains: Object.fromEntries(booking.primaryPains.map((item) => [item.value, item.label])), + meetingFormats: Object.fromEntries(booking.meetingFormats.map((item) => [item.value, item.label])), + availability: Object.fromEntries( + booking.availability.map((item) => [item.value, `${item.label} (${item.detail})`]), + ), + }; +} + +function renderReviewList(reviewList, values, labelMaps) { + const rows = [ + [labelMaps.contactName, values.contactName], + [labelMaps.contactEmail, values.contactEmail], + [labelMaps.role, labelMaps.roles[values.role] ?? values.role], + [labelMaps.clinicName, values.clinicName], + [labelMaps.city, values.city], + [labelMaps.clinicSize, labelMaps.clinicSizes[values.clinicSize] ?? values.clinicSize], + [labelMaps.primaryPain, labelMaps.primaryPains[values.primaryPain] ?? values.primaryPain], + [labelMaps.workflowNotes, values.workflowNotes], + [labelMaps.meetingFormat, labelMaps.meetingFormats[values.meetingFormat] ?? values.meetingFormat], + [ + labelMaps.availabilityTitle, + values.availability.map((item) => labelMaps.availability[item] ?? item).join(", "), + ], + ]; + + reviewList.innerHTML = rows + .map( + ([label, value]) => `
    ${label}
    ${value || "—"}
    `, + ) + .join(""); +} + +function applyErrors(form, errors, errorCopy) { + clearAllErrors(form); + + Object.entries(errors).forEach(([fieldName, errorKey]) => { + const message = fieldName === "availability" ? errorCopy.availability : errorCopy[errorKey]; + const errorSlot = form.querySelector(`[data-field-error="${fieldName}"]`); + if (errorSlot instanceof HTMLElement) { + errorSlot.textContent = message; + } + + const field = form.querySelector(`[name="${fieldName}"]`); + if (field instanceof HTMLElement) { + field.setAttribute("aria-invalid", "true"); + } + }); +} + +function clearAllErrors(form) { + form.querySelectorAll("[data-field-error]").forEach((slot) => { + if (slot instanceof HTMLElement) { + slot.textContent = ""; + } + }); + + form.querySelectorAll("[aria-invalid='true']").forEach((field) => { + field.setAttribute("aria-invalid", "false"); + }); +} + +function clearFieldError(form, fieldName) { + const errorSlot = form.querySelector(`[data-field-error="${fieldName}"]`); + if (errorSlot instanceof HTMLElement) { + errorSlot.textContent = ""; + } + + form.querySelectorAll(`[name="${fieldName}"]`).forEach((field) => { + if (field instanceof HTMLElement) { + field.setAttribute("aria-invalid", "false"); + } + }); +} + +function focusFirstField(form, fieldName) { + if (!fieldName) { + return; + } + + const field = form.querySelector(`[name="${fieldName}"]`); + if (field instanceof HTMLElement) { + field.focus(); + } } diff --git a/src/site/patient-notice.html b/src/site/patient-notice.html index 1062d76..50de0f2 100644 --- a/src/site/patient-notice.html +++ b/src/site/patient-notice.html @@ -1,120 +1,116 @@ - + - - Renvoo | Pre-launch patient message note - - - - - - + + Renvoo | Notitie patiëntbericht + + + + + + + + + + + + + + + + + - - -
    -
    + + + + + +
    + + - - -
    - - - + +
    - - - + diff --git a/src/site/pilot.html b/src/site/pilot.html new file mode 100644 index 0000000..2a1e092 --- /dev/null +++ b/src/site/pilot.html @@ -0,0 +1,306 @@ + + + + + + Renvoo | Vraag een validatiegesprek aan + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Pilot

    +

    Vraag een validatiegesprek aan

    +

    Het eerste gesprek is geen zware salesdemo. Het is een korte operator-review van uw no-show-, bevestigings- en herstelworkflow om te zien of een lichte pilot logisch is.

    + +
      +
    • 15 minuten
    • Founder-led
    • Geen live kalender-sync nodig
    • +
    +
    +
    +
    + +
    +
    +

    Wat het gesprek oplevert

    +

    Praktisch genoeg voor een eerste ja of nee

    + +
    +
    +
    +

    Workflow-scan

    +

    We lopen kort door hoe no-shows, late uitval en herstel nu worden afgehandeld.

    +
    +

    Signalen & beperkingen

    +

    We bespreken waar de praktijk te laat zicht krijgt en welke data of tooling er nu al beschikbaar is.

    +
    +

    Pilot-fit

    +

    We bepalen of er genoeg operationele pijn is voor een kleine, meetbare pilot.

    +
    +
    +
    + +
    +
    +
    +

    Lokale planner

    +

    Kies uw voorkeurstijden zoals in een lichte Calendly-flow

    +

    Deze planner werkt nu als request-flow op basis van lokale beschikbaarheidsblokken. De aanvraag wordt als nette handoff opgeslagen zolang live routing nog niet is gekoppeld.

    +
    +
      +
    • Naam en zakelijke contactgegevens
    • Korte context over praktijk en huidige workflow
    • Voorkeur voor gesprekstype en tijdsblokken
    • +
    +
    + +
    + +
    +
    +

    Plan gesprek

    +

    Vraag een validatiegesprek aan

    +

    Live routing is nog niet gekoppeld. De planner slaat daarom een nette handoff lokaal op voor demo- en launchvoorbereiding.

    +
    +
    +
    +
    +
      +
    1. Stap 1Context
    2. +
    3. Stap 2Voorkeuren
    4. +
    5. Stap 3Controleren
    6. +
    +

    Vul de praktijkcontext in om verder te gaan.

    + +
    +
    +
    + + +

    +
    +
    + + +

    +
    +
    + + +

    +
    +
    + + +

    +
    +
    + + +

    +
    +
    + + +

    +
    +
    + + +

    +
    +
    +
    + + +

    Beschrijf kort uw huidige systeem of werkwijze… Bijvoorbeeld Exquise met handmatige reminders en losse terugbellijst.

    +

    +
    +
    + + + + + +
    + + + +
    + +

    Live routing is nog niet gekoppeld. De planner slaat daarom een nette handoff lokaal op voor demo- en launchvoorbereiding.

    +
    + + +
    + +
    +
    + +
    +
    +
    +

    Nog niet klaar om te boeken?

    +

    Bekijk dan eerst de trust-pagina met datagrens, proof posture, materialen en juridische notities voordat u terugkomt naar de planner.

    +
    + Eerst vertrouwen bekijken +
    +
    + +
    + + + + diff --git a/src/site/privacy.html b/src/site/privacy.html index 8e6cdf4..427dda1 100644 --- a/src/site/privacy.html +++ b/src/site/privacy.html @@ -1,130 +1,120 @@ - + - - Renvoo | Pre-launch website privacy note - - - - - - + + Renvoo | Privacyverklaring website + + + + + + + + + + + + + + + + + - - -
    -
    + + + + + +
    + + - - -
    - - - + +
    - - - + diff --git a/src/site/product.html b/src/site/product.html new file mode 100644 index 0000000..c79d9b4 --- /dev/null +++ b/src/site/product.html @@ -0,0 +1,210 @@ + + + + + + Renvoo | Productflow voor no-showpreventie + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Product

    +

    Niet nog een reminder-tool.

    +

    Renvoo is bedoeld als operationele laag rond planningsonzekerheid. Het doel is niet meer berichten sturen, maar eerder weten waar uitval dreigt en het team betere herstelmomenten geven.

    + +
    +
    +
    + +
    +
    +

    Voor en na

    +

    Wat verandert er operationeel

    + +
    +
    +
    +

    Zonder Renvoo

    +
      +
    • Iedere afspraak krijgt ongeveer dezelfde reminderflow.
    • Risico wordt vaak pas duidelijk als de stoel al kwetsbaar is.
    • Front-desk teams reageren handmatig op gaten en no-response.
    • Terugvullen gebeurt via losse lijsten, geheugen en tijdsdruk.
    • +
    +
    +
    +

    Met Renvoo

    +
      +
    • Het team ziet eerder welke afspraken extra aandacht nodig hebben.
    • Bevestigen en verplaatsen kan gerichter gebeuren, niet alleen generiek.
    • Herstelwerk wordt onderdeel van een duidelijke workflow in plaats van losse brandjes.
    • De praktijk houdt controle over agenda-mutaties en patiëntcontactmomenten.
    • +
    +
    +
    +
    + +
    +
    +

    De vier stappen

    +

    Connect. Detect. Confirm. Replace.

    +

    De architectuur is bewust klein gehouden voor de eerste pilots: laagdrempelige intake, heldere teamzichtbaarheid en geen patiëntplatform in v1.

    +
    +
    +
    +
    + 01 +

    Connect

    +
    +

    CSV-first of read-only onboarding rond agenda-, afspraak- en communicatievelden die operationeel genoeg zijn om risico te zien.

    +
      +
    • Focus op scheduling en statusvelden
    • Geen diagnoses of klinische notities nodig
    • Lage frictie voor een eerste pilot
    • +
    +
    +
    + 02 +

    Detect

    +
    +

    Afspraken met verhoogd no-show- of late-uitvalrisico worden eerder zichtbaar dan in een standaard reminderprogramma.

    +
      +
    • Risico per afspraak in plaats van één generieke flow
    • Operatorgericht, niet klinisch
    • Gebouwd voor uitlegbaar gebruik in een praktijk
    • +
    +
    +
    + 03 +

    Confirm

    +
    +

    Het team kan sneller zien waar een bevestiging, verplaatsing of extra check zinvol is voordat de stoel verloren raakt.

    +
      +
    • Ondersteunt praktische opvolging
    • Past naast bestaande systemen
    • Helpt no-response eerder adresseren
    • +
    +
    +
    + 04 +

    Replace

    +
    +

    Wanneer een stoel vrijkomt, helpt de workflow de praktijk eerder denken in herstel en backfill terwijl daadwerkelijke agenda-acties onder praktijkcontrole blijven.

    +
      +
    • Geen autonome mutaties in v1
    • Herstel boven theater
    • Praktijk of EPD houdt de uiteindelijke controle
    • +
    +
    +
    +
    + +
    +
    +

    Lage frictie als ontwerpkeuze

    +

    Bewust eenvoudig gehouden voor de eerste kliniekpilots

    + +
    +
    +
    +

    Niet-klinische datagrens

    +

    Administratieve planning- en communicatiegegevens eerst. Dat houdt de scope smal en operationeel.

    +
    +

    CSV-first of read-only intake

    +

    De eerste stap hoeft geen zwaar integratieproject te zijn om te zien of de pilot de moeite waard is.

    +
    +

    Geen patiëntaccounts in v1

    +

    Patiëntinteractie blijft via praktijkgestuurde berichten en veilige links, niet via een nieuw consumentproduct.

    +
    +
    +
    + +
    +
    +

    Waarom tandartspraktijken eerst

    +

    De wedge is klein, maar scherp.

    + +
    +
    +
      +
    1. Lege stoeluren zijn direct economisch voelbaar.
    2. Agenda's zijn dicht gepland en laten weinig herstelruimte over.
    3. Praktijkhouders en praktijkmanagers voelen dit probleem zelf in de operatie.
    4. +
    +
    +
    + +
    +
    +
    +

    Volgende stap

    +

    Ziet dit eruit als een realistische workflow voor uw praktijk?

    +

    Dan is de beste volgende stap een kort gesprek waarin we niet demo'en, maar uw huidige no-show- en uitvalproces scherp krijgen.

    +
    + +
    +
    + +
    + + + + diff --git a/src/site/styles.css b/src/site/styles.css index 0ea1c0e..e85aa70 100644 --- a/src/site/styles.css +++ b/src/site/styles.css @@ -1,22 +1,43 @@ :root { color-scheme: light; - --bg: #f4efe8; - --surface: rgba(255, 250, 244, 0.8); - --surface-strong: #fff8ef; - --ink: #0c2a34; - --muted: #5d737b; - --line: rgba(12, 42, 52, 0.12); - --brand: #0d667c; - --brand-deep: #094c60; - --brand-bright: #29c2d2; - --peach: #ffe6d3; - --shadow: 0 24px 80px rgba(12, 42, 52, 0.12); - --radius-lg: 30px; - --radius-md: 22px; + --font-body: "Manrope", sans-serif; + --font-display: "Newsreader", serif; + --page-bg: #f4efe7; + --page-ink: #183036; + --page-muted: #586d71; + --page-line: rgba(24, 48, 54, 0.12); + --card-bg: rgba(255, 252, 248, 0.82); + --card-strong: rgba(255, 252, 248, 0.94); + --card-ink: #20363a; + --accent: #17696a; + --accent-strong: #0e4f53; + --accent-soft: rgba(23, 105, 106, 0.12); + --accent-warm: #c78c49; + --accent-warm-soft: rgba(199, 140, 73, 0.12); + --shadow-soft: 0 16px 48px rgba(17, 34, 38, 0.08); + --shadow-strong: 0 28px 80px rgba(17, 34, 38, 0.14); --radius-sm: 16px; -} - -* { + --radius-md: 24px; + --radius-lg: 32px; + --radius-pill: 999px; + --container: min(1180px, calc(100vw - 2.5rem)); + --space-0: 0; + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; + --space-7: 3rem; + --space-8: 4rem; + --space-9: 6rem; + --space-10: 8rem; + --site-transition: 220ms ease; +} + +*, +*::before, +*::after { box-sizing: border-box; } @@ -26,204 +47,395 @@ html { body { margin: 0; - background: - radial-gradient(circle at top left, rgba(41, 194, 210, 0.22), transparent 30%), - radial-gradient(circle at right 20%, rgba(13, 102, 124, 0.14), transparent 22%), - linear-gradient(180deg, #f4efe8 0%, #f7f3ee 48%, #fff8ef 100%); - color: var(--ink); - font-family: "Public Sans", sans-serif; - min-height: 100vh; + min-width: 320px; overflow-x: hidden; + background: + radial-gradient(circle at top left, rgba(23, 105, 106, 0.18), transparent 36%), + radial-gradient(circle at top right, rgba(199, 140, 73, 0.18), transparent 28%), + linear-gradient(180deg, #f7f2ea 0%, #f4efe7 44%, #efe7de 100%); + color: var(--page-ink); + font-family: var(--font-body); + font-size: 16px; + line-height: 1.65; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +body.nav-open { + overflow: hidden; } -body::before { - content: ""; - position: fixed; - inset: 0; - background-image: - linear-gradient(rgba(12, 42, 52, 0.035) 1px, transparent 1px), - linear-gradient(90deg, rgba(12, 42, 52, 0.035) 1px, transparent 1px); - background-size: 72px 72px; - mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.45), transparent 85%); - pointer-events: none; - z-index: -3; +img { + display: block; + max-width: 100%; + height: auto; } -.page-glow { - position: fixed; +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select, +textarea { + font: inherit; +} + +input[type="radio"], +input[type="checkbox"] { + width: 1.1rem; + height: 1.1rem; + padding: 0; + border: 0; border-radius: 999px; - filter: blur(80px); - opacity: 0.55; - pointer-events: none; - z-index: -2; + background: transparent; + box-shadow: none; + accent-color: var(--accent); } -.page-glow-one { - width: 320px; - height: 320px; - top: -60px; - right: -40px; - background: rgba(41, 194, 210, 0.25); +input[type="checkbox"] { + border-radius: 0.3rem; } -.page-glow-two { - width: 360px; - height: 360px; - bottom: 8%; - left: -80px; - background: rgba(13, 102, 124, 0.14); +button, +a, +summary, +label { + -webkit-tap-highlight-color: rgba(23, 105, 106, 0.12); } -img { - display: block; - max-width: 100%; +button, +a.button, +.header-cta, +.language-switch, +.choice-card { + touch-action: manipulation; } -a { - color: inherit; - text-decoration: none; +button { + border: 0; + background: none; + cursor: pointer; } -.skip-link, -.sr-only { - position: absolute; +input, +select, +textarea { + width: 100%; + border: 1px solid rgba(24, 48, 54, 0.18); + border-radius: 18px; + background: rgba(255, 252, 248, 0.9); + color: var(--card-ink); + padding: 0.9rem 1rem; + transition: + border-color var(--site-transition), + box-shadow var(--site-transition), + background-color var(--site-transition), + transform var(--site-transition); +} + +select { + appearance: none; + background-image: + linear-gradient(45deg, transparent 50%, var(--page-ink) 50%), + linear-gradient(135deg, var(--page-ink) 50%, transparent 50%); + background-position: + calc(100% - 1.35rem) calc(1.15rem), + calc(100% - 1rem) calc(1.15rem); + background-size: 0.45rem 0.45rem; + background-repeat: no-repeat; +} + +textarea { + resize: vertical; + min-height: 9rem; +} + +input::placeholder, +textarea::placeholder { + color: rgba(24, 48, 54, 0.45); +} + +input:focus-visible, +select:focus-visible, +textarea:focus-visible, +button:focus-visible, +a:focus-visible, +summary:focus-visible { + outline: 0; + border-color: rgba(23, 105, 106, 0.55); + box-shadow: 0 0 0 4px rgba(23, 105, 106, 0.16); +} + +input[aria-invalid="true"], +select[aria-invalid="true"], +textarea[aria-invalid="true"] { + border-color: rgba(173, 63, 47, 0.7); + box-shadow: 0 0 0 4px rgba(173, 63, 47, 0.12); +} + +h1, +h2, +h3 { + margin: 0; + font-family: var(--font-display); + font-weight: 500; + letter-spacing: -0.03em; + line-height: 0.98; + text-wrap: balance; +} + +h1 { + font-size: clamp(3rem, 7vw, 5.5rem); +} + +h2 { + font-size: clamp(2rem, 4.4vw, 3.55rem); +} + +h3 { + font-size: clamp(1.4rem, 2vw, 1.9rem); +} + +p, +ul, +ol, +dl, +figure { + margin: 0; +} + +ul, +ol { + padding-left: 1.1rem; +} + +main, +section { + min-width: 0; +} + +#booking, +.chapter { + scroll-margin-top: 6.5rem; } .skip-link { + position: absolute; + top: 0.9rem; left: 1rem; - top: 1rem; - z-index: 20; - padding: 0.8rem 1rem; - border-radius: 999px; - background: var(--ink); - color: #fff; - transform: translateY(-220%); - transition: transform 180ms ease; + z-index: 30; + transform: translateY(-180%); + padding: 0.75rem 1rem; + border-radius: var(--radius-pill); + background: #ffffff; + box-shadow: var(--shadow-soft); + transition: transform var(--site-transition); } .skip-link:focus { transform: translateY(0); } -.sr-only { - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; +.page-aura { + position: fixed; + z-index: -1; + width: 42rem; + height: 42rem; + border-radius: 50%; + filter: blur(16px); + pointer-events: none; } -.section-shell { - width: min(1180px, calc(100vw - 2rem)); - margin: 0 auto; +.page-aura-left { + top: -18rem; + left: -12rem; + background: radial-gradient(circle, rgba(23, 105, 106, 0.16) 0%, rgba(23, 105, 106, 0) 72%); } -.not-found-page { - display: grid; - place-items: center; - min-height: 100vh; - padding: 2rem 0; +.page-aura-right { + top: 16rem; + right: -16rem; + background: radial-gradient(circle, rgba(199, 140, 73, 0.18) 0%, rgba(199, 140, 73, 0) 74%); } -.not-found-card { - width: min(720px, 100%); - padding: 2rem; - border-radius: var(--radius-lg); - border: 1px solid rgba(255, 255, 255, 0.75); - background: rgba(255, 250, 244, 0.82); - box-shadow: var(--shadow); - text-align: center; +.site-header { + position: sticky; + top: 0; + z-index: 20; + padding: 1rem 0; + transition: + background-color var(--site-transition), + backdrop-filter var(--site-transition), + box-shadow var(--site-transition), + border-color var(--site-transition); } -.not-found-logo { - width: min(220px, 100%); - margin: 0 auto 1.5rem; +.site-header.is-scrolled { + backdrop-filter: blur(18px); + background: rgba(244, 239, 231, 0.78); + border-bottom: 1px solid rgba(24, 48, 54, 0.08); + box-shadow: 0 12px 40px rgba(17, 34, 38, 0.06); } -.site-header { - position: sticky; - top: 0; - z-index: 10; +.site-header-inner, +.chapter, +.site-footer { + width: var(--container); + margin: 0 auto; +} + +.site-header-inner { display: flex; align-items: center; justify-content: space-between; + gap: var(--space-5); + padding: 0.2rem 0; +} + +.brand-mark { + display: flex; + align-items: center; gap: 1rem; - width: min(1180px, calc(100vw - 2rem)); - margin: 1rem auto 0; - padding: 0.9rem 1.05rem; - background: rgba(255, 248, 239, 0.65); - border: 1px solid rgba(255, 255, 255, 0.55); - border-radius: 999px; - backdrop-filter: blur(22px); - transition: - box-shadow 220ms ease, - transform 220ms ease, - background 220ms ease; + min-width: 0; } -.site-header.is-scrolled { - box-shadow: 0 18px 40px rgba(12, 42, 52, 0.1); - background: rgba(255, 248, 239, 0.88); +.brand-mark img { + width: 11rem; } -.brand { - flex: 0 0 auto; +.brand-mark span { + color: var(--page-muted); + font-size: 0.92rem; + line-height: 1.45; } -.brand img { - width: 176px; +.site-nav-shell { + display: flex; + align-items: center; + gap: 1rem; } .site-nav { display: flex; - gap: 1.2rem; - font-size: 0.95rem; - color: var(--muted); + align-items: center; + gap: 0.4rem; + padding: 0.35rem; + border-radius: var(--radius-pill); + background: rgba(255, 252, 248, 0.72); + border: 1px solid rgba(24, 48, 54, 0.08); } -.site-nav a { - transition: color 180ms ease; +.site-nav a, +.language-switch, +.header-cta, +.inline-link { + transition: + color var(--site-transition), + background-color var(--site-transition), + transform var(--site-transition), + box-shadow var(--site-transition); } -.site-nav-cta { - display: none; +.site-nav a { + padding: 0.65rem 0.95rem; + border-radius: var(--radius-pill); + color: var(--page-muted); + font-size: 0.95rem; + font-weight: 600; } .site-nav a:hover, -.site-nav a:focus-visible { - color: var(--brand-deep); +.site-nav a.is-active { + color: var(--page-ink); + background: rgba(23, 105, 106, 0.08); +} + +.site-header-actions { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.language-switch { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.8rem; + padding: 0.72rem 0.95rem; + border-radius: var(--radius-pill); + border: 1px solid rgba(24, 48, 54, 0.12); + background: rgba(255, 252, 248, 0.72); + font-size: 0.9rem; + font-weight: 700; +} + +.language-switch:hover, +.header-cta:hover, +.button:hover, +.inline-link:hover { + transform: translateY(-1px); +} + +.header-cta, +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: var(--radius-pill); + padding: 0.92rem 1.35rem; + font-size: 0.95rem; + font-weight: 700; + letter-spacing: -0.01em; +} + +.header-cta, +.button-primary { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); + color: #fdfbf7; + box-shadow: 0 14px 34px rgba(14, 79, 83, 0.22); +} + +.button-secondary { + background: rgba(255, 252, 248, 0.78); + border: 1px solid rgba(24, 48, 54, 0.12); + color: var(--page-ink); +} + +.button:hover, +.header-cta:hover { + box-shadow: 0 18px 38px rgba(14, 79, 83, 0.2); } .nav-toggle { display: none; + flex-direction: column; + gap: 0.28rem; align-items: center; justify-content: center; - flex-direction: column; - gap: 0.3rem; - width: 50px; - height: 50px; - padding: 0; - border: 1px solid rgba(12, 42, 52, 0.14); - border-radius: 999px; - background: rgba(255, 255, 255, 0.68); - cursor: pointer; + width: 3rem; + height: 3rem; + border-radius: 50%; + background: rgba(255, 252, 248, 0.78); + border: 1px solid rgba(24, 48, 54, 0.1); } -.nav-toggle span:not(.sr-only) { - width: 18px; +.nav-toggle span { + width: 1rem; height: 2px; - border-radius: 999px; - background: var(--ink); + border-radius: 99px; + background: var(--page-ink); transition: - transform 180ms ease, - opacity 180ms ease; + transform 200ms ease, + opacity 200ms ease; + transform-origin: center; } .nav-toggle[aria-expanded="true"] span:nth-child(1) { - transform: translateY(5px) rotate(45deg); + transform: translateY(6px) rotate(45deg); } .nav-toggle[aria-expanded="true"] span:nth-child(2) { @@ -231,789 +443,858 @@ a { } .nav-toggle[aria-expanded="true"] span:nth-child(3) { - transform: translateY(-5px) rotate(-45deg); + transform: translateY(-6px) rotate(-45deg); } -.nav-cta, -.button { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 999px; - transition: - transform 180ms ease, - box-shadow 180ms ease, - background 180ms ease, - color 180ms ease; +.site-main { + padding-bottom: var(--space-10); } -.nav-cta { - padding: 0.8rem 1.15rem; - background: var(--ink); - color: #fff; - font-size: 0.92rem; +.chapter { + padding-top: var(--space-9); } -.nav-cta:hover, -.nav-cta:focus-visible, -.button:hover, -.button:focus-visible { - transform: translateY(-2px); +.hero-home, +.hero-page { + padding-top: clamp(3rem, 9vw, 6rem); } -.hero { +.hero-grid { display: grid; - grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr); - gap: 2rem; - align-items: center; - padding: 4.8rem 0 3.25rem; + grid-template-columns: minmax(0, 1.22fr) minmax(18rem, 0.78fr); + gap: clamp(1.5rem, 4vw, 3.2rem); + align-items: end; +} + +.hero-grid-wide { + grid-template-columns: minmax(0, 1fr); +} + +.hero-copy, +.hero-rail, +.hero-note, +.pricing-panel, +.booking-sidecard, +.glass-card, +.story-card, +.path-card, +.timeline-card, +.compare-card, +.workflow-card, +.cta-strip, +.legal-card, +.download-card, +.review-card, +.success-card, +.founder-card { + min-width: 0; } -.eyebrow { - margin: 0 0 0.8rem; - color: var(--brand-deep); - font-size: 0.88rem; - font-weight: 700; - letter-spacing: 0.16em; - text-transform: uppercase; +.hero-copy { + max-width: 43rem; } -h1, -h2, -h3 { - font-family: "Space Grotesk", sans-serif; - letter-spacing: -0.04em; - margin: 0; +.eyebrow { + display: inline-flex; + align-items: center; + gap: 0.55rem; + margin-bottom: 1rem; + color: var(--accent-strong); + font-size: 0.82rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; } -h1 { - font-size: clamp(3rem, 8vw, 5.8rem); - line-height: 0.96; - max-width: 11ch; +.eyebrow.subdued { + color: var(--page-muted); } -h2 { - font-size: clamp(2rem, 4vw, 3.35rem); - line-height: 1; +.hero-lead, +.section-intro, +.subdued-copy, +.support-note { + color: var(--page-muted); + font-size: clamp(1rem, 1.45vw, 1.18rem); } -h3 { - font-size: 1.35rem; - line-height: 1.08; +.hero-lead { + margin-top: 1.5rem; + max-width: 40rem; } -.lead, -.section-heading p, -.problem-card p, -.workflow-card p, -.proof-card p, -.pilot-card p, -.founder-copy p, -.contact-card p, -.site-footer p { - color: var(--muted); - line-height: 1.7; +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; + margin-top: 2rem; } -.lead { - max-width: 36rem; - font-size: 1.12rem; - margin: 1.2rem 0 0; +.hero-badges, +.bullet-list, +.download-stack, +.footer-links, +.booking-progress { + list-style: none; + padding: 0; } -.hero-actions { +.hero-badges { display: flex; flex-wrap: wrap; - gap: 0.9rem; - margin-top: 1.8rem; + gap: 0.75rem; + margin-top: 1.6rem; } -.button { - min-height: 54px; - padding: 0.95rem 1.4rem; +.hero-badges li, +.step-chip { + display: inline-flex; + align-items: center; + border-radius: var(--radius-pill); + padding: 0.5rem 0.85rem; + background: rgba(255, 252, 248, 0.76); + border: 1px solid rgba(24, 48, 54, 0.1); + color: var(--page-muted); + font-size: 0.9rem; font-weight: 600; } -.button-primary { - background: linear-gradient(135deg, var(--brand) 0%, var(--brand-bright) 100%); - color: #fff; - box-shadow: 0 18px 35px rgba(13, 102, 124, 0.25); +.hero-note { + position: relative; + padding: 1.6rem; + border-radius: var(--radius-lg); + background: linear-gradient(180deg, rgba(255, 252, 248, 0.9) 0%, rgba(249, 243, 234, 0.78) 100%); + border: 1px solid rgba(24, 48, 54, 0.08); + box-shadow: var(--shadow-soft); } -.button-secondary { - border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.55); +.hero-note::after { + content: ""; + position: absolute; + inset: auto 1.6rem -0.9rem auto; + width: 6rem; + height: 0.8rem; + border-radius: var(--radius-pill); + background: linear-gradient(90deg, rgba(23, 105, 106, 0.28), rgba(199, 140, 73, 0.3)); + filter: blur(8px); +} + +.hero-note-kicker, +.footer-title { + margin-bottom: 0.85rem; + font-size: 0.85rem; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent-strong); } -.hero-badges, -.contact-tags, -.pilot-list, -.boundary-list, -.signal-card ul { - list-style: none; - padding: 0; - margin: 0; +.hero-note ul, +.bullet-list { + display: grid; + gap: 0.75rem; } -.hero-badges { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - margin-top: 1.3rem; +.story-card p, +.path-card p, +.timeline-card p, +.compare-card p, +.workflow-card p, +.glass-card p, +.pricing-panel p, +.booking-sidecard p, +.legal-card p, +.download-card p, +.success-card p, +.review-card p { + color: var(--page-muted); } -.hero-badges li, -.contact-tags span { - padding: 0.7rem 1rem; - border-radius: 999px; - background: rgba(255, 255, 255, 0.68); - border: 1px solid rgba(255, 255, 255, 0.9); - color: var(--ink); - font-size: 0.92rem; +.hero-note li, +.bullet-list li { + color: var(--card-ink); } -.panel-frame, -.insight-band, -.proof-card, -.pilot-card, -.contact-card, -.founder-card { - border: 1px solid rgba(255, 255, 255, 0.7); - background: var(--surface); - backdrop-filter: blur(12px); - box-shadow: var(--shadow); +.hero-note li::marker, +.bullet-list li::marker, +.why-list li::marker { + color: var(--accent); } -.panel-frame { - padding: 1.1rem; - border-radius: var(--radius-lg); +.section-heading { + max-width: 40rem; } -.panel-topline { - display: flex; - justify-content: space-between; - gap: 1rem; - color: var(--muted); - font-size: 0.86rem; - margin-bottom: 1rem; +.section-intro { + margin-top: 1rem; } -.panel-grid { +.editorial-grid, +.split-cards, +.path-cards, +.timeline-grid, +.workflow-grid { display: grid; - gap: 1rem; + gap: 1.2rem; + margin-top: 2rem; } -.signal-card { - padding: 1.2rem; - border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.68); - border: 1px solid rgba(12, 42, 52, 0.08); +.editorial-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); } -.signal-step, -.workflow-card span { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 999px; - min-width: 48px; - min-height: 32px; - padding: 0 0.85rem; - font-size: 0.86rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; +.split-cards, +.compare-grid, +.materials-layout, +.booking-layout, +.pilot-split, +.footer-grid { + display: grid; + gap: 1.25rem; + margin-top: 2rem; } -.signal-step { - color: var(--brand-deep); - background: rgba(41, 194, 210, 0.16); +.split-cards, +.compare-grid, +.pilot-split { + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.signal-card h2 { - font-size: 1.5rem; - margin-top: 0.85rem; +.path-cards { + grid-template-columns: repeat(3, minmax(0, 1fr)); } -.signal-card p, -.signal-card small, -.signal-card li { - color: var(--muted); +.timeline-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); } -.signal-card ul { - display: grid; - gap: 0.55rem; - margin-top: 0.9rem; +.workflow-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.signal-card li, -.pilot-list li, -.boundary-list li { - position: relative; - padding-left: 1.2rem; +.story-card, +.path-card, +.timeline-card, +.compare-card, +.workflow-card, +.glass-card, +.pricing-panel, +.booking-sidecard, +.legal-card, +.download-card, +.review-card, +.success-card { + border-radius: var(--radius-md); + border: 1px solid rgba(24, 48, 54, 0.08); + background: var(--card-bg); + box-shadow: var(--shadow-soft); +} + +.story-card, +.path-card, +.timeline-card, +.compare-card, +.workflow-card, +.glass-card, +.pricing-panel, +.booking-sidecard, +.legal-card, +.download-card, +.review-card, +.success-card { + padding: 1.45rem; +} + +.story-card h3, +.path-card h3, +.timeline-card h3, +.compare-card h3, +.workflow-card h3, +.glass-card h3, +.pricing-panel h3, +.booking-sidecard h3, +.legal-card h2, +.download-card h3, +.review-card h3, +.success-card h3 { + margin-bottom: 0.9rem; } -.signal-card li::before, -.pilot-list li::before, -.boundary-list li::before { - content: ""; - position: absolute; - top: 0.62rem; - left: 0; - width: 8px; - height: 8px; - border-radius: 999px; - background: var(--brand-bright); +.path-card { + background: linear-gradient(180deg, rgba(255, 252, 248, 0.92) 0%, rgba(252, 249, 243, 0.78) 100%); } -.status-stack { - display: flex; - flex-wrap: wrap; - gap: 0.55rem; - margin-top: 1rem; +.compare-card-accent, +.cta-strip, +.review-card, +.success-card { + background: linear-gradient(180deg, rgba(236, 246, 244, 0.94) 0%, rgba(245, 249, 248, 0.82) 100%); } -.status-chip { - padding: 0.55rem 0.8rem; - border-radius: 999px; - background: rgba(13, 102, 124, 0.09); - color: var(--brand-deep); - font-size: 0.9rem; - font-weight: 600; +.workflow-card-top { + display: flex; + align-items: center; + gap: 0.9rem; + margin-bottom: 1rem; } -.recovery-meter { - margin: 1rem 0 0.75rem; - padding: 0.35rem; - border-radius: 999px; - background: rgba(12, 42, 52, 0.08); +.step-count { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.35rem; + height: 2.35rem; + border-radius: 50%; + background: var(--accent-soft); + color: var(--accent-strong); + font-size: 0.9rem; + font-weight: 800; } -.recovery-meter span { - display: block; - width: 72%; - height: 10px; - border-radius: 999px; - background: linear-gradient(90deg, var(--brand) 0%, var(--brand-bright) 100%); +.bullet-list.compact { + gap: 0.55rem; } -.insight-band, -.path, -.proof, -.workflow, -.pilot, -.founder, -.materials, -.faq, -.contact, -.site-footer, -.legal-page { - padding-bottom: 2.4rem; +.why-list { + max-width: 45rem; + padding: 1.4rem 1.4rem 1.4rem 1.7rem; + border-radius: var(--radius-md); + background: rgba(255, 252, 248, 0.8); + border: 1px solid rgba(24, 48, 54, 0.08); + box-shadow: var(--shadow-soft); } -.insight-band { - padding: 2rem; - border-radius: var(--radius-lg); +.why-list ol { + display: grid; + gap: 0.85rem; } -.section-heading { - max-width: 42rem; - margin-bottom: 1.6rem; +.materials-layout { + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); } -.problem-grid, -.path-grid, -.workflow-grid, -.proof-grid, -.pilot-grid, -.materials-grid, -.faq-grid { +.download-stack, +.preview-grid { display: grid; gap: 1rem; } -.problem-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); +.preview-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.problem-card, -.workflow-card { - padding: 1.4rem; +.preview-card { + overflow: hidden; border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.62); - border: 1px solid rgba(12, 42, 52, 0.08); + background: rgba(255, 252, 248, 0.8); + border: 1px solid rgba(24, 48, 54, 0.08); + box-shadow: var(--shadow-soft); } -.workflow-grid { - grid-template-columns: repeat(4, minmax(0, 1fr)); +.preview-card img { + width: 100%; + object-fit: cover; } -.path-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); +.preview-card figcaption { + padding: 0.9rem 1rem 1.05rem; + color: var(--page-muted); + font-size: 0.92rem; } -.workflow-card span { - background: rgba(13, 102, 124, 0.12); - color: var(--brand-deep); +.faq-grid { + display: grid; + gap: 1rem; + margin-top: 2rem; } -.path-card { - padding: 1.4rem; - border-radius: var(--radius-lg); - background: rgba(255, 255, 255, 0.7); - border: 1px solid rgba(255, 255, 255, 0.9); - box-shadow: var(--shadow); +.faq-item { + border-radius: var(--radius-md); + border: 1px solid rgba(24, 48, 54, 0.08); + background: rgba(255, 252, 248, 0.82); + box-shadow: var(--shadow-soft); + overflow: hidden; } -.path-card span { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 34px; - padding: 0 0.9rem; - border-radius: 999px; - background: rgba(41, 194, 210, 0.16); - color: var(--brand-deep); - font-size: 0.86rem; +.faq-item summary { + list-style: none; + padding: 1.2rem 1.3rem; font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.path-card h3 { - margin-top: 1rem; + cursor: pointer; } -.path-card p { - color: var(--muted); - line-height: 1.7; +.faq-item summary::-webkit-details-marker { + display: none; } -.workflow-card h3 { - margin-top: 1rem; +.faq-item p { + padding: 0 1.3rem 1.25rem; + color: var(--page-muted); } -.proof-grid { - grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr); +.booking-layout { + grid-template-columns: minmax(0, 1.08fr) minmax(18rem, 0.72fr); + align-items: start; } -.proof-card, -.pilot-card, -.contact-card { - padding: 1.6rem; +.booking-form, +.booking-success { border-radius: var(--radius-lg); + border: 1px solid rgba(24, 48, 54, 0.08); + background: var(--card-strong); + box-shadow: var(--shadow-strong); } -.proof-card-primary { - background: - linear-gradient(135deg, rgba(13, 102, 124, 0.98), rgba(41, 194, 210, 0.9)), - var(--surface); - color: #fff; -} - -.proof-card-primary .eyebrow, -.proof-card-primary p, -.proof-card-primary span { - color: rgba(255, 255, 255, 0.85); +.booking-form { + padding: 1.5rem; } -.metric-row { +.booking-progress { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.85rem; - margin-top: 1.35rem; + gap: 0.8rem; } -.metric-row div { - padding: 1rem; - border-radius: var(--radius-sm); - background: rgba(255, 255, 255, 0.12); +.booking-progress li { + padding: 0.9rem 1rem; + border-radius: 20px; + background: rgba(24, 48, 54, 0.04); + color: var(--page-muted); + transition: + background-color var(--site-transition), + color var(--site-transition), + transform var(--site-transition); } -.metric-row strong { +.booking-progress li span, +.booking-progress li strong { display: block; - font-family: "Space Grotesk", sans-serif; - font-size: 1.8rem; - letter-spacing: -0.05em; } -.boundary-list, -.pilot-list { - display: grid; - gap: 0.75rem; - margin-top: 1rem; +.booking-progress li span { + margin-bottom: 0.25rem; + font-size: 0.8rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; } -.pilot-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); +.booking-progress li.is-active, +.booking-progress li.is-complete { + color: var(--page-ink); } -.pilot-card-accent { - background: linear-gradient(180deg, rgba(255, 230, 211, 0.95), rgba(255, 248, 239, 0.95)); +.booking-progress li.is-active { + background: rgba(23, 105, 106, 0.12); } -.materials-grid { - grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr); - align-items: start; +.booking-progress li.is-complete { + background: rgba(199, 140, 73, 0.14); } -.materials-card { - padding: 1.6rem; - border-radius: var(--radius-lg); - background: rgba(255, 255, 255, 0.68); - border: 1px solid rgba(255, 255, 255, 0.85); - box-shadow: var(--shadow); +.form-status, +.booking-preview-note, +.field-error, +.field-hint, +.support-note { + font-size: 0.95rem; } -.materials-previews { +.form-status { + margin-top: 1rem; + color: var(--accent-strong); +} + +.booking-step { + margin-top: 1.5rem; +} + +.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } -.preview-tile { - margin: 0; - padding: 0.9rem; - border-radius: var(--radius-lg); - background: rgba(255, 255, 255, 0.72); - border: 1px solid rgba(255, 255, 255, 0.9); - box-shadow: var(--shadow); +.field, +.field-full { + display: grid; + gap: 0.5rem; } -.preview-tile-large { - grid-column: 1 / -1; +.field-full { + margin-top: 1rem; } -.preview-tile img { - width: 100%; - border-radius: 18px; - aspect-ratio: 16 / 10; - object-fit: cover; - object-position: top; +.field label, +.choice-group legend { + font-weight: 700; } -.preview-tile-large img { - aspect-ratio: 16 / 9; +.field-hint { + color: var(--page-muted); } -.preview-tile figcaption { - margin-top: 0.75rem; - color: var(--muted); - font-size: 0.92rem; +.field-error { + min-height: 1.3rem; + color: #ad3f2f; } -.download-actions, -.contact-actions { - display: flex; - flex-wrap: wrap; - gap: 0.9rem; - margin-top: 1.2rem; +.choice-group { + margin: 0; + padding: 0; + border: 0; } -.compliance-actions { - margin-top: 1.6rem; +.choice-group + .choice-group { + margin-top: 1.4rem; } -.faq-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); +.choice-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.9rem; + margin-top: 1rem; } -.faq-item { - padding: 1.2rem 1.25rem; - border: 1px solid rgba(12, 42, 52, 0.1); - border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.66); - box-shadow: 0 18px 40px rgba(12, 42, 52, 0.08); +.choice-grid-dense { + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.faq-item summary { +.choice-card { + display: grid; + gap: 0.35rem; + padding: 1rem; + border-radius: 20px; + border: 1px solid rgba(24, 48, 54, 0.1); + background: rgba(255, 252, 248, 0.8); cursor: pointer; - list-style: none; - font-family: "Space Grotesk", sans-serif; - font-size: 1.16rem; - font-weight: 700; - letter-spacing: -0.03em; + transition: + border-color var(--site-transition), + background-color var(--site-transition), + transform var(--site-transition), + box-shadow var(--site-transition); } -.faq-item summary::-webkit-details-marker { - display: none; +.choice-card input { + width: auto; + margin: 0; } -.faq-item summary::after { - content: "+"; - float: right; - color: var(--brand-deep); - font-size: 1.4rem; - line-height: 1; +.choice-card small { + color: var(--page-muted); + font-size: 0.88rem; } -.faq-item[open] summary::after { - content: "−"; +.choice-card:hover, +.choice-card:focus-within { + border-color: rgba(23, 105, 106, 0.26); + background: rgba(236, 246, 244, 0.66); + box-shadow: 0 14px 30px rgba(17, 34, 38, 0.06); } -.faq-item p { - margin: 0.9rem 0 0; - color: var(--muted); - line-height: 1.7; +.choice-card:has(input:checked) { + border-color: rgba(23, 105, 106, 0.42); + background: rgba(236, 246, 244, 0.92); + box-shadow: 0 14px 30px rgba(17, 34, 38, 0.08); } -.pricing-line { - font-size: 1.08rem; - color: var(--ink); +.booking-review dl { + display: grid; + gap: 0.9rem; } -.founder-card { +.booking-review div { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(180px, 220px); - gap: 1rem; - align-items: center; - padding: 1.5rem; - border-radius: var(--radius-lg); + gap: 0.2rem; + padding-bottom: 0.9rem; + border-bottom: 1px solid rgba(24, 48, 54, 0.08); } -.founder-mark { - justify-self: end; - width: min(220px, 100%); - filter: drop-shadow(0 18px 40px rgba(13, 102, 124, 0.16)); +.booking-review dt { + font-size: 0.82rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--page-muted); } -.contact-card { - text-align: center; - padding: 2.1rem; +.booking-review dd { + margin: 0; + color: var(--page-ink); +} + +.booking-preview-note { + margin-top: 1.15rem; + color: var(--page-muted); } -.contact-tags { +.form-actions, +.cta-row, +.footer-meta { display: flex; flex-wrap: wrap; - justify-content: center; - gap: 0.75rem; - margin-top: 1.4rem; + align-items: center; + gap: 0.8rem; } -.site-footer { +.form-actions { + margin-top: 1.5rem; +} + +.booking-success { + padding: 0; +} + +.success-card { + padding: 1.5rem; +} + +.success-card pre { + margin-top: 1rem; + overflow: auto; + border-radius: 20px; + background: rgba(24, 48, 54, 0.06); + padding: 1rem; + font-size: 0.88rem; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} + +.cta-strip { display: flex; justify-content: space-between; - gap: 1rem; - padding-top: 0.6rem; - padding-bottom: 2.2rem; - font-size: 0.95rem; + align-items: center; + gap: 1.4rem; + padding: 1.5rem; } -.footer-links { - display: flex; - flex-wrap: wrap; - gap: 1rem; +.cta-strip h2 { + margin-bottom: 0.85rem; } -.footer-links a { - color: var(--brand-deep); +.inline-link { + display: inline-flex; + margin-top: 1.2rem; + color: var(--accent-strong); + font-weight: 700; +} + +.inline-link:hover { + color: var(--accent); } -.legal-page { - padding-top: 2.8rem; +.cta-strip .hero-actions { + margin-top: 0; } -.legal-hero { - padding-bottom: 1.8rem; +.legal-grid { + max-width: 54rem; } .legal-stack { display: grid; gap: 1rem; - padding-bottom: 2.2rem; } -.legal-card { - padding: 1.5rem; - border-radius: var(--radius-lg); - border: 1px solid rgba(255, 255, 255, 0.72); - background: var(--surface); - box-shadow: var(--shadow); +.site-footer { + padding-top: var(--space-9); + padding-bottom: var(--space-7); } -.legal-card p { - color: var(--muted); - line-height: 1.7; +.footer-grid { + grid-template-columns: minmax(0, 1.3fr) repeat(3, minmax(0, 0.65fr)); + padding-top: 2rem; + border-top: 1px solid rgba(24, 48, 54, 0.12); } -.legal-list { +.footer-summary p:last-child, +.footer-links li, +.footer-meta { + color: var(--page-muted); +} + +.footer-kicker { + margin-bottom: 0.85rem; + font-size: 0.85rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent-strong); +} + +.footer-links { display: grid; - gap: 0.75rem; - margin: 1rem 0 0; - padding: 0; - list-style: none; + gap: 0.6rem; } -.legal-list li { - position: relative; - padding-left: 1.2rem; - color: var(--muted); - line-height: 1.7; +.footer-links a:hover { + color: var(--page-ink); } -.legal-list li::before { - content: ""; - position: absolute; - top: 0.62rem; - left: 0; - width: 8px; - height: 8px; - border-radius: 999px; - background: var(--brand-bright); +.footer-meta { + justify-content: space-between; + margin-top: 1.6rem; + padding-top: 1rem; + border-top: 1px solid rgba(24, 48, 54, 0.08); +} + +button:disabled { + cursor: progress; + opacity: 0.72; + transform: none; } -.reveal { +[data-motion="intro"], +[data-reveal] { opacity: 0; - transform: translateY(26px); - transition: - opacity 700ms ease, - transform 700ms ease; + transform: translate3d(0, 24px, 0); } -.reveal.is-visible { +.is-loaded [data-motion="intro"] { opacity: 1; - transform: translateY(0); + transform: translate3d(0, 0, 0); + transition: + opacity 560ms ease, + transform 560ms ease; + transition-delay: var(--motion-delay, 0ms); } -@media (max-width: 1080px) { - body.nav-open { - overflow: hidden; - } +[data-reveal].is-visible { + opacity: 1; + transform: translate3d(0, 0, 0); + transition: + opacity 640ms ease, + transform 640ms ease; + transition-delay: var(--reveal-delay, 0ms); +} - .hero, - .path-grid, - .proof-grid, - .pilot-grid, - .materials-grid, - .founder-card { +@media (max-width: 1100px) { + .hero-grid, + .editorial-grid, + .path-cards, + .timeline-grid, + .workflow-grid, + .split-cards, + .compare-grid, + .materials-layout, + .booking-layout, + .pilot-split, + .footer-grid { grid-template-columns: 1fr; } - .workflow-grid, - .faq-grid { + .preview-grid, + .choice-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .site-header { - flex-wrap: wrap; + .booking-sidecard { + order: -1; } +} +@media (max-width: 980px) { .nav-toggle { display: inline-flex; - margin-left: auto; } - .site-nav { + .site-nav-shell { + position: absolute; + top: calc(100% + 0.6rem); + right: 0; + left: 0; display: none; - width: 100%; - order: 4; - flex-direction: column; - gap: 0.4rem; - padding: 0.6rem; - border-radius: 26px; - background: rgba(255, 255, 255, 0.82); - border: 1px solid rgba(255, 255, 255, 0.9); - } - - .site-nav.is-open { - display: flex; + padding: 0 1.25rem; } - .site-nav a { - padding: 0.9rem 1rem; - border-radius: 18px; - background: rgba(244, 239, 232, 0.72); + .site-nav-shell.is-open { + display: grid; } - .nav-cta { - display: none; + .site-nav-shell.is-open .site-nav, + .site-nav-shell.is-open .site-header-actions { + background: rgba(249, 244, 236, 0.98); + box-shadow: var(--shadow-strong); } - .site-nav-cta { - display: inline-flex; - align-items: center; - justify-content: center; - margin-top: 0.35rem; - background: var(--ink); - color: #fff; + .site-nav { + display: grid; + gap: 0.2rem; + padding: 0.55rem; + border-radius: 26px 26px 0 0; } - .problem-grid { - grid-template-columns: 1fr; + .site-header-actions { + display: grid; + gap: 0.75rem; + padding: 0.8rem 0.55rem 0.55rem; + border-radius: 0 0 26px 26px; + border: 1px solid rgba(24, 48, 54, 0.08); + border-top: 0; } - .founder-mark { - justify-self: start; - width: 180px; + .site-header-inner { + position: relative; } - .site-footer { - flex-direction: column; + .brand-mark span { + display: none; } +} - .footer-links { - flex-direction: column; +@media (max-width: 760px) { + :root { + --container: min(100vw - 1.25rem, 100%); } -} -@media (max-width: 720px) { .site-header { - gap: 0.7rem; - padding: 0.8rem 0.95rem; + padding: 0.75rem 0; } - .brand img { - width: 140px; + .chapter { + padding-top: var(--space-8); } - .nav-cta { - padding: 0.72rem 0.95rem; - font-size: 0.86rem; + .hero-home, + .hero-page { + padding-top: 2.6rem; } - .hero { - padding-top: 3.25rem; + .hero-actions, + .cta-strip, + .form-actions, + .cta-row, + .footer-meta { + flex-direction: column; + align-items: stretch; } - h1 { - max-width: 9ch; + .button, + .header-cta, + .language-switch { + width: 100%; } - .metric-row, - .path-grid, - .workflow-grid, - .faq-grid, - .materials-previews { - grid-template-columns: 1fr; + .hero-badges { + display: grid; } - .panel-topline { - flex-direction: column; + .field-grid, + .preview-grid, + .choice-grid, + .choice-grid-dense, + .booking-progress { + grid-template-columns: 1fr; } - .insight-band, - .proof-card, - .pilot-card, - .contact-card, - .founder-card, - .panel-frame { + .booking-form, + .success-card { padding: 1.2rem; } - .hero-badges, - .contact-tags, - .hero-actions, - .download-actions, - .contact-actions { - flex-direction: column; - } - - .button, - .hero-badges li, - .contact-tags span, - .site-nav-cta { - width: 100%; + .site-footer { + padding-bottom: var(--space-8); } } @@ -1022,16 +1303,17 @@ h3 { scroll-behavior: auto; } - .reveal, - .site-header, - .skip-link, - .nav-cta, - .button, - .nav-toggle span { - transition: none; + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; } - .reveal { + [data-motion="intro"], + [data-reveal] { opacity: 1; transform: none; } diff --git a/src/site/trust.html b/src/site/trust.html new file mode 100644 index 0000000..31859b4 --- /dev/null +++ b/src/site/trust.html @@ -0,0 +1,226 @@ + + + + + + Renvoo | Vertrouwen, datagrens en clinic-materialen + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Vertrouwen

    +

    Gebouwd om met praktijken te valideren, niet om teveel te beloven.

    +

    Deze pagina is voor de kritische route: wat Renvoo wel en niet claimt, welke data wel en niet in scope zijn, en welke clinic-materialen al klaar liggen voor een echt gesprek.

    + +
    +
    +
    + +
    +
    +

    Datagrens & rolverdeling

    +

    Nauw genoeg om werkbaar te blijven

    + +
    +
    +
    +

    Niet-klinische scope

    +

    Renvoo is ontworpen rond administratieve planning- en communicatiegegevens, niet rond diagnose, behandeladvies of klinische notities.

    +
    +

    Praktijk als controller

    +

    Voor patiënt- en afspraakgegevens in de operatie blijft de praktijk controller en werkt Renvoo als processor onder instructie.

    +
    +

    Geen patiëntplatform in v1

    +

    De eerste pilots blijven weg van patiëntaccounts, openbare marktplaatsdynamiek en onnodige consumer-features.

    +
    +
    +
    + +
    +
    +

    Bewijspositie

    +

    Wat vandaag eerlijk gezegd kan worden

    + +
    +
    +
    +
      +
    • De kerncase rust op operationele logica en scenario-math, niet op verzonnen traction.
    • Het doel van de pilot is echte reductie, herstel en admin-tijdswinst valideren met praktijkdata.
    • De founder-led motion is gericht op workflow-validatie, niet op een generieke self-serve funnel.
    • +
    +
    +
    +

    Founder

    +

    Renvoo is being built by Mohamed Ibrahim, former Electrical Subteam Lead for Team Polar at TU/e and part of the NVIDIA 6G Developer Program.

    +
    +
    +
    + +
    +
    +

    Veelgehoorde vragen

    +

    De belangrijkste bezwaren zonder startup-theater

    + +
    +
    +
    + We sturen al reminders. Wat verandert er dan? +

    Reminders zijn het startpunt, niet de oplossing. Renvoo richt zich op wat er gebeurt wanneer de reminder niet genoeg is: geen reactie, te late reactie en extra herstelwerk voor het team.

    +
    + We willen niet nog een zwaar systeem erbij. +

    Dat is precies waarom de huidige richting laagdrempelig is: CSV-first of read-only onboarding, geen patiëntaccounts in v1 en eerst een korte workflowvalidatie in plaats van een grote rollout.

    +
    + Hoe zit het met patiëntdata? +

    Die voorzichtigheid is terecht. Renvoo is bewust ontworpen rond niet-klinische administratieve planning- en communicatiegegevens. Het doel is operationele verbetering met smallere data-exposure.

    +
    + Moeten patiënten een account aanmaken? +

    Nee. De v1-posture is clinic SaaS, geen patiëntplatform. Patiëntinteractie loopt via praktijkgestuurde berichten en veilige links.

    +
    + We hebben nu geen tijd voor een grote verandering. +

    Het validatiegesprek is juist bedoeld om te zien of er genoeg operationele pijn is voor een kleine pilot, niet om meteen een zwaar verandertraject te starten.

    +
    + Hoe weten we of het echt de moeite waard is? +

    Dat is precies de pilotvraag. De huidige case is logisch, maar de pilot moet bewijzen of no-show-reductie, herstel en admin-tijdswinst in uw praktijk ook echt zichtbaar worden.

    +
    +
    +
    + +
    +
    +

    Clinic-materialen

    +

    De outreach-assets zijn al concreet genoeg voor echte gesprekken

    +

    De website is niet het enige artefact. De huidige funnel hangt samen met dezelfde one-pager, deck en operatorverhalen die ook in founder-led outreach worden gebruikt.

    +
    +
    +
    +
    +
    +

    Clinic one-pager

    +

    Compact leave-behind for operators after a short intro or follow-up email.

    + Download the one-pager +
    +

    PDF leave-behind

    +

    Ready-to-forward PDF version of the one-pager.

    + Download the PDF +
    +
    +
    +
    +
    + Preview van de Renvoo clinic one-pager +
    One-pager preview
    +
    + Preview van de eerste slide van de Renvoo clinic deck +
    Deck cover slide
    +
    + Preview van de oplossingsslide van de Renvoo clinic deck +
    Deck solution slide
    +
    +
    +
    +
    + + + +
    +
    +
    +

    Volgende stap

    +

    Als de posture klopt, plan dan het korte gesprek.

    +

    De funnel is bewust simpel: eerst begrijpen, dan vertrouwen, dan pas een gesprek aanvragen. Geen theatrale claim nodig om te zien of de workflow pijn echt genoeg is.

    +
    + +
    +
    + +
    + + + + diff --git a/tests/site-booking.test.ts b/tests/site-booking.test.ts new file mode 100644 index 0000000..d088444 --- /dev/null +++ b/tests/site-booking.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; + +import { + createBookingPayload, + createPlainTextSummary, + getFirstInvalidField, + validateBookingStep, +} from "../src/site/lib/booking.js"; + +describe("booking flow helpers", () => { + it("validates required context fields on step 1", () => { + const errors = validateBookingStep(1, { + contactName: "", + contactEmail: "not-an-email", + role: "", + clinicName: "", + city: "", + clinicSize: "", + primaryPain: "", + workflowNotes: "short", + meetingFormat: "", + availability: [], + }); + + expect(errors.contactName).toBe("required"); + expect(errors.contactEmail).toBe("email"); + expect(errors.workflowNotes).toBe("tooShort"); + expect(getFirstInvalidField(errors)).toBe("contactName"); + }); + + it("requires a format and availability on step 2", () => { + const errors = validateBookingStep(2, { + contactName: "Mohamed", + contactEmail: "mohamed@clinic.nl", + role: "owner", + clinicName: "Bright Dental", + city: "Eindhoven", + clinicSize: "small", + primaryPain: "no-shows", + workflowNotes: "Manual reminders plus a backfill spreadsheet.", + meetingFormat: "", + availability: [], + }); + + expect(errors.meetingFormat).toBe("required"); + expect(errors.availability).toBe("required"); + }); + + it("creates a stable plain-text summary for preview routing", () => { + const payload = createBookingPayload( + { + contactName: "Mohamed Ibrahim", + contactEmail: "mohamed@clinic.nl", + role: "owner", + clinicName: "Bright Dental", + city: "Eindhoven", + clinicSize: "small", + primaryPain: "no-shows", + workflowNotes: "Manual reminders plus a backfill spreadsheet.", + meetingFormat: "video", + availability: ["tue-morning", "thu-afternoon"], + }, + { + submittedAt: "2026-04-18T10:00:00.000Z", + locale: "nl-NL", + page: "pilot", + mode: "preview", + }, + ); + + const summary = createPlainTextSummary(payload, { + summaryTitle: "Renvoo booking request", + submittedAt: "Submitted at", + mode: "Mode", + contactName: "Name", + contactEmail: "Email", + role: "Role", + clinicName: "Clinic", + city: "City", + clinicSize: "Clinic size", + primaryPain: "Pain", + workflowNotes: "Workflow", + meetingFormat: "Format", + availabilityTitle: "Availability", + roles: { owner: "Clinic owner" }, + clinicSizes: { small: "2-4 chairs" }, + primaryPains: { "no-shows": "No-shows" }, + meetingFormats: { video: "Video call" }, + availability: { + "tue-morning": "Tuesday morning (09:00-11:30 CET)", + "thu-afternoon": "Thursday afternoon (13:30-16:30 CET)", + }, + }); + + expect(summary).toContain("Name: Mohamed Ibrahim"); + expect(summary).toContain("Format: Video call"); + expect(summary).toContain("Availability: Tuesday morning"); + expect(summary).toContain("Mode: preview"); + }); +});