diff --git a/static/css/marketing.css b/static/css/marketing.css
index 3aef73f..d4515bb 100644
--- a/static/css/marketing.css
+++ b/static/css/marketing.css
@@ -576,6 +576,9 @@
.mt-8 {
margin-top: calc(var(--spacing) * 8);
}
+ .mt-10 {
+ margin-top: calc(var(--spacing) * 10);
+ }
.mt-12 {
margin-top: calc(var(--spacing) * 12);
}
@@ -963,6 +966,9 @@
.min-w-\[300px\] {
min-width: 300px;
}
+ .min-w-\[720px\] {
+ min-width: 720px;
+ }
.min-w-full {
min-width: 100%;
}
@@ -1279,6 +1285,11 @@
border-color: var(--border-primary);
}
}
+ .divide-brand-border {
+ :where(& > :not(:last-child)) {
+ border-color: #e6ebf2;
+ }
+ }
.truncate {
overflow: hidden;
text-overflow: ellipsis;
@@ -1708,6 +1719,9 @@
.bg-blue-600 {
background-color: var(--color-blue-600);
}
+ .bg-brand-b3\/10 {
+ background-color: color-mix(in oklab, #00c896 10%, transparent);
+ }
.bg-brand-bg {
background-color: #f7f9fc;
}
@@ -2123,6 +2137,9 @@
.pt-6 {
padding-top: calc(var(--spacing) * 6);
}
+ .pt-8 {
+ padding-top: calc(var(--spacing) * 8);
+ }
.pt-\[62px\] {
padding-top: 62px;
}
@@ -2518,12 +2535,6 @@
color: color-mix(in oklab, var(--color-white) 50%, transparent);
}
}
- .text-white\/60 {
- color: color-mix(in srgb, #fff 60%, transparent);
- @supports (color: color-mix(in lab, red, red)) {
- color: color-mix(in oklab, var(--color-white) 60%, transparent);
- }
- }
.text-white\/70 {
color: color-mix(in srgb, #fff 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -2569,6 +2580,9 @@
.italic {
font-style: italic;
}
+ .not-italic {
+ font-style: normal;
+ }
.tabular-nums {
--tw-numeric-spacing: tabular-nums;
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
@@ -3178,6 +3192,13 @@
}
}
}
+ .hover\:bg-brand-bg\/50 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: color-mix(in oklab, #f7f9fc 50%, transparent);
+ }
+ }
+ }
.hover\:bg-emerald-700 {
&:hover {
@media (hover: hover) {
@@ -4078,6 +4099,11 @@
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
+ .md\:flex-row {
+ @media (width >= 48rem) {
+ flex-direction: row;
+ }
+ }
.md\:gap-4 {
@media (width >= 48rem) {
gap: calc(var(--spacing) * 4);
diff --git a/templates/marketing/_footer.html b/templates/marketing/_footer.html
index 7f3f605..b6a2a79 100644
--- a/templates/marketing/_footer.html
+++ b/templates/marketing/_footer.html
@@ -1,13 +1,61 @@
-')
+ footer_html = body[footer_start:footer_end]
+ assert 'text-white/70' in footer_html, "Footer text must use /70 opacity for WCAG AA"
+ # Negative regression
+ assert 'text-white/40' not in footer_html, "Footer must not regress to /40 opacity"
+ assert 'text-white/50' not in footer_html, "Footer must not regress to /50 opacity"
+
+
+def test_comparatif_section_present():
+ """Comparatif section is present after Pricing with table + sourcing footnote."""
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ assert 'comparatif-title' in body
+ assert 'COMPARATIF' in body
+ assert 'DictIA face aux solutions cloud' in body
+ # Sourcing footnote (LPC art. 219 hygiene)
+ assert 'sources publiques' in body, "Must disclose sources for competitor claims"
+ assert '2026-04-27' in body, "Must date the comparison"
+ # Trademark disclaimer
+ assert 'marques déposées' in body or 'marques déposées' in body, \
+ "Trademark disclaimer required for competitor names"
+
+
+def test_comparatif_table_has_4_competitors_and_6_criteria():
+ """Comparatif table lists DictIA + 3 competitors over 6 criteria rows."""
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ # Column headers
+ for col in ['DictIA', 'MS Teams Premium', 'Otter.ai Business', 'Whisper local']:
+ assert col in body, f"Comparatif missing column: {col}"
+ # 6 criteria (extract by their distinctive phrasing)
+ criteria_keywords = [
+ 'Conforme Loi', # row 1
+ 'Cloud Act', # row 2
+ 'Large-v3 fine-tun', # row 3 (escaped or raw)
+ 'Diarisation', # row 4
+ 'utilisateur/mois', # row 5
+ 'Audit trail' # row 6
+ ]
+ for kw in criteria_keywords:
+ assert kw in body, f"Comparatif missing criterion containing: {kw}"
+
+
+def test_comparatif_uses_responsive_overflow_scroll():
+ """Comparatif table wraps in overflow-x-auto for narrow viewports + has accessible caption."""
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ assert 'overflow-x-auto' in body
+ # Caption is sr-only but mandatory for table accessibility
+ assert '
' in body
+ # Scope attributes on column and row headers
+ assert 'scope="col"' in body
+ assert 'scope="row"' in body
+
+
+def test_conformite_section_present():
+ """Conformité forteresse section is present with 4 pillar cards."""
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ assert 'conformite-title' in body
+ assert 'CONFORMIT' in body and 'FORTERESSE' in body
+ # Soft hedge: "conçue avec" (not "certifiée par")
+ assert 'conçue avec' in body or 'conçue avec' in body, \
+ "Must use soft hedge 'conçue avec' (LPC art. 219)"
+
+
+def test_conformite_4_pillars():
+ """Conformité has 4 pillars: hébergement, Loi 25, Cadre IA, AGPL."""
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ pillar_keywords = [
+ 'OVH Beauharnois', # pillar 1
+ 'LPRPSP', # pillar 2 (Loi 25 reference)
+ 'LGGRI', # pillar 3 (Cadre IA reference)
+ 'AGPL' # pillar 4
+ ]
+ for kw in pillar_keywords:
+ assert kw in body, f"Conformité missing pillar reference: {kw}"
+ # Soft hedges (LPC art. 219)
+ assert 'Mapp' in body, "Must use 'Mappé' (not 'Certifié')"
+ # Citation/contact for verification
+ assert 'info@dictia.ca' in body
+
+
+def test_conformite_uses_wcag_safe_text_on_dark():
+ """Conformité card text uses text-white/80 minimum on bg-brand-navy."""
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ # Within the conformite section block specifically
+ section_start = body.find('id="conformite-title"')
+ # Find next
+ section_end = body.find('', section_start)
+ section_html = body[section_start:section_end]
+ assert 'text-white/80' in section_html or 'text-white/70' in section_html, \
+ "Conformité must use /70+ on dark for WCAG AA"
+
+
+def test_no_unverifiable_competitor_claims():
+ """Comparatif must NOT contain unhedged percentage claims about competitors (LPC art. 219)."""
+ client = app.test_client()
+ body = client.get('/').data.decode('utf-8')
+ # Forbidden patterns: bold quantitative claims like '5 stars', '100% accurate', 'X% precision'
+ # We allow our own '95%+' (already hedged with methodology footnote elsewhere)
+ forbidden_phrases = [
+ 'Otter.ai a 100', # No claims about Otter accuracy
+ 'Teams a 99', # No claims about Teams accuracy
+ '50% moins cher', # No comparative pricing without verification
+ ]
+ for phrase in forbidden_phrases:
+ assert phrase not in body, f"Forbidden competitive claim: {phrase}"