feat(marketing): A-2.7a footer 4-col + comparatif table + conformité 4 pillars

- Replace placeholder _footer.html with full 4-column footer (Brand, Produit,
  Légal, Compte) — added aria-labelledby landmark, sr-only headings, address
  block, tel: link, external-link new-tab announcements, NBSP per OQLF
- Add Comparatif section: DictIA vs MS Teams vs Otter.ai vs Whisper local
  on 6 criteria. Sourced (politiques publiques + grilles officielles 2026-04-27),
  trademark disclaimer, responsive table with sr-only caption + scope attrs
- Add Conformité forteresse section: 4 pillars (OVH Beauharnois, mapped Loi 25
  LPRPSP refs, compatible Cadre IA LGGRI, AGPL v3 verifiable Gitea link)
- Soft hedges throughout: 'mappé', 'conçu avec', 'compatible' (no 'certifié',
  'endossé', 'reconnu' — LPC art. 219 / Competition Act s. 52 hygiene)
- 11 new tests covering footer landmarks, comparatif sourcing, conformité
  hedges, WCAG /70 contrast, and forbidden competitive claim regressions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-27 19:21:39 -04:00
parent 7d67b64ddc
commit 0d69fcd034
4 changed files with 400 additions and 17 deletions

View File

@@ -576,6 +576,9 @@
.mt-8 { .mt-8 {
margin-top: calc(var(--spacing) * 8); margin-top: calc(var(--spacing) * 8);
} }
.mt-10 {
margin-top: calc(var(--spacing) * 10);
}
.mt-12 { .mt-12 {
margin-top: calc(var(--spacing) * 12); margin-top: calc(var(--spacing) * 12);
} }
@@ -963,6 +966,9 @@
.min-w-\[300px\] { .min-w-\[300px\] {
min-width: 300px; min-width: 300px;
} }
.min-w-\[720px\] {
min-width: 720px;
}
.min-w-full { .min-w-full {
min-width: 100%; min-width: 100%;
} }
@@ -1279,6 +1285,11 @@
border-color: var(--border-primary); border-color: var(--border-primary);
} }
} }
.divide-brand-border {
:where(& > :not(:last-child)) {
border-color: #e6ebf2;
}
}
.truncate { .truncate {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -1708,6 +1719,9 @@
.bg-blue-600 { .bg-blue-600 {
background-color: var(--color-blue-600); background-color: var(--color-blue-600);
} }
.bg-brand-b3\/10 {
background-color: color-mix(in oklab, #00c896 10%, transparent);
}
.bg-brand-bg { .bg-brand-bg {
background-color: #f7f9fc; background-color: #f7f9fc;
} }
@@ -2123,6 +2137,9 @@
.pt-6 { .pt-6 {
padding-top: calc(var(--spacing) * 6); padding-top: calc(var(--spacing) * 6);
} }
.pt-8 {
padding-top: calc(var(--spacing) * 8);
}
.pt-\[62px\] { .pt-\[62px\] {
padding-top: 62px; padding-top: 62px;
} }
@@ -2518,12 +2535,6 @@
color: color-mix(in oklab, var(--color-white) 50%, transparent); 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 { .text-white\/70 {
color: color-mix(in srgb, #fff 70%, transparent); color: color-mix(in srgb, #fff 70%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -2569,6 +2580,9 @@
.italic { .italic {
font-style: italic; font-style: italic;
} }
.not-italic {
font-style: normal;
}
.tabular-nums { .tabular-nums {
--tw-numeric-spacing: 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,); 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\:bg-emerald-700 {
&:hover { &:hover {
@media (hover: hover) { @media (hover: hover) {
@@ -4078,6 +4099,11 @@
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
} }
.md\:flex-row {
@media (width >= 48rem) {
flex-direction: row;
}
}
.md\:gap-4 { .md\:gap-4 {
@media (width >= 48rem) { @media (width >= 48rem) {
gap: calc(var(--spacing) * 4); gap: calc(var(--spacing) * 4);

View File

@@ -1,13 +1,61 @@
<footer class="bg-brand-navy2 text-white py-12 mt-20"> <footer class="bg-brand-navy2 text-white py-16 mt-20" aria-labelledby="footer-heading">
<div class="max-w-[1200px] mx-auto px-6 text-center"> <h2 id="footer-heading" class="sr-only">Navigation du pied de page</h2>
<p class="text-sm text-white/60"> <div class="max-w-[1200px] mx-auto px-6">
© 2026 DictIA Inc. · 77 ch. de la Seigneurie, Inverness QC G0S 1K0 · <div class="grid md:grid-cols-4 gap-8 mb-12">
{# Column 1 — Brand + contact #}
<div>
<a href="/" class="font-black text-xl grad-text" aria-label="DictIA — page d'accueil">DictIA</a>
<p class="text-sm text-white/70 mt-3">Transcription IA conforme Loi&nbsp;25, conçue au Québec.</p>
<address class="not-italic text-xs text-white/70 mt-4 leading-relaxed">
77&nbsp;ch. de la Seigneurie<br>
Inverness QC G0S&nbsp;1K0<br>
<a href="tel:+15819968471" class="hover:text-white">(581)&nbsp;996-8471</a><br>
<a href="mailto:info@dictia.ca" class="hover:text-white">info@dictia.ca</a> <a href="mailto:info@dictia.ca" class="hover:text-white">info@dictia.ca</a>
</p> </address>
<p class="text-xs text-white/40 mt-2"> </div>
<a href="/legal/conditions" class="hover:text-white">Conditions</a> ·
<a href="/legal/confidentialite" class="hover:text-white">Confidentialité (Loi 25)</a> · {# Column 2 — Produit #}
<a href="/legal/cookies" class="hover:text-white">Cookies</a> <nav aria-label="Produit">
</p> <p class="eyebrow text-white/70 mb-4">Produit</p>
<ul class="space-y-2 text-sm text-white/70">
<li><a href="/fonctionnalites" class="hover:text-white">Fonctionnalités</a></li>
<li><a href="/tarifs" class="hover:text-white">Tarifs</a></li>
<li><a href="/conformite" class="hover:text-white">Conformité</a></li>
<li><a href="/blog" class="hover:text-white">Blog</a></li>
</ul>
</nav>
{# Column 3 — Légal #}
<nav aria-label="Légal">
<p class="eyebrow text-white/70 mb-4">Légal</p>
<ul class="space-y-2 text-sm text-white/70">
<li><a href="/legal/conditions" class="hover:text-white">Conditions d'utilisation</a></li>
<li><a href="/legal/confidentialite" class="hover:text-white">Confidentialité (Loi&nbsp;25)</a></li>
<li><a href="/legal/cookies" class="hover:text-white">Cookies</a></li>
<li><a href="/legal/remboursement" class="hover:text-white">Remboursement</a></li>
<li><a href="/legal/accessibilite" class="hover:text-white">Accessibilité</a></li>
</ul>
</nav>
{# Column 4 — Compte #}
<nav aria-label="Compte">
<p class="eyebrow text-white/70 mb-4">Compte</p>
<ul class="space-y-2 text-sm text-white/70">
<li><a href="/login" class="hover:text-white">Connexion</a></li>
<li><a href="/signup" class="hover:text-white">Créer un compte</a></li>
<li><a href="/contact" class="hover:text-white">Contact</a></li>
<li><a href="https://gitea.innova-ai.ca/Innova-AI/dictia-public" target="_blank" rel="noopener" class="hover:text-white">Code source AGPL&nbsp;<span aria-hidden="true"></span><span class="sr-only">(s'ouvre dans un nouvel onglet)</span></a></li>
</ul>
</nav>
</div>
<div class="pt-8 border-t border-white/[0.045] flex flex-col md:flex-row justify-between items-center gap-4 text-xs text-white/70">
<p>© 2026 DictIA Inc. · AGPL v3 · Fait au Québec</p>
<div class="flex gap-4">
<a href="https://www.linkedin.com/company/dictiaqc" rel="noopener" target="_blank" class="hover:text-white">LinkedIn</a>
<a href="https://www.facebook.com/dictiaqc" rel="noopener" target="_blank" class="hover:text-white">Facebook</a>
</div>
</div>
</div> </div>
</footer> </footer>

View File

@@ -283,6 +283,153 @@
</div> </div>
</div> </div>
</section> </section>
{# ===== COMPARATIF ===== #}
<section class="bg-white py-20" aria-labelledby="comparatif-title">
<div class="max-w-[1200px] mx-auto px-6">
<div class="text-center max-w-3xl mx-auto mb-12">
<p class="eyebrow grad-text mb-4">COMPARATIF</p>
<h2 id="comparatif-title" class="text-[clamp(2rem,3vw,2.75rem)] font-black mb-4 text-brand-navy">
DictIA face aux solutions cloud américaines.
</h2>
<p class="text-lg text-brand-navy/70">
Comparaison factuelle au 2026-04-27. Sources&nbsp;: politiques de confidentialité publiques + grilles tarifaires officielles. Détails sur demande&nbsp;: <a href="mailto:info@dictia.ca" class="grad-text font-semibold hover:underline">info@dictia.ca</a>.
</p>
</div>
<div class="overflow-x-auto rounded-[18px] border border-brand-border">
<table class="w-full min-w-[720px] text-sm">
<caption class="sr-only">Comparaison DictIA, Microsoft Teams Premium, Otter.ai Business, Whisper local sur 6 critères</caption>
<thead class="bg-brand-bg">
<tr>
<th scope="col" class="text-left p-4 font-bold text-brand-navy">Critère</th>
<th scope="col" class="p-4 font-bold text-brand-navy">DictIA</th>
<th scope="col" class="p-4 font-bold text-brand-navy/70">MS Teams Premium</th>
<th scope="col" class="p-4 font-bold text-brand-navy/70">Otter.ai Business</th>
<th scope="col" class="p-4 font-bold text-brand-navy/70">Whisper local (DIY)</th>
</tr>
</thead>
<tbody class="divide-y divide-brand-border">
{% for row in [
{
'critere': 'Conforme Loi&nbsp;25 sans transfert hors-Québec',
'dictia': '✓ Hébergement OVH Beauharnois',
'teams': '⚠ Soumis Cloud Act (US)',
'otter': '✗ Hébergement US',
'whisper': '✓ Aucun transfert (local)'
},
{
'critere': 'Exposé au Cloud Act US',
'dictia': '✗ Aucune exposition',
'teams': '✓ Microsoft = entité US',
'otter': '✓ Otter.ai Inc. = US',
'whisper': '✗ Local'
},
{
'critere': 'WhisperX Large-v3 fine-tuné FR-CA',
'dictia': '✓ FR-CA optimisé',
'teams': '⚠ FR générique (FR-FR)',
'otter': '✗ Anglais privilégié',
'whisper': '⚠ FR générique de base'
},
{
'critere': 'Diarisation 8&nbsp;+ locuteurs (pyannote)',
'dictia': '✓ Jusqu\'à 8',
'teams': '⚠ Limité ~6 (Premium)',
'otter': '✓ Variable',
'whisper': '✗ Non incluse'
},
{
'critere': 'Coût par utilisateur/mois',
'dictia': '0&nbsp;$ (forfait fixe)',
'teams': '~14&nbsp;$ CAD (Premium)',
'otter': '~20&nbsp;$ US (Business)',
'whisper': '0&nbsp;$ (mais GPU&nbsp;+ DevOps requis)'
},
{
'critere': 'Audit trail intégré (Loi&nbsp;25 art.&nbsp;3.5)',
'dictia': '✓ Inclus par défaut',
'teams': '⚠ Via M365 Audit séparé',
'otter': '⚠ Logs basiques seulement',
'whisper': '✗ À développer soi-même'
}
] %}
<tr class="hover:bg-brand-bg/50 transition-colors">
<th scope="row" class="text-left p-4 font-semibold text-brand-navy/80">{{ row.critere | safe }}</th>
<td class="p-4 text-center font-semibold text-brand-navy">
<span class="inline-block px-2 py-1 rounded-md bg-brand-b3/10 text-brand-navy">{{ row.dictia | safe }}</span>
</td>
<td class="p-4 text-center text-brand-navy/70">{{ row.teams | safe }}</td>
<td class="p-4 text-center text-brand-navy/70">{{ row.otter | safe }}</td>
<td class="p-4 text-center text-brand-navy/70">{{ row.whisper | safe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="text-xs text-brand-navy/70 mt-6 text-center max-w-3xl mx-auto">
Comparatif établi à partir de sources publiques (politiques de confidentialité, grilles tarifaires officielles, documentation produit) au 2026-04-27. Microsoft Teams Premium et Otter.ai sont des marques déposées de leurs propriétaires respectifs. DictIA n'est pas affilié à ces produits.
</p>
</div>
</section>
{# ===== CONFORMITÉ FORTERESSE ===== #}
<section class="bg-brand-navy text-white py-20 overflow-hidden relative" aria-labelledby="conformite-title">
{# Subtle decorative orb — green this time, like the Solution section #}
<div class="absolute top-1/3 left-1/4 w-[500px] h-[500px] rounded-full pointer-events-none" aria-hidden="true"
style="background: radial-gradient(circle, rgba(0,200,150,0.07) 0%, transparent 60%); filter: blur(60px);"></div>
<div class="relative max-w-[1200px] mx-auto px-6">
<div class="text-center max-w-2xl mx-auto mb-12">
<p class="eyebrow grad-text mb-4">CONFORMITÉ — FORTERESSE QUÉBÉCOISE</p>
<h2 id="conformite-title" class="text-[clamp(2rem,3vw,2.75rem)] font-black mb-4">
Architecture <span class="grad-text">conçue avec</span> les exigences professionnelles québécoises.
</h2>
<p class="text-lg text-white/70">
DictIA mappe son architecture aux cadres réglementaires applicables au secteur public et aux ordres professionnels du Québec. Détails techniques (EFVP, audit trail, déclaration CAI) disponibles sur demande&nbsp;: <a href="mailto:info@dictia.ca" class="grad-text font-semibold hover:underline">info@dictia.ca</a>.
</p>
</div>
{# 4 conformity pillars — dark cards with grad-bg icon corners (matches Solution pillars style) #}
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{% for card in [
{
'icon': '🇨🇦',
'title': 'Hébergement OVH Beauharnois',
'desc': 'Centre de données SOC&nbsp;2 Type&nbsp;II en territoire québécois. Aucune sortie de juridiction sans consentement explicite et tracé.'
},
{
'icon': '⚖️',
'title': 'Mappé Loi&nbsp;25 (LPRPSP)',
'desc': 'Audit trail art.&nbsp;3.5, EFVP préparée art.&nbsp;3.3, registre des consentements art.&nbsp;14. Modèles de déclaration CAI fournis.'
},
{
'icon': '🏛️',
'title': 'Compatible Cadre IA secteur public',
'desc': 'DictIA est conçu pour s\'inscrire dans le cadre de gestion des systèmes d\'IA du secteur public québécois (LGGRI). Documentation détaillée sur demande.'
},
{
'icon': '🔓',
'title': 'Code source AGPL&nbsp;v3 vérifiable',
'desc': 'Architecture entièrement auditable sur <a href="https://gitea.innova-ai.ca/Innova-AI/dictia-public" target="_blank" rel="noopener" class="underline hover:text-white">Gitea public</a>. Aucune boîte noire. Vos auditeurs peuvent examiner chaque ligne.'
}
] %}
<article class="bg-white/[0.05] backdrop-blur-sm p-6 rounded-[0.75rem] border border-white/[0.08]">
<div class="w-10 h-10 grad-bg rounded-[0.5rem] mb-4 flex items-center justify-center text-lg shadow-cta" aria-hidden="true">{{ card.icon }}</div>
<h3 class="text-lg font-bold mb-2 text-white">{{ card.title | safe }}</h3>
<p class="text-sm text-white/80 leading-relaxed">{{ card.desc | safe }}</p>
</article>
{% endfor %}
</div>
<div class="text-center mt-10">
<p class="text-sm text-white/70">
Lancement prévu&nbsp;: <strong class="text-white">printemps 2026</strong>. Pré-inscription ouverte.
</p>
</div>
</div>
</section>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@@ -439,3 +439,165 @@ def test_pricing_cta_url_no_double_slash():
for slug in ['dictia-8', 'dictia-16', 'dictia-cloud']: for slug in ['dictia-8', 'dictia-16', 'dictia-cloud']:
assert f'href="/checkout/{slug}"' in body, f"Missing single-slash href for {slug}" assert f'href="/checkout/{slug}"' in body, f"Missing single-slash href for {slug}"
assert f'href="/checkout//{slug}"' not in body, f"Double-slash regression for {slug}" assert f'href="/checkout//{slug}"' not in body, f"Double-slash regression for {slug}"
def test_footer_has_4_columns_with_aria_labels():
"""Full footer has 4 columns (Brand, Produit, Légal, Compte) with proper landmarks."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Footer landmark with accessible name
assert 'footer-heading' in body, "Footer must have an accessible heading"
assert 'aria-label="Produit"' in body
assert 'aria-label="L' in body and 'gal"' in body # Légal (handles entity-encoded é)
assert 'aria-label="Compte"' in body
# Address with tel + mailto
assert '<address' in body
assert 'href="tel:+15819968471"' in body
assert 'href="mailto:info@dictia.ca"' in body
def test_footer_links_complete():
"""Footer must link to all 4 product pages, 5 legal pages, and account flows."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Product
for url in ['/fonctionnalites', '/tarifs', '/conformite', '/blog']:
assert f'href="{url}"' in body, f"Footer missing product link {url}"
# Legal
for url in ['/legal/conditions', '/legal/confidentialite', '/legal/cookies',
'/legal/remboursement', '/legal/accessibilite']:
assert f'href="{url}"' in body, f"Footer missing legal link {url}"
# Account
for url in ['/login', '/signup', '/contact']:
assert f'href="{url}"' in body, f"Footer missing account link {url}"
# External AGPL source link
assert 'gitea.innova-ai.ca/Innova-AI/dictia-public' in body
assert 'rel="noopener"' in body, "External links must use rel=noopener"
def test_footer_external_link_screen_reader_hint():
"""External link to Gitea has sr-only hint indicating new tab opens."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# The "↗" arrow should be aria-hidden + sr-only fallback
assert "s'ouvre dans un nouvel onglet" in body or "s&#39;ouvre dans un nouvel onglet" in body, \
"External link must announce new tab opening to screen readers"
def test_footer_uses_wcag_safe_text_on_dark():
"""Footer text uses text-white/70 minimum (WCAG AA on bg-brand-navy2)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# No regression to weaker /40 or /50 in footer area
# Check the footer block specifically
footer_start = body.find('<footer')
footer_end = body.find('</footer>') + len('</footer>')
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&eacute;pos&eacute;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 '<caption class="sr-only">' 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&ccedil;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>
section_end = body.find('</section>', 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}"