diff --git a/src/marketing/routes.py b/src/marketing/routes.py index 790632a..ed57499 100644 --- a/src/marketing/routes.py +++ b/src/marketing/routes.py @@ -31,36 +31,49 @@ TESTIMONIALS = [ ] -# FAQ — 7 verifiable Q&A. Each question/answer must remain factually defensible -# (LPC art. 219). Answers reference public sources where applicable. +# FAQ — 10 verifiable Q&A enrichies depuis Website-Sanity/components/sections/dictai-page-content.tsx +# (round 3 — synchronisation avec source canonique production dictia.ca/solutions/dictai). +# Chaque question/réponse doit rester factuellement défendable (LPC art. 219). FAQ = [ { - 'q': 'Comment DictIA est-il conforme à la Loi 25 (RPRP)?', - 'a': 'DictIA héberge les données chez OVHcloud Beauharnois (Québec), produit un audit trail intégré conforme à l\'art. 3.5 LPRPSP, fournit un modèle d\'évaluation des facteurs relatifs à la vie privée (EFVP) art. 3.3, et trace les consentements art. 14. Code source AGPL v3 entièrement vérifiable.', + 'q': 'Comment fonctionne la transcription?', + 'a': 'DictIA utilise WhisperX Large-v3, le moteur de transcription de pointe d\'OpenAI, exécuté directement sur le GPU local (DictIA 8 et 16) ou sur un GPU cloud dédié au Québec (DictIA Cloud). Vous téléversez un fichier audio ou vidéo, et la transcription est générée automatiquement avec identification des locuteurs. Pour la conformité Loi 25, l\'audit trail (art. 3.5 LPRPSP), le registre des consentements (art. 14) et l\'EFVP (art. 3.3) sont fournis par défaut.', }, { - 'q': 'Quelle est la différence entre DictIA Cloud et DictIA on-premise?', - 'a': 'DictIA Cloud (à partir de 369 $/mois) est hébergé chez OVH Beauharnois — aucun matériel à gérer, opérationnel sous 48 h. DictIA on-premise (DictIA 8 et DictIA 16, à partir de 3 450 $ + 173 $/mois) tourne sur GPU dans vos murs — vos données ne sortent jamais de votre réseau.', + 'q': 'Quels formats audio/vidéo sont supportés?', + 'a': 'DictIA accepte tous les formats courants : MP3, WAV, M4A, FLAC, OGG, MP4, MKV, WEBM, et plus encore. Aucune conversion préalable nécessaire. Les exports natifs incluent DOCX, PDF, SRT, VTT, TXT, JSON et MD. Modèles spécifiques disponibles pour avocats (interrogatoire numéroté), notaires (procès-verbal d\'assemblée) et CPA (transcription d\'entrevue).', }, { - 'q': 'Quelle précision puis-je attendre pour le français québécois?', - 'a': 'DictIA utilise WhisperX Large-v3 fine-tuné sur audio professionnel québécois. La précision typique observée sur nos jeux de tests internes dépasse 95 %. La méthodologie complète (corpus, métriques WER, conditions) est disponible sur demande : info@dictia.ca.', + 'q': 'Combien de temps pour transcrire 1 heure d\'audio?', + 'a': 'Environ 2 minutes sur GPU. C\'est 99 % plus rapide que la transcription manuelle, qui prend typiquement 4 à 6 heures pour 1 heure d\'audio. La précision typique observée sur nos jeux de tests internes dépasse 95 % en français canadien. Méthodologie complète disponible sur demande : info@dictia.ca.', }, { - 'q': 'Pouvez-vous transcrire des audiences ou des interrogatoires confidentiels?', - 'a': 'Oui, à condition que vous respectiez les obligations de votre ordre (Barreau, Chambre des notaires, etc.) en matière de consentement et de confidentialité. DictIA on-premise est recommandé pour ce type d\'usage : les données ne quittent jamais votre infrastructure. Consultez votre ordre avant tout déploiement.', + 'q': 'La transcription est-elle vraiment confidentielle?', + 'a': 'Avec DictIA 8 et 16, vos données ne quittent jamais votre bureau — le traitement est 100 % local, sans connexion internet requise. Avec DictIA Cloud, les données sont hébergées exclusivement au Canada (OVH Beauharnois, QC et GCP Toronto, ON). Aucun transfert hors-frontières, zéro Cloud Act.', }, { - 'q': 'Quels formats d\'export sont supportés?', - 'a': 'DOCX, PDF, SRT, VTT, TXT, JSON et MD. Modèles spécifiques disponibles pour avocats (interrogatoire numéroté), notaires (procès-verbal d\'assemblée) et CPA (transcription d\'entrevue). Intégrations natives : Word, Outlook, Teams, Notion, Obsidian, Zapier, Make, n8n.', + 'q': 'Teams Copilot est-il légal pour mes réunions?', + 'a': 'Non. Teams Copilot envoie les transcriptions vers des serveurs Microsoft soumis au Cloud Act américain. La Loi 25 (art. 44-45) exige un consentement explicite pour transmettre des données biométriques (voix) hors du Québec. Depuis septembre 2023, toute transcription sur Teams Copilot est en violation — sans exception.', }, { - 'q': 'Que se passe-t-il si je résilie mon abonnement?', - 'a': 'Vos données restent exportables pendant 90 jours après résiliation (DOCX/PDF/JSON). Passé ce délai, suppression définitive avec confirmation écrite — politique conforme à l\'art. 23 LPRPSP. Aucun frais de résiliation. Détails dans nos conditions d\'utilisation.', + 'q': 'Otter.ai est-il en violation?', + 'a': 'Oui. Otter.ai héberge les données sur AWS us-east-1 (Virginie, USA). Vos enregistrements de réunions — y compris les discussions confidentielles avec vos clients — transitent et sont stockés sur des serveurs américains soumis au Cloud Act. C\'est une violation de la Loi 25 depuis septembre 2023.', }, { - 'q': 'Le code source est-il vraiment open source AGPL v3?', - 'a': 'Oui. Code source complet sur Gitea public. Conséquence pratique de l\'AGPL : tout fork hébergé doit publier ses modifications. Transparence vérifiable par vos auditeurs internes ou un tiers de confiance.', + 'q': 'Que dit le Barreau du Québec sur l\'IA?', + 'a': 'En octobre 2024, le Barreau a émis une directive interdisant explicitement l\'utilisation d\'outils IA qui envoient des données client vers des serveurs étrangers. Une violation peut entraîner des sanctions disciplinaires. DictIA est conçu comme une solution conforme au Code de déontologie du Barreau (architecture mappée — voir notre page Conformité).', + }, + { + 'q': 'DictIA s\'intègre-t-il à Clio Manage ou PCLaw?', + 'a': 'L\'intégration native Clio Manage est prévue pour Q1 2026. En attendant, DictIA exporte nativement en DOCX, compatible avec tous les logiciels de gestion de dossiers. L\'importation manuelle prend moins de 30 secondes par transcription. Intégrations natives disponibles : Word, Outlook, Teams, Notion, Obsidian, Zapier, Make, n8n.', + }, + { + 'q': 'Ai-je besoin de connaissances techniques?', + 'a': 'Non. DictIA est une solution clé en main : nous fournissons le matériel (solutions locales), installons tout sur site, formons votre équipe et assurons la maintenance mensuelle à distance. Vous n\'avez besoin d\'aucune expertise technique. En cas de résiliation, vos données restent exportables pendant 90 jours (art. 23 LPRPSP).', + }, + { + 'q': 'DictIA est-il open source?', + 'a': 'Oui. Le code source est sous licence AGPL v3 — transparence totale. La stack complète (WhisperX, pyannote, Mistral, Ollama, FastAPI, PostgreSQL) est 100 % open source, sans aucune redevance logicielle. Code source complet sur Gitea public. Conséquence pratique de l\'AGPL : tout fork hébergé doit publier ses modifications.', }, ] diff --git a/static/css/marketing.css b/static/css/marketing.css index ab7550a..6eeb75a 100644 --- a/static/css/marketing.css +++ b/static/css/marketing.css @@ -361,8 +361,8 @@ .sticky { position: sticky; } - .-inset-8 { - inset: calc(var(--spacing) * -8); + .-inset-1 { + inset: calc(var(--spacing) * -1); } .inset-0 { inset: calc(var(--spacing) * 0); @@ -370,6 +370,9 @@ .inset-\[-8px\] { inset: -8px; } + .inset-\[30\%\] { + inset: 30%; + } .inset-x-0 { inset-inline: calc(var(--spacing) * 0); } @@ -382,17 +385,14 @@ .end { inset-inline-end: var(--spacing); } - .-top-1 { - top: calc(var(--spacing) * -1); - } .-top-1\.5 { top: calc(var(--spacing) * -1.5); } .-top-3 { top: calc(var(--spacing) * -3); } - .-top-10 { - top: calc(var(--spacing) * -10); + .-top-12 { + top: calc(var(--spacing) * -12); } .top-0 { top: calc(var(--spacing) * 0); @@ -424,6 +424,9 @@ .top-16 { top: calc(var(--spacing) * 16); } + .top-\[12\%\] { + top: 12%; + } .top-\[42px\] { top: 42px; } @@ -436,8 +439,8 @@ .-right-1\.5 { right: calc(var(--spacing) * -1.5); } - .-right-6 { - right: calc(var(--spacing) * -6); + .-right-12 { + right: calc(var(--spacing) * -12); } .right-0 { right: calc(var(--spacing) * 0); @@ -469,8 +472,8 @@ .right-\[3\%\] { right: 3%; } - .-bottom-10 { - bottom: calc(var(--spacing) * -10); + .right-\[6\%\] { + right: 6%; } .bottom-0 { bottom: calc(var(--spacing) * 0); @@ -487,9 +490,6 @@ .bottom-full { bottom: 100%; } - .-left-10 { - left: calc(var(--spacing) * -10); - } .left-0 { left: calc(var(--spacing) * 0); } @@ -586,9 +586,6 @@ .-mx-4 { margin-inline: calc(var(--spacing) * -4); } - .mx-1 { - margin-inline: calc(var(--spacing) * 1); - } .mx-3 { margin-inline: calc(var(--spacing) * 3); } @@ -673,9 +670,6 @@ .mr-4 { margin-right: calc(var(--spacing) * 4); } - .-mb-\[0\.5rem\] { - margin-bottom: calc(0.5rem * -1); - } .-mb-px { margin-bottom: -1px; } @@ -880,9 +874,6 @@ .h-\[450px\] { height: 450px; } - .h-\[460px\] { - height: 460px; - } .h-\[500px\] { height: 500px; } @@ -946,6 +937,9 @@ .min-h-\[8rem\] { min-height: 8rem; } + .min-h-\[85vh\] { + min-height: 85vh; + } .min-h-\[110px\] { min-height: 110px; } @@ -1027,9 +1021,6 @@ .w-56 { width: calc(var(--spacing) * 56); } - .w-64 { - width: calc(var(--spacing) * 64); - } .w-72 { width: calc(var(--spacing) * 72); } @@ -1045,6 +1036,9 @@ .w-\[68px\] { width: 68px; } + .w-\[80px\] { + width: 80px; + } .w-\[88px\] { width: 88px; } @@ -1233,9 +1227,6 @@ .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } - .animate-plus-breathe { - animation: plus-breathe 2s ease-in-out infinite; - } .animate-pulse { animation: var(--animate-pulse); } @@ -1304,15 +1295,15 @@ .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } + .grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } .grid-cols-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); } .grid-cols-\[2fr_2fr_3fr\] { grid-template-columns: 2fr 2fr 3fr; } - .grid-cols-\[180px_1fr_170px\] { - grid-template-columns: 180px 1fr 170px; - } .flex-col { flex-direction: column; } @@ -1379,9 +1370,6 @@ .gap-8 { gap: calc(var(--spacing) * 8); } - .gap-12 { - gap: calc(var(--spacing) * 12); - } .gap-\[1\.5px\] { gap: 1.5px; } @@ -1413,6 +1401,13 @@ margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-2\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-3 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -1631,6 +1626,9 @@ --tw-border-style: dashed; border-style: dashed; } + .\!border-transparent { + border-color: transparent !important; + } .border-\[var\(--bg-accent\)\] { border-color: var(--bg-accent); } @@ -1712,6 +1710,9 @@ .border-brand-b2\/40 { border-color: color-mix(in oklab, #00bdd8 40%, transparent); } + .border-brand-b3\/15 { + border-color: color-mix(in oklab, #00c896 15%, transparent); + } .border-brand-b3\/60 { border-color: color-mix(in oklab, #00c896 60%, transparent); } @@ -1823,6 +1824,12 @@ border-color: color-mix(in oklab, var(--color-white) 6%, transparent); } } + .border-white\/\[0\.07\] { + border-color: color-mix(in srgb, #fff 7.000000000000001%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 7.000000000000001%, transparent); + } + } .border-white\/\[0\.08\] { border-color: color-mix(in srgb, #fff 8%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2015,6 +2022,9 @@ .bg-brand-b1\/\[0\.06\] { background-color: color-mix(in oklab, #0062ff 6%, transparent); } + .bg-brand-b3 { + background-color: #00c896; + } .bg-brand-b3\/10 { background-color: color-mix(in oklab, #00c896 10%, transparent); } @@ -2054,12 +2064,6 @@ .bg-brand-navy2 { background-color: #0b1525; } - .bg-emerald-500\/20 { - background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-emerald-500) 20%, transparent); - } - } .bg-emerald-600 { background-color: var(--color-emerald-600); } @@ -2255,12 +2259,6 @@ background-color: color-mix(in oklab, var(--color-white) 6%, transparent); } } - .bg-white\/\[0\.08\] { - background-color: color-mix(in srgb, #fff 8%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 8%, transparent); - } - } .bg-yellow-50 { background-color: var(--color-yellow-50); } @@ -2315,6 +2313,10 @@ --tw-gradient-from: color-mix(in oklab, #0062ff 6%, transparent); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } + .from-brand-b3 { + --tw-gradient-from: #00c896; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } .from-orange-500 { --tw-gradient-from: var(--color-orange-500); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -2354,6 +2356,10 @@ --tw-gradient-to: var(--color-amber-600); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); } + .to-brand-b1 { + --tw-gradient-to: #0062ff; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } .to-brand-b3\/10 { --tw-gradient-to: color-mix(in oklab, #00c896 10%, transparent); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); @@ -2533,6 +2539,9 @@ .pt-4 { padding-top: calc(var(--spacing) * 4); } + .pt-5 { + padding-top: calc(var(--spacing) * 5); + } .pt-6 { padding-top: calc(var(--spacing) * 6); } @@ -2572,9 +2581,6 @@ .pb-0 { padding-bottom: calc(var(--spacing) * 0); } - .pb-1 { - padding-bottom: calc(var(--spacing) * 1); - } .pb-2 { padding-bottom: calc(var(--spacing) * 2); } @@ -2638,6 +2644,9 @@ .font-mono { font-family: JetBrains Mono Variable, JetBrains Mono, monospace; } + .font-sans { + font-family: Inter Variable, Inter, system-ui, sans-serif; + } .text-2xl { font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); @@ -2684,6 +2693,9 @@ .text-\[9px\] { font-size: 9px; } + .text-\[10\.5px\] { + font-size: 10.5px; + } .text-\[10px\] { font-size: 10px; } @@ -2708,9 +2720,6 @@ .text-\[clamp\(1\.75rem\,2\.5vw\,2\.25rem\)\] { font-size: clamp(1.75rem, 2.5vw, 2.25rem); } - .text-\[clamp\(2\.5rem\,4vw\,4rem\)\] { - font-size: clamp(2.5rem, 4vw, 4rem); - } .text-\[clamp\(2\.25rem\,4vw\,3\.5rem\)\] { font-size: clamp(2.25rem, 4vw, 3.5rem); } @@ -2727,6 +2736,10 @@ --tw-leading: calc(var(--spacing) * 5); line-height: calc(var(--spacing) * 5); } + .leading-\[0\.92\] { + --tw-leading: 0.92; + line-height: 0.92; + } .leading-\[1\.05\] { --tw-leading: 1.05; line-height: 1.05; @@ -2771,10 +2784,6 @@ --tw-tracking: 0.2em; letter-spacing: 0.2em; } - .tracking-\[0\.12em\] { - --tw-tracking: 0.12em; - letter-spacing: 0.12em; - } .tracking-\[0\.14em\] { --tw-tracking: 0.14em; letter-spacing: 0.14em; @@ -2827,6 +2836,9 @@ .whitespace-pre-wrap { white-space: pre-wrap; } + .\!text-white { + color: var(--color-white) !important; + } .text-\[var\(--bg-accent\)\] { color: var(--bg-accent); } @@ -2989,9 +3001,6 @@ .text-cyan-300 { color: var(--color-cyan-300); } - .text-emerald-300 { - color: var(--color-emerald-300); - } .text-emerald-500 { color: var(--color-emerald-500); } @@ -3052,9 +3061,6 @@ .text-red-100 { color: var(--color-red-100); } - .text-red-300 { - color: var(--color-red-300); - } .text-red-400 { color: var(--color-red-400); } @@ -3151,6 +3157,12 @@ color: color-mix(in oklab, var(--color-white) 60%, transparent); } } + .text-white\/65 { + color: color-mix(in srgb, #fff 65%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 65%, transparent); + } + } .text-white\/70 { color: color-mix(in srgb, #fff 70%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -3163,10 +3175,10 @@ color: color-mix(in oklab, var(--color-white) 80%, transparent); } } - .text-white\/90 { - color: color-mix(in srgb, #fff 90%, transparent); + .text-white\/85 { + color: color-mix(in srgb, #fff 85%, transparent); @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 90%, transparent); + color: color-mix(in oklab, var(--color-white) 85%, transparent); } } .text-yellow-400 { @@ -3225,20 +3237,6 @@ color: var(--text-muted); } } - .placeholder-white\/40 { - &::-moz-placeholder { - color: color-mix(in srgb, #fff 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 40%, transparent); - } - } - &::placeholder { - color: color-mix(in srgb, #fff 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, var(--color-white) 40%, transparent); - } - } - } .accent-brand-b1 { accent-color: #0062ff; } @@ -3326,12 +3324,6 @@ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .shadow-black\/40 { - --tw-shadow-color: color-mix(in srgb, #000 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent); - } - } .ring-\[var\(--border-accent\)\] { --tw-ring-color: var(--border-accent); } @@ -3359,10 +3351,6 @@ --tw-blur: blur(8px); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } - .blur-3xl { - --tw-blur: blur(var(--blur-3xl)); - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } .filter { filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } @@ -3494,6 +3482,13 @@ } } } + .group-hover\:text-brand-b1 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: #0062ff; + } + } + } .group-hover\:opacity-60 { &:is(:where(.group):hover *) { @media (hover: hover) { @@ -3711,6 +3706,16 @@ } } } + .hover\:border-white\/30 { + &:hover { + @media (hover: hover) { + border-color: color-mix(in srgb, #fff 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 30%, transparent); + } + } + } + } .hover\:border-yellow-500\/30 { &:hover { @media (hover: hover) { @@ -4022,16 +4027,6 @@ } } } - .hover\:bg-white\/\[0\.04\] { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, #fff 4%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 4%, transparent); - } - } - } - } .hover\:bg-white\/\[0\.05\] { &:hover { @media (hover: hover) { @@ -4058,6 +4053,14 @@ } } } + .hover\:from-brand-b1 { + &:hover { + @media (hover: hover) { + --tw-gradient-from: #0062ff; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + } + } .hover\:from-orange-600 { &:hover { @media (hover: hover) { @@ -4090,6 +4093,14 @@ } } } + .hover\:to-brand-b3 { + &:hover { + @media (hover: hover) { + --tw-gradient-to: #00c896; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + } + } .hover\:to-purple-600 { &:hover { @media (hover: hover) { @@ -4516,6 +4527,11 @@ outline-color: var(--color-red-700); } } + .focus-visible\:outline-white { + &:focus-visible { + outline-color: var(--color-white); + } + } .active\:scale-95 { &:active { --tw-scale-x: 95%; @@ -4669,6 +4685,11 @@ width: calc(var(--spacing) * 40); } } + .sm\:w-auto { + @media (width >= 40rem) { + width: auto; + } + } .sm\:max-w-\[80px\] { @media (width >= 40rem) { max-width: 80px; @@ -4689,11 +4710,26 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); } } + .sm\:grid-cols-4 { + @media (width >= 40rem) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } + .sm\:grid-cols-8 { + @media (width >= 40rem) { + grid-template-columns: repeat(8, minmax(0, 1fr)); + } + } .sm\:flex-row { @media (width >= 40rem) { flex-direction: row; } } + .sm\:flex-nowrap { + @media (width >= 40rem) { + flex-wrap: nowrap; + } + } .sm\:items-center { @media (width >= 40rem) { align-items: center; @@ -5118,6 +5154,12 @@ line-height: var(--tw-leading, var(--text-sm--line-height)); } } + .md\:text-xl { + @media (width >= 48rem) { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + } .md\:text-\[10px\] { @media (width >= 48rem) { font-size: 10px; @@ -5224,31 +5266,16 @@ grid-template-columns: 1fr 240px; } } - .lg\:grid-cols-\[1fr_minmax\(0\,560px\)\] { - @media (width >= 64rem) { - grid-template-columns: 1fr minmax(0,560px); - } - } .lg\:flex-row { @media (width >= 64rem) { flex-direction: row; } } - .lg\:justify-start { - @media (width >= 64rem) { - justify-content: flex-start; - } - } .lg\:gap-10 { @media (width >= 64rem) { gap: calc(var(--spacing) * 10); } } - .lg\:gap-16 { - @media (width >= 64rem) { - gap: calc(var(--spacing) * 16); - } - } .lg\:border-r { @media (width >= 64rem) { border-right-style: var(--tw-border-style); @@ -5280,9 +5307,10 @@ padding-bottom: calc(var(--spacing) * 8); } } - .lg\:text-left { + .lg\:text-3xl { @media (width >= 64rem) { - text-align: left; + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); } } .xl\:grid-cols-4 { @@ -6132,14 +6160,6 @@ transform: translateY(0); } } -@keyframes plus-breathe { - 0%, 100% { - transform: scale(1); - } - 50% { - transform: scale(1.05); - } -} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { diff --git a/templates/marketing/_partials/_pricing_tiers.html b/templates/marketing/_partials/_pricing_tiers.html index a11e00f..020bdb8 100644 --- a/templates/marketing/_partials/_pricing_tiers.html +++ b/templates/marketing/_partials/_pricing_tiers.html @@ -7,7 +7,7 @@ 'DictIA 8', '3 450 $', '173 $', - 'PME · RH · Manufacturiers', + 'PME · Manufacturiers · RH · Services — local, vos données ne quittent jamais votre bureau.', ['GPU 8 Go RTX', 'Volume illimité', 'WhisperX FR-CA', 'Diarisation 8 locuteurs', 'Support inclus'] ) }} {{ pricing_card( @@ -15,7 +15,7 @@ 'DictIA 16', '5 750 $', '201 $', - 'Cabinets juridiques · CPA · Finance', + 'Cabinets juridiques · CPA · Services financiers — local, Mistral 7B sur votre GPU.', ['GPU 16 Go RTX', 'Mistral 7B local', 'Q&R sur enregistrement', 'Tout DictIA 8', 'Support prioritaire'], recommended=True ) }} @@ -24,7 +24,7 @@ 'DictIA Cloud', '0 $', '369 $', - 'Organismes · Municipalités · Multi-sites', + 'Organismes · Municipalités · Multi-sites — Cloud QC, opérationnel en 48 h, aucun matériel requis.', ['Hébergé OVH Beauharnois (Québec)', 'Opérationnel sous 48 h', 'Aucun matériel à gérer', 'SLA visé 99,9 %', 'Conforme Loi 25'] ) }} diff --git a/templates/marketing/landing.html b/templates/marketing/landing.html index b578278..1650f89 100644 --- a/templates/marketing/landing.html +++ b/templates/marketing/landing.html @@ -4,35 +4,146 @@ {% block description %}DictIA transcrit vos réunions confidentielles 100% au Québec. Conforme Loi 25, Barreau, CPA, ChAD. Conçu avec 9 ordres professionnels — lancement printemps 2026.{% endblock %} {% block content %} -{# ===== HERO ===== #} -{# Local keyframes scoped to the hero — audio progress bar loops 50%→75%→50% over 15s #} +{# ===== HERO — round 3 : reproduction fidèle dictia.ca/solutions/dictai ===== #} +{# Source : InnovA-AI/Website-Sanity/components/sections/dictai-page-content.tsx (lignes 260-518) + Animations Framer Motion → CSS pure + Alpine.js : + 1. 3-step flow auto-cycle 1→2→3 (setInterval 1.8 s, désactivé reduced-motion) + 2. Magnetic CTA primary (mousemove → translate, max 8 px) + 3. Mouse parallax orb (mousemove window → translate, inertie via CSS transition) + 4. Shockwave on click (CSS pseudo-element, scale 0→4 + opacity 1→0) + 5. Hero title fade-in + word-staggered animation #} -
+
+ {# Shockwave overlays — fixed-position ripples on CTA click #} + + {# Cosmic orbs background — 3 radial gradients (blue 16%, cyan 7%, green 11%) + subtle grid + horizontal accent line #} -
- {# 2-col grid on lg+: text left, app mockup right. On - {# ---------- COLUMN LEFT: hero copy ---------- #} -
- {# Eyebrow with gradient text and 0.18em tracking #} -

- TRANSCRIPTION IA · CONFORME LOI 25 · QUÉBEC -

+ {# 3D abstract orb — reacts to mouse via Alpine ox/oy → CSS variables #} + - {# H1 — clamp typography + grad-text accent on key phrase #} -

- Transcrivez vos réunions - sans risquer votre permis. -

+
+ {# Single-column hero — texte centré (lg : aligné gauche). Le visuel canonique est le 3-step flow inline (pas de mockup app). #} +
- {# Sub-headline — ≤25 words, value prop #} -

- DictIA convertit vos audio en texte, résumé et points d'action — 100% au Québec, conforme Barreau, CPA Québec, ChAD et 6 autres ordres professionnels. -

+ {# Eyebrow / breadcrumb back-link "Toutes les solutions" — link to anchor #solutions sur landing #} + + + Toutes les solutions + - {# Dual CTA — primary (demo) + secondary ghost (pricing) #} -
- {% from 'macros/button.html' import button %} - {{ button('Réserver une démo', href='/contact', variant='primary', size='lg') }} - {{ button('Voir les tarifs', href='/tarifs', variant='ghost', size='lg', icon='') }} -
+ {# Sous-eyebrow — pillars #} +
+ + TRANSCRIPTION IA · CONFORME LOI 25 · QUÉBEC + + +
+

+ Audio → Texte · Résumés IA · Conforme Loi 25 & ordres professionnels +

- {# Social proof microcopy — defensible: refers to pre-launch waitlist + factual ordres pros count #} -

- - - Conçu avec 9 ordres professionnels québécois - - · - Pré-inscription ouverte - · - Lancement printemps 2026 -

+ {# Brand wordmark large — Dict + IA accent #} +

+ DictIA +

+ + {# 3-step flow inline — REMPLACE le mockup app actuel. Auto-cycle 1→2→3 toutes les 1.8s. #} +
+ {% set flow_steps = [ + ('Importez un fichier', ''), + ('Texte en 2 min', ''), + ('Résumé + actions', '') + ] %} + {% for label, icon in flow_steps %} + + + {{ label | safe }} + + {% if not loop.last %} + + {% endif %} + {% endfor %}
- {# ---------- COLUMN RIGHT: app mockup (lg+ only) ---------- #} - {# /column right #} -
{# /grid #}
@@ -1075,11 +1062,14 @@

COMMENT ÇA MARCHE

- Du fichier au résumé en 4 étapes. + Du fichier au résumé — en temps réel.

Aucune installation côté utilisateur, aucune conversion préalable. DictIA orchestre l'ensemble du pipeline — du téléversement à l'export — en moins de deux minutes pour une heure d'audio.

+

+ Survolez une fonctionnalité pour voir la machine en action. Glissez pour calculer votre gain de productivité. +

{# Pipeline track + 4 nodes — Alpine state drives all visuals. @@ -1235,6 +1225,82 @@
+{# ===== LANGUES + IA LOCALE ===== #} +{# Section compacte : grille 99+ langues détectées (gauche) + carte IA Mistral 7B LOCAL (droite). #} +
+
+
+ {# Colonne gauche — 99+ langues #} +
+
+ +
+

99+ langues détectées

+

WhisperX Large-v3 · multilingue par défaut

+
+
+ + {# Grille codes langue — 12 puces (FR mis en avant via grad-bg) #} +
    + {% for code in ['FR','EN','ES','DE','PT','IT','NL','PL','ZH','JA','KO','AR','RU','HI','TR','VI','TH','SV','DA','NO','FI'] %} +
  • + + {{ code }} + +
  • + {% endfor %} +
+ +

+ + Auto · Détection automatique de la langue à l'upload +

+
+ + {# Colonne droite — IA intégrée Mistral 7B LOCAL #} +
+ {# Subtle orb décoratif #} + + +
+
+ +
+

IA intégrée — Mistral 7B (LOCAL)

+

Inférence sur votre GPU · zéro cloud étranger

+
+
+ +
    +
  • + + Résumé · Points d'action · Q&R +
  • +
  • + + Données hébergées sur vos serveurs · jamais partagées +
  • +
  • + + Zéro connexion OpenAI · Google · Microsoft +
  • +
  • + + Inférence hors-ligne · résultats en secondes +
  • +
+
+
+
+
+
+ {# ===== BENTO FEATURES ===== #}
@@ -1472,6 +1538,10 @@ {% include 'marketing/_partials/_pricing_tiers.html' %} +

+ Tous les prix en CAD, taxes en sus (TPS 5 % + TVQ 9,975 %). +

+ {# ROI CALCULATOR — Alpine.js, hypotheses transparentes pour LPC art. 219 hygiene #}

CALCULATEUR ROI

@@ -1633,8 +1703,22 @@ Architecture conçue avec les exigences professionnelles québécoises.

- 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 : info@dictia.ca. + DictIA mappe son architecture aux cadres réglementaires applicables au secteur public et aux ordres professionnels du Québec. DictIA a été conçu pour les secteurs réglementés du Québec — Loi 25, Cloud Act, Barreau, ChAD, AMF. Détails techniques (EFVP, audit trail, déclaration CAI) disponibles sur demande : info@dictia.ca.

+ + {# Chips claims — 3 marqueurs canoniques (~192 000 pros · 5 ordres · 0 donnée hors-Québec) #} +
+ {% for chip in [ + ('~192 000 professionnels Tier 1', ''), + ('5 ordres · directives IA formelles', ''), + ('0 donnée transmise hors Québec', '') + ] %} + + + {{ chip[0] | safe }} + + {% endfor %} +
{# 4 conformity pillars — dark cards with grad-bg icon corners (matches Solution pillars style). @@ -1910,6 +1994,140 @@
+{# ===== CÉGEPS · CONFORMITÉ AU 19 JUIN 2026 (spotlight) ===== #} +{# Source canonique : texte fourni par l'utilisateur — Cadre IA MCN, Énoncé de principes mis à jour + + Indication d'application IAG (publié 19 déc. 2025, conformité 19 juin 2026, art. 21 LGGRI). #} + +
+
+ + {# Bandeau d'eyebrow + badge pulse #} +
+ + + Conformité imminente + + + Adopté 19 déc. 2025 · Décret officiel + +
+ +
+

+ Conformité au 19 juin 2026 — vous dirigez un cégep, un CISSS ou un ministère ? +

+

+ Vous avez jusqu'au 19 juin 2026. +

+

+ Depuis le 19 décembre 2025, tous les organismes publics québécois doivent appliquer un cadre IA strict. Aucun outil cloud non approuvé ne peut recevoir de renseignements confidentiels — fini ChatGPT ou Teams Copilot dans les CA, les séances cliniques ou les comités universitaires. +

+
+ + {# Carte spotlight — Cadre IA MCN détaillé #} +
+
+ +
+

+ Cadre IA MCN — Énoncé de principes mis à jour + Indication d'application IAG +

+

Publié le 19 décembre 2025 sous l'art. 21 LGGRI

+
+
+ +

+ Interdit aux organismes publics (art. 2 LGGRI) d'entrer des renseignements confidentiels dans un système IA non approuvé — ministères, organismes budgétaires, Santé Québec, CISSS/CIUSSS, centres de services scolaires, cégeps, universités. Régime allégé pour entreprises du gouvernement (art. 4 — Hydro-Québec, SAQ, Loto-Québec, CDPQ). +

+ +
    + {% for bullet in [ + ('Énoncé de principes — 12 principes éthiques applicables à tout système IA dans l\'administration publique', 0), + ('Indication d\'application IAG — gouvernance, gestion des risques, mesures de contrôle, protection des données, formation du personnel', 80), + ('Délai conformité : 19 juin 2026 (6 mois post publication 19 déc. 2025)', 160), + ('Municipalités, MRC et Assemblée nationale non visées par l\'Énoncé — mais Loi sur l\'accès (A-2.1) reste applicable aux séances publiques', 240), + ('Loi 25 — voix = donnée biométrique (LCCJTI art. 44-45), déclaration CAI obligatoire si banque biométrique', 320), + ('Loi 96 (C-11) — documents générés en français pour organisations 25+ employés', 400), + ('Hébergement au Québec — aucune société US dans la chaîne (Cloud Act inapplicable)', 480) + ] %} +
  • + + {{ bullet[0] | safe }} +
  • + {% endfor %} +
+ + {# Liste organismes visés (chips) #} +
+

Organismes visés par le Cadre IA MCN

+
+ {% for org in ['Ministères', 'Santé Québec', 'CISSS/CIUSSS', 'Universités', 'Cégeps', 'Hydro-Québec', 'SAQ', 'SAAQ', 'CDPQ'] %} + {{ org }} + {% endfor %} +
+
+
+
+
+ +{# ===== PARTENAIRE DE CONFIANCE — CyberPerformance ===== #} +
+
+
+ + Partenaire de confiance + + +
+ + + {# Icône SVG handshake générique (pas d'emoji, pas de logo bitmap) #} + + + + + + CyberPerformance + Marketing numérique · Lévis, QC + + + + cyberperformance.ca + + (s'ouvre dans un nouvel onglet) + + +
+
+ {# ===== TÉMOIGNAGES (placeholder pré-lancement) ===== #}
@@ -2019,15 +2237,19 @@

PRÊT ?

- Réservez votre pré-inscription. + Prêt à protéger vos données ?

-

+

+ Réservez une démonstration gratuite. Nous analyserons vos besoins et vous recommanderons le forfait adapté à votre réalité. +

+

Lancement printemps 2026. Les premiers utilisateurs bénéficient d'une remise de bienvenue et d'un accompagnement direct par notre équipe technique. Aucun engagement.

{% from 'macros/button.html' import button %} - {{ button('Pré-inscription par courriel', href='mailto:info@dictia.ca?subject=Pré-inscription%20DictIA', variant='primary', size='lg', icon='') }} + {{ button('Réserver ma démo gratuite', href='/contact', variant='primary', size='lg', icon='') }} + {{ button('Pré-inscription par courriel', href='mailto:info@dictia.ca?subject=Pré-inscription%20DictIA', variant='ghost', size='lg', icon='') }} {{ button('Voir les forfaits', href='#tarifs', variant='ghost', size='lg') }}
diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..713e2a9 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,25 @@ +"""Test bootstrap — Windows shim for fcntl (used by src/init_db.py on POSIX). + +Allows running tests on Windows even though the production app targets Linux. +Mirrors the stub used by serve_marketing.py for local preview. +""" +import os +import sys +import types + +# Stub fcntl BEFORE pytest collects any test that imports src.app +if sys.platform.startswith('win') and 'fcntl' not in sys.modules: + fcntl_stub = types.ModuleType('fcntl') + fcntl_stub.LOCK_EX = 2 + fcntl_stub.LOCK_NB = 4 + fcntl_stub.LOCK_UN = 8 + fcntl_stub.LOCK_SH = 1 + fcntl_stub.flock = lambda *_a, **_kw: None + fcntl_stub.fcntl = lambda *_a, **_kw: 0 + sys.modules['fcntl'] = fcntl_stub + +# Minimal env so src/config/app_config.py doesn't sys.exit on missing config +os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:') +os.environ.setdefault('SECRET_KEY', 'test-secret-key') +os.environ.setdefault('TRANSCRIPTION_BASE_URL', 'http://local-stub') +os.environ.setdefault('TRANSCRIPTION_API_KEY', 'local-stub') diff --git a/tests/test_marketing_landing_template.py b/tests/test_marketing_landing_template.py index 11f8af0..8aab7c2 100644 --- a/tests/test_marketing_landing_template.py +++ b/tests/test_marketing_landing_template.py @@ -83,22 +83,30 @@ def test_landing_no_login_redirect_for_anonymous(): def test_hero_has_h1_with_grad_text_accent(): - """Hero H1 contains grad-text span on the brand tagline.""" + """Hero H1 (round 3) contains the brand wordmark with grad-text accent on 'IA'. + + Round 3 replaces the old tagline ('sans risquer votre permis') with the canonical + DictIA wordmark + the H2 phrase 'Transcription IA locale en 2 minutes'. + """ client = app.test_client() body = client.get('/').data.decode('utf-8') assert 'id="hero-title"' in body, "Missing hero-title id on H1" assert 'grad-text' in body, "Missing grad-text class somewhere" - assert 'sans risquer votre permis' in body, "Missing key brand tagline" + # New canonical brand H2 phrase (cyan/grad on key claim) + assert 'Transcription IA locale en 2' in body, "Missing canonical H2 phrase" + # Hero word-staggered reveal hook on the wordmark + assert 'hero-h1-word' in body, "Missing word-staggered reveal class" def test_hero_has_dual_cta(): - """Hero has both primary (Réserver une démo) and ghost (Voir les tarifs) CTAs.""" + """Hero (round 3) has primary (Réserver une démo) and ghost (Voir les forfaits) CTAs.""" client = app.test_client() body = client.get('/').data.decode('utf-8') assert 'href="/contact"' in body assert 'href="/tarifs"' in body assert 'Réserver une démo' in body or 'Réserver une démo' in body - assert 'Voir les tarifs' in body + # Round 3 canonical wording: 'Voir les forfaits' (matches dictia.ca/solutions/dictai) + assert 'Voir les forfaits' in body, "Round 3 secondary CTA must say 'Voir les forfaits'" def test_hero_has_cosmic_orbs_background(): @@ -121,25 +129,28 @@ def test_hero_has_social_proof_microcopy(): def test_hero_has_staggered_animations(): - """Hero elements use tc-fade-in-up with staggered delays.""" + """Hero (round 3) elements use tc-fade-in-up with staggered delays — canonical cadence. + + Round 3 staggers : 0 (back-link), 75 (eyebrow), 200 (3-step flow), + 280 (H2 phrase), 360 (sub), 440 (stats), 520 (CTAs), 600 (social proof). + """ client = app.test_client() body = client.get('/').data.decode('utf-8') assert 'animate-tc-fade-in-up' in body, "Missing fade-in animation" - assert 'animation-delay: 0ms' in body - assert 'animation-delay: 75ms' in body - assert 'animation-delay: 150ms' in body - assert 'animation-delay: 300ms' in body - assert 'animation-delay: 400ms' in body + for delay in ['0ms', '75ms', '200ms', '280ms', '360ms', '440ms', '520ms', '600ms']: + assert f'animation-delay: {delay}' in body, f"Missing staggered delay {delay}" assert 'animation-fill-mode: backwards' in body, \ "Missing animation-fill-mode (causes flash before delay fires)" def test_hero_eyebrow_has_brand_messaging(): - """Hero eyebrow declares the 3 brand pillars.""" + """Hero eyebrow declares the 3 brand pillars (round 3 uses OQLF NBSP : LOI 25).""" client = app.test_client() body = client.get('/').data.decode('utf-8') assert 'TRANSCRIPTION IA' in body - assert 'CONFORME LOI 25' in body + # OQLF-conformant : non-breaking space before "25" (NBSP entity) + assert 'CONFORME LOI 25' in body or 'CONFORME LOI 25' in body, \ + "Missing 'CONFORME LOI 25' eyebrow (with or without NBSP)" assert 'QU' in body # Either QUÉBEC or QUÉBEC @@ -665,31 +676,32 @@ def test_testimonials_use_personas_not_fake_names(): assert name not in body, f"Forbidden fabricated testimonial name: {name}" -def test_faq_section_with_7_questions(): - """FAQ section present with 7 questions.""" +def test_faq_section_with_10_questions(): + """FAQ section (round 3) present with 10 canonical questions from dictai-page-content.tsx.""" client = app.test_client() body = client.get('/').data.decode('utf-8') assert 'faq-title' in body - # 7 panel IDs (loop.index is 1-indexed) - for i in range(1, 8): + # 10 panel IDs (loop.index is 1-indexed) + for i in range(1, 11): assert f'id="faq-panel-{i}"' in body, f"Missing FAQ panel {i}" - # Question topic anchors - topics = ['Loi', 'Cloud et DictIA on-premise', 'fran', 'audiences', 'formats', - 'résilie', 'AGPL'] + # Round 3 canonical topic anchors (sourced from dictai-page-content.tsx) + topics = ['Comment fonctionne la transcription', 'formats audio', '1 heure d', + 'confidentielle', 'Teams Copilot', 'Otter.ai', 'Barreau du Qu', + 'Clio Manage', 'connaissances techniques', 'open source'] for topic in topics: assert topic in body, f"FAQ missing topic anchor: {topic}" def test_faq_alpine_accordion_bindings(): - """FAQ uses Alpine.js x-data + @click + :aria-expanded for accessible accordion.""" + """FAQ uses Alpine.js x-data + @click + :aria-expanded for accessible accordion (10 items).""" client = app.test_client() body = client.get('/').data.decode('utf-8') - # 7 x-data="{ open: false }" instances - assert body.count('x-data="{ open: false }"') == 7, \ - "FAQ must have 7 independent Alpine accordion items" + # 10 x-data="{ open: false }" instances (round 3 enrichment) + assert body.count('x-data="{ open: false }"') == 10, \ + "FAQ must have 10 independent Alpine accordion items" # Each toggle button has @click and :aria-expanded - assert body.count('@click="open = !open"') == 7 - assert body.count(':aria-expanded="open.toString()"') == 7 + assert body.count('@click="open = !open"') == 10 + assert body.count(':aria-expanded="open.toString()"') == 10 # Use built-in x-transition (NOT x-collapse plugin which is not bundled) assert 'x-collapse' not in body, "Must NOT use x-collapse plugin (not loaded — use x-transition)" assert 'x-transition.opacity' in body, "FAQ panels must use built-in x-transition" @@ -720,7 +732,8 @@ def test_faq_jsonld_schema_present(): assert parsed['@context'] == 'https://schema.org' assert parsed['@type'] == 'FAQPage' assert isinstance(parsed['mainEntity'], list) - assert len(parsed['mainEntity']) == 7, "FAQPage must contain exactly 7 questions" + # Round 3: enriched to 10 canonical questions from dictai-page-content.tsx + assert len(parsed['mainEntity']) == 10, "FAQPage must contain exactly 10 questions (round 3)" for q in parsed['mainEntity']: assert q['@type'] == 'Question' assert q['acceptedAnswer']['@type'] == 'Answer' @@ -729,18 +742,25 @@ def test_faq_jsonld_schema_present(): def test_cta_final_section(): - """CTA final section with mailto pré-inscription + ghost button to #tarifs.""" + """CTA final (round 3) — primary démo gratuite + mailto pré-inscription + ghost button to #tarifs.""" client = app.test_client() body = client.get('/').data.decode('utf-8') assert 'cta-title' in body - assert 'pré-inscription' in body or 'pré-inscription' in body + # Round 3 wording reinforced: "Prêt à protéger vos données" + démo gratuite + assert 'prot' in body and 'donn' in body, "Missing 'protéger vos données' headline" + assert 'démo gratuite' in body or 'démo gratuite' in body, \ + "Round 3 primary CTA must say 'démo gratuite'" + # Pré-inscription wording (any case) preserved as secondary path + assert 'pré-inscription' in body or 'pré-inscription' in body \ + or 'Pré-inscription' in body or 'Pré-inscription' in body, \ + "Pré-inscription wording must be preserved" # mailto with subject assert 'href="mailto:info@dictia.ca?subject=Pr%C3%A9-inscription%20DictIA"' in body or \ 'href="mailto:info@dictia.ca?subject=Pré-inscription%20DictIA"' in body, \ "CTA must have mailto with subject prefilled" # Anchor link to existing #tarifs section assert 'href="#tarifs"' in body, "Secondary CTA must anchor to pricing" - # Ghost variant button + # Ghost variant button still in use (mailto + #tarifs) assert 'border-white/[0.08]' in body # ghost button class @@ -839,12 +859,16 @@ def test_round2_no_external_js_libs_added(): def test_round2_preserves_existing_sections(): - """Round 2 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité.""" + """Round 2 + 3 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité. + + NOTE: round 3 replaced the hero copy ('sans risquer votre permis' → canonical wordmark + + 'Transcription IA locale en 2 minutes'). The hero ID + pipeline are still required. + """ client = app.test_client() body = client.get('/').data.decode('utf-8') - # Hero (round 0) + # Hero (round 3 canonical hero replaces round 0) assert 'hero-title' in body - assert 'sans risquer votre permis' in body + assert 'Transcription IA locale en 2' in body, "Round 3 hero canonical phrase missing" # Pipeline (round 1) — auto-advance + 4 nodes assert 'pipeline-title' in body assert 'Du fichier au résumé' in body @@ -880,7 +904,8 @@ def test_routes_passes_testimonials_and_faq_to_template(): assert hasattr(routes, 'TESTIMONIALS'), "Module must define TESTIMONIALS list" assert hasattr(routes, 'FAQ'), "Module must define FAQ list" assert len(routes.TESTIMONIALS) == 3, "Must have 3 placeholder testimonials" - assert len(routes.FAQ) == 7, "Must have 7 FAQ entries" + # Round 3: enriched FAQ from 7 to 10 canonical questions (sourced from dictai-page-content.tsx) + assert len(routes.FAQ) == 10, "Must have 10 FAQ entries (round 3)" # Each testimonial must NOT contain a 'quote' field (no fabricated quotes pre-launch) for t in routes.TESTIMONIALS: assert 'quote' not in t, "Pre-launch testimonials must not contain quote fields"