Visual Design System — Decision Record¶
Date: 2026-03-27 Status: Locked Depends on: Content editing decision (content_editing.md) Blocks: B3 (design system), B4 (page templates), A1 (ContentBlock types)
1. Research Basis¶
Sites Analyzed (22 sites across 5 tiers)¶
Tier 1 — Best-in-class reference (custom builds, 8.5-9.5/10):
- elone-clinic.com — Warm taupe #C5A37F, Raleway, patient-centric nav, extreme minimalism
- selarl-gadcabinetdentaire.chirurgiens-dentistes.fr — Dark theme, glassmorphism, video hero, floating pill nav
- dentistes-bastia-amurza.fr (Mediweb Premium) — Fahkwang + Montserrat, muted gold #a09d7d, 64.45% container
Tier 2 — Strong design (7-8/10):
- oralpes.com — 4-font system, teal + orange, dark teal text #094251, square buttons
- stelladentalsuite.co.uk — Jost + Cabin, warm beige #e8d4c6, sharp-cornered buttons, fixed mobile dock
- ardentis.ch — NPS score display, price transparency, 17-clinic network
Tier 3 — Award-winning international (researched via web):
- Grand Street Dental (NYC) — Serif/sans pairing, teal + yellow, editorial feel
- Zen Dental Studio (SF) — Charcoal + sand + gold at partial opacity, Tailwind-based
- Dentologie (Atlanta) — Playfair Display + Inter, HSL warm cream, no pure white anywhere
- Beehive Dental (Canada) — Poppins, warm tan #FFCFAE, full-screen video hero, 0px radius buttons
- Smile Lounge (Brooklyn) — Moneta serif + Montserrat, earth tones only
Tier 4 — French dental standard (5-6/10): - dr-addi-laurence, docteurs-nakkache-soyer, dr-balazuc-armbruster-marine - Mediweb standard sites (Lumiere, Victoire, Archident, Rosalba, Mourlaas) - dentelia.fr, minttandartsen.be
Tier 5 — Low-end baseline (2-3/10): - thomas-fortin.fr (SoLocal), Denti.site sites
Design Systems Researched¶
- shadcn/ui (CSS variable theming with OKLCH)
- Radix Themes (12-step functional color scale)
- Open Props (spacing/sizing scales)
- Tailwind CSS v4 (
@theme inline+ CSS custom properties)
Visual Inspection Notes (Chrome, 2026-03-27)¶
Sites visually inspected in-browser to validate CSS/HTML analysis:
- Elone Clinic — Confirmed: extreme minimalism, fashion-level photography (couple smiling outdoors, close-up portrait), full-bleed sections, centered text on dark backgrounds. The hero is lifestyle, not clinical. Each service is a full-viewport image tile. No visible nav — just hamburger + search.
- GAD Cabinet — Confirmed: full dark theme, video hero of modern office interior, thin-bordered rounded cards for values ("ECOUTE", "EXCELLENCE"), geometric logo, "RDV" dropdown CTA always visible.
- Oralpes — Confirmed: teal logo, office interior video hero with warm wood tones, bold orange CTA on peach referral banner, accessibility widget bottom-right.
- Stella Dental UK — Confirmed: scrolling black promo bar marquee, full-bleed team photo in plant-filled boutique interior (feels like hotel, not clinic), branded merchandise visible, warm beige treatment cards in 2-col grid with "LEARN MORE" outline buttons.
- Amurza (Mediweb Premium) — Confirmed: muted gold/olive hero background, Fahkwang uppercase headings with wide letter-spacing, outline "PRENDRE RDV" button, phone number prominent.
Critical visual takeaway: Photography quality and style is the single most impactful visual decision — more than any CSS token. The gap between Elone/Stella (lifestyle/boutique photography) and standard sites (stock or generic office photos) is immediately visible and dramatic. Real team photos in warm, well-lit environments make or break the design.
Key Finding¶
The single biggest differentiator between premium and average dental sites is warmth over clinical. Replacing cold blue-and-white with warm tones (beige, tan, teal, gold) through background tints, serif headings, generous whitespace, and real photography is what separates premium dental web design from template sites.
| Dimension | Average French Practice | Premium International |
|---|---|---|
| Color | Blue + white, clinical | Warm earth tones, curated |
| Typography | Single sans-serif | Serif/sans pairing, fluid sizing |
| Hero | Small banner ~400px | Full-viewport video/slideshow |
| Whitespace | Cramped, dense sections | Generous (10-15vh section padding) |
| Photography | Stock photos | Custom, 20+ original images |
| CTAs | Blue button, one style | Accent-colored, dual CTA, micro-interactions |
| Navigation | 8+ items, dropdowns | 4-6 items, prominent booking CTA |
| Text color | Pure black #000 |
Dark teal/charcoal #094251 or #1a1b18 |
| Backgrounds | Pure white #FFF |
Warm off-white #FAFAF8, alternating tinted sections |
2. Typography¶
Font Pairing: DM Serif Display + DM Sans¶
| Role | Font | Weight(s) | Fallback |
|---|---|---|---|
| Headings (h1-h4) | DM Serif Display | 400 (regular only) | Georgia, serif |
| Body, UI, nav | DM Sans | 400, 500, 600 | system-ui, sans-serif |
Why this pairing: - DM Serif Display — High-contrast serif with elegant curves. Designed for display/heading use. Signals authority and sophistication without being overly formal. Free (Google Fonts). Full Latin Extended support for French (e, e, e, c, oe, etc.). The serif heading is the single strongest "premium" signal found across all award-winning dental sites (Dentologie, Grand Street, Smile Lounge, Beehive all use serif headings). - DM Sans — Same design family as DM Serif Display (both from Colophon Foundry), guaranteeing automatic visual harmony. Low-contrast geometric sans-serif optimized for small-size legibility. Variable font (reduces HTTP requests). Full Latin Extended support for French. More distinctive than Inter (which is ubiquitous in AI-generated and template designs) while being equally readable.
Alternatives per practice personality (if a practice wants a different feel):
| Personality | Heading | Body |
|---|---|---|
| Warm family practice | Lora | Nunito Sans |
| Modern editorial | DM Serif Display | Instrument Sans |
| Clean geometric | Plus Jakarta Sans | Satoshi |
| Clinical premium | Outfit | General Sans |
Type Scale (Minor Third ratio, 1.2)¶
Base: 16px (1rem). Line-height: 1.625 body, 1.2 headings.
| Token | Size | rem | Use |
|---|---|---|---|
text-xs |
11px | 0.694rem | Fine print, legal, copyright |
text-sm |
13px | 0.833rem | Captions, meta, breadcrumbs |
text-base |
16px | 1rem | Body text |
text-lg |
19px | 1.2rem | Lead paragraphs, card descriptions |
text-xl |
23px | 1.44rem | H4, subheadings |
text-2xl |
28px | 1.728rem | H3 |
text-3xl |
33px | 2.074rem | H2 |
text-4xl |
40px | 2.488rem | H1 |
text-5xl |
48px | 2.986rem | Hero display |
Fluid hero sizing for mobile adaptation:
Font weights: 400 (body), 500 (emphasis/nav), 600 (subheadings/buttons). Headings use DM Serif Display at 400 (its only weight — the serif contrast provides visual weight naturally). DM Sans supports the full 100-1000 variable weight range.
Letter-spacing: 0.01em on body (slight), 0.03em on uppercase nav items, -0.02em on large headings (tightened).
3. Color Token System¶
Architecture: OKLCH + CSS Custom Properties + Tailwind v4¶
Why OKLCH: Rotating the hue channel produces visually consistent results across all 9 practice themes. The lightness channel directly correlates to perceived brightness, so WCAG contrast ratios remain predictable when only hue changes. This is the format used by shadcn/ui and recommended for Tailwind v4.
Why CSS custom properties: Per-practice theming via a class on <html> (e.g., class="theme-aubagne"). Each practice overrides 2-3 variables (primarily hue rotation). No build-time theme generation needed.
Semantic Token Set¶
Following the shadcn/ui pattern, adapted for dental/medical context:
:root {
/* === Surface / Text pairs === */
--background: oklch(0.995 0.003 80); /* warm off-white, never pure white */
--foreground: oklch(0.20 0.02 230); /* dark blue-gray, never pure black */
--card: oklch(1.0 0 0); /* white cards on off-white bg */
--card-foreground: oklch(0.20 0.02 230);
--muted: oklch(0.96 0.008 80); /* warm light bg for alternating sections */
--muted-foreground: oklch(0.45 0.02 230); /* secondary text */
/* === Brand colors (per-practice customizable) === */
--primary: oklch(0.55 0.12 195); /* default: warm teal */
--primary-foreground: oklch(0.98 0 0); /* white on primary */
--secondary: oklch(0.92 0.02 195); /* tinted light surface */
--secondary-foreground: oklch(0.30 0.03 195);
--accent: oklch(0.70 0.14 70); /* warm amber for CTAs (backgrounds/buttons only) */
--accent-foreground: oklch(0.15 0.02 70); /* dark on accent */
--accent-text: oklch(0.50 0.14 70); /* amber for inline text/links on white — passes WCAG AA */
/* === Functional === */
--destructive: oklch(0.55 0.20 25); /* errors, warnings */
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.91 0.005 80); /* warm subtle borders */
--input: oklch(0.91 0.005 80); /* form input borders */
--ring: oklch(0.55 0.12 195); /* focus ring = primary */
/* === Radius === */
--radius: 0.625rem; /* 10px base — modern, not childish */
/* === Transitions === */
--transition-base: 0.3s ease;
}
Per-Practice Theming¶
Each practice only overrides the hue channel (and optionally chroma) of --primary and --accent. Lightness stays constant to preserve contrast ratios.
/* Aubagne — warm teal (default) */
.theme-aubagne {
--primary: oklch(0.55 0.12 195);
--accent: oklch(0.70 0.14 70);
}
/* Le Canet — deeper blue */
.theme-le-canet {
--primary: oklch(0.55 0.14 235);
--accent: oklch(0.70 0.14 55);
}
/* Bodin Boulogne — sage/olive (premium feel) */
.theme-bodin {
--primary: oklch(0.55 0.10 145);
--accent: oklch(0.70 0.12 80);
}
/* David Simon Thiais — warm taupe (Elone-inspired) */
.theme-david-simon {
--primary: oklch(0.55 0.08 60);
--accent: oklch(0.70 0.14 35);
}
Rule: Each practice gets a primary hue (from SiteConfig in Aletheia) + an auto-derived accent hue (complementary or split-complementary). The --secondary, --muted, and --border tokens are derived from --primary's hue by adjusting lightness and chroma. This means a single hue value in SiteConfig generates the entire practice palette.
Tailwind v4 Integration¶
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-accent-text: var(--accent-text);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: var(--radius);
--radius-lg: calc(var(--radius) * 1.6);
--radius-xl: calc(var(--radius) * 2.4);
}
Usage in components: bg-primary text-primary-foreground, bg-muted, border-border, rounded-md.
Contrast Compliance¶
With --foreground at lightness 0.20 on --background at 0.995: contrast ratio > 12:1 (exceeds WCAG AAA).
With --primary at lightness 0.55 on white: contrast ratio ~5:1 (passes WCAG AA for large text, borderline for small text — use --foreground for small body text, --primary for headings and interactive elements).
With --accent at lightness 0.70 on white: fails for text — use only for backgrounds/buttons with --accent-foreground (lightness 0.15) on top.
With --accent-text at lightness 0.50 on white: contrast ratio ~5.5:1 (passes WCAG AA). Use for inline accent-colored text, links, and small interactive labels.
4. Spacing & Sizing Scale¶
Base Grid: 4px (0.25rem)¶
Aligns with Tailwind's default spacing scale. All spacing values are multiples of 4px.
| Token | Value | px | Primary use |
|---|---|---|---|
space-0.5 |
0.125rem | 2px | Hairline gaps, icon padding |
space-1 |
0.25rem | 4px | Tight inline gaps |
space-1.5 |
0.375rem | 6px | Small internal padding |
space-2 |
0.5rem | 8px | Compact element spacing, badge padding |
space-3 |
0.75rem | 12px | Button horizontal padding, input padding |
space-4 |
1rem | 16px | Standard gap between elements |
space-5 |
1.25rem | 20px | Card internal padding (mobile) |
space-6 |
1.5rem | 24px | Card internal padding (desktop) |
space-8 |
2rem | 32px | Between component groups |
space-10 |
2.5rem | 40px | Section title to content |
space-12 |
3rem | 48px | Between sections (mobile) |
space-16 |
4rem | 64px | Between sections (desktop) |
space-20 |
5rem | 80px | Major section breaks |
space-24 |
6rem | 96px | Page-level vertical rhythm |
Section Padding (Viewport-Relative)¶
Premium dental sites use viewport-relative padding for proportional scaling. Inspired by GAD Cabinet's 2vw-11vw approach:
--section-padding-y: clamp(3rem, 8vh, 6rem); /* vertical: 48px → 96px */
--section-padding-x: clamp(1rem, 5vw, 4rem); /* horizontal: 16px → 64px */
Container¶
--container-max: 1280px; /* card grids, multi-column layouts */
--container-content: 900px; /* body text sections — optimal 60-75 chars/line at 16px DM Sans */
--container-narrow: 768px; /* blog posts, legal pages */
--container-wide: 1440px; /* full-bleed sections with internal container */
Border Radius Scale¶
Base: --radius: 0.625rem (10px). Derived:
| Token | Value | Use |
|---|---|---|
rounded-sm |
6px | Small chips, badges |
rounded-md |
10px | Buttons, inputs, cards |
rounded-lg |
16px | Large cards, modals |
rounded-xl |
24px | Image containers, hero overlays |
Not using: 9999px pill radius (marks site as template/WordPress per research). Not using 0px sharp corners (requires extremely strong photography to not look unfinished).
5. Component Visual Language¶
5.1 Navigation¶
Pattern: Sticky header, minimal items, prominent booking CTA.
- Desktop: Fixed top,
height: 70px. White background withbackdrop-filter: blur(12px)on scroll (frosted glass, as seen in GAD, Dentologie, Amurza).box-shadow: 0 1px 0 var(--border)at rest, subtle shadow on scroll. - Mobile (< 768px): Hamburger menu. Never hide the booking CTA (anti-pattern from Balazuc). Sticky bottom bar:
[Appeler] [Prendre RDV]— inspired by Stella Dental's fixed mobile dock. - Items: 6 nav items max + CTA button. Dropdown on "Votre Besoin" and "Nos Soins".
- Typography: DM Sans 500, 14px, uppercase with
letter-spacing: 0.03em. - CTA button:
bg-accent text-accent-foreground rounded-md, padding10px 20px.
5.2 Hero¶
Pattern: Full-viewport with high-quality image or video + overlay text + dual CTA.
- Height:
min-height: 80vh(desktop),min-height: 60vh(mobile). Not 100vh (avoids content being completely hidden below fold). - Image hero: WebP,
object-fit: cover,object-position: center. Responsivesrcsetwith 3 breakpoints. - Video hero: When
video_urlis provided, renders as background<video>instead of<img>: autoplay muted loop playsinline— plays silently on load like GAD, Amurza, Oralpes.posterattribute = theimagefield (required even when video is set — serves as fallback + LQIP while video loads).object-fit: coveron the<video>element, same sizing as image hero.- Mobile: serve video only on viewports >= 768px. Below 768px, show the poster image instead (saves bandwidth, avoids battery drain). Use a
<picture>/<source media>pattern or JS check. prefers-reduced-motion: if user prefers reduced motion, show poster image instead of video.- Format: MP4 (H.264) as primary, WebM (VP9) as progressive enhancement
<source>. Max file size: 8MB (aim for 15-30s loop at 1080p). Served via Cloudflare CDN. - No sound toggle: unlike Amurza's approach, our videos are always muted (simpler, no consent issue). Sound-enabled videos use the
videoContentBlock type instead. - Overlay:
linear-gradient(to right, oklch(0.15 0.02 230 / 0.6), transparent 70%)— dark on left for text readability. Same overlay for both image and video heroes. - Content: Left-aligned. H1 in DM Serif Display at
text-5xl(hero size). Tagline in DM Sans attext-lg. Two CTAs: - Primary: "Prendre rendez-vous" —
bg-accent text-accent-foreground(warm amber) - Secondary: "01 XX XX XX XX" —
border-primary-foreground bg-transparent text-primary-foreground(outline white) - Animation: Content fades in with 0.6s stagger (heading, tagline, CTAs). Respect
prefers-reduced-motion.
5.3 Cards¶
Three card variants used across the platform:
Service Card (homepage + service hub):
[Image 16:10 ratio, rounded-xl top corners]
[Category tag — text-sm, text-primary, uppercase]
[Title — DM Serif Display, text-xl]
[Excerpt — DM Sans 400, text-base, text-muted-foreground, 2-line clamp]
[Arrow icon →]
bg-card border border-border rounded-lg. Hover: shadow-md translate-y-[-2px] with transition: var(--transition-base).
Team Card (team page):
[Photo 3:4 ratio, rounded-lg, object-fit cover]
[Name — DM Serif Display, text-lg]
[Specialty — DM Sans 400, text-sm, text-primary]
[CTA link — "Prendre RDV" text-sm]
Stat Card (trust bar):
[Number — DM Serif Display, text-4xl, text-primary]
[Label — DM Sans 500, text-sm, text-muted-foreground, uppercase]
5.4 Buttons¶
Two variants + sizes:
Primary (main CTAs): bg-accent text-accent-foreground rounded-md. Hover: brightness(1.05), translateY(-1px), shadow-sm.
Secondary (supporting actions): border border-primary text-primary bg-transparent rounded-md. Hover: bg-primary text-primary-foreground.
Ghost (tertiary, in-page links): text-primary underline-offset-4 hover:underline.
Sizes:
- sm: h-9 px-3 text-sm (nav, inline)
- md: h-11 px-5 text-base (default)
- lg: h-13 px-8 text-lg (hero, standalone CTAs)
Font: DM Sans 600, no uppercase on buttons (uppercase reserved for nav/labels).
5.5 Section Rhythm¶
Pages alternate between white and muted backgrounds to create visual rhythm:
[Hero — full-width image/video]
[Section A — bg-background (warm off-white)]
[Section B — bg-muted (warm tinted)]
[Section A — bg-background]
[Section B — bg-muted]
[CTA Section — bg-primary text-primary-foreground]
[Footer — bg-foreground text-background (dark)]
Each section: padding: var(--section-padding-y) var(--section-padding-x). Content constrained to --container-max.
5.6 Footer¶
4-column layout:
Col 1: Logo + practice description (2-3 lines) + social icons
Col 2: Navigation links (Le Cabinet, L'Equipe, Nos Soins...)
Col 3: Horaires (table: Lun-Ven + Samedi)
Col 4: Contact (address, phone, email) + mini map
---
Bottom bar: (c) 2026 [Practice Name] | Mentions legales | Politique de confidentialite | Accessibilite
bg-foreground text-background(dark footer). Links:text-muted-foreground hover:text-background.- Social icons: 24px,
text-muted-foreground hover:text-accent.
5.7 Breadcrumbs¶
On all inner pages. text-sm, text-muted-foreground. Arrow separator >. Current page in text-foreground (not linked). Schema.org BreadcrumbList markup.
Placement: Above the hero, below the nav. Sits inside a bg-background strip with padding: space-3 0 between the sticky nav and the hero/content area. Not on the homepage (root page has no breadcrumb). On service detail (L2) pages with compact hero (40vh), the breadcrumb appears between the nav and the hero image.
5.8 FAQ Accordion¶
- Question: DM Sans 600,
text-lg. Chevron icon rotates on open. - Answer: DM Sans 400,
text-base,text-muted-foreground. Max-width:--container-narrow. - Border between items:
border-b border-border. - Schema.org
FAQPagemarkup on every FAQ block. - Animation: smooth height transition, respect
prefers-reduced-motion.
5.9 Before/After¶
Image comparison slider component:
- Two images side by side with draggable divider.
- Labels: "Avant" / "Apres" in text-sm badges.
- rounded-lg overflow-hidden.
- Caption below: treatment type + link to service page (cross-linking).
5.10 CTA Block¶
Full-width call-to-action section:
- bg-primary text-primary-foreground, padding: var(--section-padding-y).
- Centered heading (DM Serif Display, text-3xl): "Prenez rendez-vous"
- Subtext: practice phone + opening hours summary.
- Two buttons (Primary amber + Secondary outline white).
- Appears at the bottom of every content page.
6. ContentBlock Types¶
These are the block_type values for the ContentBlock model's JSONField in Aletheia. Each type has a defined JSON schema for its content field.
| block_type | Description | JSON content fields |
|---|---|---|
hero |
Full-width hero with image/video + heading + CTAs | heading, tagline, image, video_url, cta_primary_text, cta_primary_url, cta_secondary_text, cta_secondary_url, overlay_position |
text |
Markdown body text | body (Markdown string) |
text_media |
50/50 text + image/video block | heading, body, media_file, media_position (left/right), media_type (image/video) |
cards_grid |
Grid of cards (services, features) | heading, cards[] (each: title, excerpt, image, url, icon) |
cta |
Call-to-action banner | heading, subtext, cta_primary_text, cta_primary_url, cta_secondary_text, cta_secondary_url, style (primary/accent) |
faq |
Accordion FAQ with schema.org | heading, items[] (each: question, answer) |
testimonials |
Patient testimonial carousel | heading, testimonials[] (each: quote, author, rating, treatment_type) |
before_after |
Before/after image comparison | heading, cases[] (each: before_image, after_image, caption, treatment_url) |
stats |
Trust metrics bar | stats[] (each: value, label, icon) |
team_grid |
Practitioner cards | heading, subtext, max_display (int), show_cta (bool) — data from Aletheia Dentist model, not duplicated |
gallery |
Image gallery with lightbox | heading, images[] (each: image, caption, category) |
map |
Embedded map + practice info | heading, show_hours (bool), show_parking (bool) — data from Aletheia Practice model |
related_services |
"Voir aussi" cross-links | heading, services[] (each: title, url, icon) — or auto-generated from Page.related_services |
video |
Embedded video player | heading, video_url, poster_image, caption |
quote |
Pull quote / highlight | quote, author, role |
Validation: Each block_type has a JSON Schema stored in the codebase (not in DB). Aletheia validates content against the schema on save. Invalid content blocks cannot be published.
7. Homepage Specification¶
Section Order (default template)¶
1. [Hero] — block_type: hero
Full-viewport, practice team photo or video
H1: "[Votre cabinet dentaire a] [Ville]"
Dual CTA: "Prendre rendez-vous" + phone number
2. [Votre Besoin] — block_type: cards_grid
"Comment pouvons-nous vous aider ?"
5 patient-need cards (icons + titles)
Links to /votre-besoin/ pages
3. [Team Preview] — block_type: team_grid
"Notre equipe"
3-4 practitioner cards (photo + name + specialty + CTA)
"Voir toute l'equipe" link
4. [Services] — block_type: cards_grid
"Nos soins"
3-4 service category cards (Implantologie, Esthetique, Parodontologie...)
Image + title + excerpt
5. [Trust Bar] — block_type: stats
4 metrics inline:
"[X] ans d'experience" | "[X] praticiens" | "[X] patients/an" | "NPS [X]"
bg-muted section
6. [Before/After] — block_type: before_after (optional)
"Resultats"
2-3 case previews + "Voir tous nos cas cliniques"
7. [Testimonials] — block_type: testimonials (optional)
"Ils nous font confiance"
3-card carousel with ratings
8. [CTA] — block_type: cta
bg-primary full-width
"Prenez rendez-vous" + phone + hours summary
9. [Map] — block_type: map
Embedded map + address + hours + parking info
Visual Rhythm¶
hero → full-bleed image/video
votre besoin → bg-background (off-white)
team → bg-muted (warm tinted)
services → bg-background
trust bar → bg-muted
before/after → bg-background
testimonials → bg-muted
cta → bg-primary (brand color, full-bleed)
map → bg-background
footer → bg-foreground (dark)
8. Service Page Specifications¶
Service Hub (L1) — e.g., /implant-dentaire-aubagne/¶
1. [Breadcrumb] Accueil > Implantologie Aubagne
2. [Hero] block_type: hero
H1: "Implantologie a [Ville]"
Hero image (treatment-specific)
No CTA in hero (too early for service pages)
3. [Introduction] block_type: text
200-word intro. Lead paragraph in text-lg.
4. [Sub-services] block_type: cards_grid
"Nos traitements en implantologie"
Grid of clickable cards (Remplacer 1 dent, All-on-4, Chirurgie guidee...)
5. [Trust/Why Us] block_type: text_media
"Pourquoi nous choisir"
50/50: text + cabinet/equipment photo
6. [FAQ] block_type: faq
2-3 most common questions
Schema.org FAQPage
7. [CTA] block_type: cta
"Prendre rendez-vous" + phone + Doctolib
Service Detail (L2) — e.g., /implant-dentaire-aubagne/chirurgie-guidee/¶
1. [Breadcrumb] Accueil > Implantologie Aubagne > Chirurgie guidee
2. [Hero] block_type: hero (compact — 40vh, not full-screen)
H1: "Chirurgie guidee a [Ville]"
3. [Content] block_type: text
400-600 words. Treatment description, procedure, benefits.
Includes inline images via Markdown.
4. [Before/After] block_type: before_after
Linked cases for this specific treatment
5. [Related] block_type: related_services
"Voir aussi" — 2-3 related treatments
(e.g., Chirurgie guidee → Mise en charge immediate, All-on-4)
6. [FAQ] block_type: faq
Treatment-specific FAQ (schema.org)
7. [CTA] block_type: cta
"Prendre rendez-vous pour une chirurgie guidee"
9. Accessibility (RGAA 4.1.2 / WCAG 2.1 AA)¶
Compliance Target¶
WCAG 2.1 AA (baseline requirement at launch). RGAA 4.1.2 (aspirational, progressive post-launch compliance). RGAA exceeds legal requirements for most individual practices (<10 employees) but: - The platform serves 9+ practices — cumulative scope matters - Accessibility is a competitive advantage (0/60 benchmarked sites are RGAA-compliant) - Required deliverables (RGAA): accessibility declaration in footer, compliance percentage, feedback mechanism
Key Technical Requirements Built Into Design System¶
| Requirement | Implementation |
|---|---|
| Color contrast | All text/bg pairs designed for > 4.5:1 (AA). --foreground on --background > 12:1. Primary used only for large text and interactive elements. |
| Focus indicators | outline: 2px solid var(--ring); outline-offset: 2px on all interactive elements. Never outline: none. |
| Touch targets | Minimum 48x48px on mobile (buttons, links, nav items). |
| Keyboard navigation | All interactive components operable via Tab, Enter, Space, Escape, Arrow keys. |
| Skip link | "Aller au contenu principal" as first focusable element, hidden until focused. |
| Landmarks | <header>, <nav>, <main>, <footer>, <aside> on every page. |
| Heading hierarchy | Single <h1> per page. Sequential h1-h6, no skipping levels. |
| Language | <html lang="fr">. lang attribute on any EN/JP content blocks. |
| Images | Alt text on all informative images. alt="" on decorative images. |
| Forms | Explicit <label> elements. <fieldset>/<legend> for groups. Linked error messages. Required field indicators. |
| Motion | Respect prefers-reduced-motion: disable fade-in animations, parallax, auto-play carousels. |
| Links | Descriptive text (no "cliquez ici"). Visually distinct from body text (underline or color + underline). |
10. Image Strategy¶
Formats & Sizes¶
| Context | Format | Sizes (srcset) | Aspect Ratio |
|---|---|---|---|
| Hero | WebP (AVIF if supported) | 640w, 1280w, 1920w | 16:9 (desktop), 4:3 (mobile crop) |
| Service card | WebP | 400w, 800w | 16:10 |
| Team portrait | WebP | 300w, 600w | 3:4 |
| Before/after | WebP | 400w, 800w | 4:3 |
| Gallery | WebP | 400w, 800w, 1200w | Varied (preserve original) |
| Logo | SVG | n/a | n/a |
| Icons | SVG (inline) | n/a | 1:1 |
Processing Pipeline (Aletheia — django-imagekit)¶
Upload original → auto-generate: 1. WebP at 3 responsive sizes 2. AVIF at 3 responsive sizes (bonus, if Pillow supports) 3. Blur placeholder (20px wide, base64-inlined for LQIP)
All images served via Cloudflare CDN with Cache-Control: public, max-age=31536000, immutable.
Video Strategy¶
Videos are supported in hero, text_media, and video block types.
| Context | Behavior | Format | Max size |
|---|---|---|---|
| Hero background | Autoplay, muted, loop, no controls | MP4 (H.264) + WebM (VP9) | 8MB, 15-30s loop, 1080p |
| text_media inline | Autoplay muted loop (same as hero) when in view | MP4 + WebM | 8MB |
| video standalone | Play on click, with controls, poster image | MP4 + WebM | 50MB (longer format, e.g., practice tour) |
Storage: Videos uploaded to Aletheia MediaFile model with media_type=video. Stored on local disk (same as images). Served via Cloudflare CDN.
No server-side transcoding at v1. Practices upload MP4 directly. Aletheia validates file size and format on upload. Future: add ffmpeg transcoding to Celery pipeline if needed.
Poster images: Required for all videos. Auto-extracted from first frame if not provided (future Celery task), or manually uploaded.
Photography Direction¶
Based on research, premium dental sites use: - Real team and office photography — never stock. This is the single most impactful visual investment. - Lifestyle over clinical — patients smiling, warm lighting, modern interiors. Not close-ups of dental instruments. - Consistent color grading — warm tones, slightly desaturated, matching the site palette. - Minimum per practice: 5 team portraits, 3 office interior shots, 1 hero image (team group shot or office exterior).
11. Animation & Interaction¶
Principles¶
- Subtle, purposeful, never gratuitous.
- All animations respect
prefers-reduced-motion. - Default transition:
var(--transition-base)=0.3s ease.
Specific Animations¶
| Element | Animation | Duration |
|---|---|---|
| Page sections | Fade-in-up on scroll (AOS-style) | 0.6s, staggered 100ms |
| Hero content | Sequential fade-in (heading → tagline → CTAs) | 0.6s, stagger 200ms |
| Cards hover | translateY(-2px) + shadow-md |
0.3s |
| Buttons hover | translateY(-1px) + brightness(1.05) |
0.2s |
| Nav on scroll | Background opacity 0 → 1 + shadow | 0.3s |
| FAQ accordion | Smooth height transition | 0.3s |
| Mobile menu | Slide-in from right | 0.3s |
| Breadcrumb separator | None (static) | — |
What We Don't Do¶
- No parallax scrolling (performance cost, accessibility issue, feels dated)
- No auto-playing video with sound (consent required)
- No infinite scroll (bad for SEO, confusing for patients)
- No decorative SVG blob animations (GAD does this well, but complexity/maintenance cost is not worth it for multi-tenant)
12. Empty States¶
Optional homepage sections (before/after, testimonials) and optional page blocks must degrade gracefully when a practice has no content for them.
Rule: Hide, Don't Show Empty¶
If a ContentBlock section has no data (e.g., zero testimonials, zero case studies), the section is not rendered at all — no heading, no empty container, no "coming soon" placeholder. The visual rhythm adjusts automatically (next section takes over the background alternation).
Implementation¶
// Pseudo-logic in Next.js page template
{testimonials.length > 0 && <TestimonialsSection data={testimonials} />}
{cases.length > 0 && <BeforeAfterSection data={cases} />}
Per-Block Minimums¶
| Block type | Minimum to render | Behavior below minimum |
|---|---|---|
testimonials |
1 testimonial | Hide entire section |
before_after |
1 case | Hide entire section |
team_grid |
1 practitioner | Hide entire section |
stats |
2 stats | Hide entire section (1 stat alone looks odd) |
gallery |
3 images | Hide entire section (1-2 images look incomplete) |
cards_grid |
1 card | Hide entire section |
faq |
1 item | Hide entire section |
related_services |
1 service | Hide entire section |
Homepage Guaranteed Sections¶
These sections must always have content (enforced by SiteConfig validation in Aletheia):
- Hero — at minimum: H1 + practice name (no image = solid
bg-primarywith white text) - Services — at minimum: the practice's enabled service categories
- CTA — always rendered (phone number from Practice model, always available)
- Map — always rendered (address from Practice model, always available)
Hero Fallback¶
If a practice has no hero image uploaded yet:
- Desktop: bg-primary solid background with text-primary-foreground. H1 + tagline + dual CTA centered.
- No gradient overlay (no image to overlay on).
- This ensures a practice can go live before professional photography is done.
13. Error Pages¶
404 — Page Not Found¶
Simple, on-brand, helpful. No illustration or gimmick.
[Nav — standard sticky header]
[Centered content, max-width: --container-content]
H1 (DM Serif Display, text-4xl): "Page introuvable"
Body (DM Sans 400, text-lg, text-muted-foreground):
"La page que vous cherchez n'existe pas ou a ete deplacee."
[Two buttons, centered, gap-4]
Primary: "Retour a l'accueil" → /
Secondary: "Nous contacter" → /contact/
[Footer — standard]
- Background:
bg-background(warm off-white). No muted/alternating sections. - Centered vertically with
min-height: 60vhon the content area. - Breadcrumb: hidden (no meaningful path to show).
- The 404 page uses the practice's theme (resolved from the Host header, same as any other page).
500 — Server Error¶
Minimal static page (no API calls, no dynamic content — it must render even when the backend is down):
[Minimal header — logo only, no nav links]
[Centered content]
H1: "Une erreur est survenue"
Body: "Nous travaillons a resoudre le probleme. Veuillez reessayer dans quelques instants."
[Single button]
Primary: "Recharger la page" → javascript:location.reload()
[Phone number — hardcoded fallback from build-time env]
"Besoin d'aide ? Appelez le [phone]"
- Must be a static HTML file (not server-rendered) so it works when Next.js itself is down.
- Uses inline styles (not Tailwind classes) to avoid CSS loading dependency.
- Practice phone number injected at build time from environment variables.
Summary of Locked Decisions¶
| Decision | Choice | Rationale |
|---|---|---|
| Heading font | DM Serif Display | Serif = premium signal. Free, full French support. |
| Body font | DM Sans | Same family as DM Serif Display, distinctive, variable font, full French support. |
| Type scale | Minor Third (1.2), 16px base | Balanced for medical content (lots of body text). |
| Color system | OKLCH + CSS custom properties | Per-practice theming via hue rotation. Predictable contrast. |
| Default palette | Warm teal primary + amber accent | Warm over clinical. Teal = trust + health. Amber CTAs = visual pop. |
| Backgrounds | Warm off-white, never pure white | Premium feel (Dentologie, Zen Dental, all Tier 1 sites). |
| Text color | Dark blue-gray, never pure black | Softer, more refined (Oralpes, Amurza, all premium sites). |
| Border radius | 10px base | Modern without being childish (9999px) or harsh (0px). |
| Spacing | 4px grid, Tailwind-aligned | Industry standard, framework-native. |
| Section padding | Viewport-relative clamp() | Proportional scaling (GAD, premium international pattern). |
| Container | 1280px max + 900px content | 1280px for grids, 900px for body text (optimal line length). |
| Nav | Sticky, 6 items max, frosted glass on scroll | Minimal, premium, always-accessible booking CTA. |
| Hero | 80vh, image/video, dual CTA | Full-viewport impact without hiding all content. |
| Buttons | Accent-colored primary, outline secondary | Warm CTA color (not primary brand color) for conversion. |
| Cards | 3 variants (service, team, stat) | Covers all template needs with consistent language. |
| ContentBlock types | 16 types | Covers all page templates. JSON-validated in Aletheia. |
| Accessibility | RGAA 4.1.2 full compliance | Competitive advantage + legal compliance. |
| Images | WebP + AVIF, 3 sizes, LQIP blur placeholder | Performance (LCP < 2.5s target). |
| Animations | Fade-in-up on scroll, hover micro-interactions | Subtle, accessible, performant. |
| Empty states | Hide sections with no data, hero fallback to solid bg-primary | Graceful degradation, no "coming soon" placeholders. |
| Error pages | 404 on-brand with nav; 500 static HTML with inline styles | 404 uses practice theme; 500 works when backend is down. |