From 69baa1be2feb0d216891f6874720a32b6d97ba67 Mon Sep 17 00:00:00 2001 From: Allison Date: Tue, 28 Apr 2026 12:21:16 -0400 Subject: [PATCH] =?UTF-8?q?feat(marketing):=20round=202=20=E2=80=94=20int?= =?UTF-8?q?=C3=A8gre=203=20sections=20de=20dictia.ca/solutions/dictai=20(c?= =?UTF-8?q?ycle/wave/cadre)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cycle "Trois options. Une seule est conforme." (entre PAS Problème & Solution) 3 colonnes comparatives (humaine 315$/h / cloud US illégal / DictIA 173$/mo) Phases reveal 1→4 via IntersectionObserver + setTimeout chain Anneaux pulsants source node + horloge rotation + particules fuites cloud Overlay légal NON CONFORME sur col 2 - Wave "Onde de transformation" (entre Solution & Pipeline) Slider mouse-X interactif : 30 barres SVG morphent rouge → cyan Particules tombantes -$/-h (CSS keyframes staggered) Étiquettes douleur PAINS / SOLUTIONS flottantes Mobile : toggle button, pas de mouse interaction - Cadre réglementaire "Moniteur d'Interception" (entre Conformité & Témoignages) Mappe 6 textes officiels : Loi 25, Loi 96, Cloud Act US, Guide IA Barreau, Cadre IA MCN, CAI Liens vers sources autoritaires (legisquebec, congress.gov, barreau, tresor, cai) HUD console typing reveal + caret blink + folder QC→US transition aria-live="polite" sur verdict, role="list" sur REGS Texte 100% canonique extrait de Website-Sanity dictai-cycle/wave/contraste.tsx. Toutes animations CSS pure + Alpine.js + IntersectionObserver natif (zéro lib JS externe). prefers-reduced-motion désactive tout. +802 lignes landing.html, +119 lignes tests (6 nouveaux test_round2_*), npm run build:css exécuté. Co-Authored-By: Claude Opus 4.7 (1M context) --- static/css/marketing.css | 353 ++++++++++ templates/marketing/landing.html | 802 +++++++++++++++++++++++ tests/test_marketing_landing_template.py | 119 ++++ 3 files changed, 1274 insertions(+) diff --git a/static/css/marketing.css b/static/css/marketing.css index 81f6776..ab7550a 100644 --- a/static/css/marketing.css +++ b/static/css/marketing.css @@ -135,6 +135,7 @@ --leading-tight: 1.25; --leading-snug: 1.375; --leading-relaxed: 1.625; + --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; @@ -144,6 +145,7 @@ --animate-spin: spin 1s linear infinite; --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; --blur-sm: 8px; + --blur-md: 12px; --blur-xl: 24px; --blur-3xl: 64px; --default-transition-duration: 150ms; @@ -365,6 +367,9 @@ .inset-0 { inset: calc(var(--spacing) * 0); } + .inset-\[-8px\] { + inset: -8px; + } .inset-x-0 { inset-inline: calc(var(--spacing) * 0); } @@ -461,6 +466,9 @@ .right-4 { right: calc(var(--spacing) * 4); } + .right-\[3\%\] { + right: 3%; + } .-bottom-10 { bottom: calc(var(--spacing) * -10); } @@ -470,6 +478,9 @@ .bottom-1\/4 { bottom: calc(1 / 4 * 100%); } + .bottom-3 { + bottom: calc(var(--spacing) * 3); + } .bottom-4 { bottom: calc(var(--spacing) * 4); } @@ -512,12 +523,18 @@ .left-80 { left: calc(var(--spacing) * 80); } + .left-\[3\%\] { + left: 3%; + } .z-10 { z-index: 10; } .z-20 { z-index: 20; } + .z-30 { + z-index: 30; + } .z-40 { z-index: 40; } @@ -674,9 +691,15 @@ .mb-2 { margin-bottom: calc(var(--spacing) * 2); } + .mb-2\.5 { + margin-bottom: calc(var(--spacing) * 2.5); + } .mb-3 { margin-bottom: calc(var(--spacing) * 3); } + .mb-3\.5 { + margin-bottom: calc(var(--spacing) * 3.5); + } .mb-4 { margin-bottom: calc(var(--spacing) * 4); } @@ -821,9 +844,21 @@ .h-72 { height: calc(var(--spacing) * 72); } + .h-\[1\.5px\] { + height: 1.5px; + } .h-\[2px\] { height: 2px; } + .h-\[3px\] { + height: 3px; + } + .h-\[17px\] { + height: 17px; + } + .h-\[52px\] { + height: 52px; + } .h-\[62px\] { height: 62px; } @@ -833,6 +868,9 @@ .h-\[88px\] { height: 88px; } + .h-\[90px\] { + height: 90px; + } .h-\[95vh\] { height: 95vh; } @@ -917,6 +955,12 @@ .min-h-screen { min-height: 100vh; } + .w-1 { + width: calc(var(--spacing) * 1); + } + .w-1\.5 { + width: calc(var(--spacing) * 1.5); + } .w-1\/2 { width: calc(1 / 2 * 100%); } @@ -992,12 +1036,21 @@ .w-80 { width: calc(var(--spacing) * 80); } + .w-\[3px\] { + width: 3px; + } + .w-\[52px\] { + width: 52px; + } .w-\[68px\] { width: 68px; } .w-\[88px\] { width: 88px; } + .w-\[100px\] { + width: 100px; + } .w-\[400px\] { width: 400px; } @@ -1016,6 +1069,9 @@ .w-full { width: 100%; } + .w-px { + width: 1px; + } .max-w-2xl { max-width: var(--container-2xl); } @@ -1251,6 +1307,9 @@ .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; } @@ -1311,6 +1370,9 @@ .gap-4 { gap: calc(var(--spacing) * 4); } + .gap-5 { + gap: calc(var(--spacing) * 5); + } .gap-6 { gap: calc(var(--spacing) * 6); } @@ -1388,6 +1450,10 @@ -moz-column-gap: calc(var(--spacing) * 6); column-gap: calc(var(--spacing) * 6); } + .gap-x-8 { + -moz-column-gap: calc(var(--spacing) * 8); + column-gap: calc(var(--spacing) * 8); + } .space-x-1 { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; @@ -1429,6 +1495,9 @@ .gap-y-2 { row-gap: calc(var(--spacing) * 2); } + .gap-y-3 { + row-gap: calc(var(--spacing) * 3); + } .divide-y { :where(& > :not(:last-child)) { --tw-divide-y-reverse: 0; @@ -1480,6 +1549,9 @@ .rounded-none { border-radius: 0; } + .rounded-sm { + border-radius: var(--radius-sm); + } .rounded-xl { border-radius: var(--radius-xl); } @@ -1622,6 +1694,21 @@ .border-brand-b1 { border-color: #0062ff; } + .border-brand-b1\/15 { + border-color: color-mix(in oklab, #0062ff 15%, transparent); + } + .border-brand-b1\/20 { + border-color: color-mix(in oklab, #0062ff 20%, transparent); + } + .border-brand-b1\/25 { + border-color: color-mix(in oklab, #0062ff 25%, transparent); + } + .border-brand-b1\/30 { + border-color: color-mix(in oklab, #0062ff 30%, transparent); + } + .border-brand-b1\/35 { + border-color: color-mix(in oklab, #0062ff 35%, transparent); + } .border-brand-b2\/40 { border-color: color-mix(in oklab, #00bdd8 40%, transparent); } @@ -1664,6 +1751,9 @@ border-color: color-mix(in oklab, var(--color-purple-500) 30%, transparent); } } + .border-red-100 { + border-color: var(--color-red-100); + } .border-red-200 { border-color: var(--color-red-200); } @@ -1703,6 +1793,12 @@ border-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } + .border-white\/15 { + border-color: color-mix(in srgb, #fff 15%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-white) 15%, transparent); + } + } .border-white\/20 { border-color: color-mix(in srgb, #fff 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1844,12 +1940,21 @@ .bg-amber-100 { background-color: var(--color-amber-100); } + .bg-amber-500 { + background-color: var(--color-amber-500); + } .bg-amber-500\/10 { background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent); } } + .bg-amber-500\/15 { + background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent); + } + } .bg-amber-500\/20 { background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1898,12 +2003,18 @@ .bg-brand-b1 { background-color: #0062ff; } + .bg-brand-b1\/5 { + background-color: color-mix(in oklab, #0062ff 5%, transparent); + } .bg-brand-b1\/10 { background-color: color-mix(in oklab, #0062ff 10%, transparent); } .bg-brand-b1\/15 { background-color: color-mix(in oklab, #0062ff 15%, transparent); } + .bg-brand-b1\/\[0\.06\] { + background-color: color-mix(in oklab, #0062ff 6%, transparent); + } .bg-brand-b3\/10 { background-color: color-mix(in oklab, #00c896 10%, transparent); } @@ -1925,6 +2036,18 @@ .bg-brand-navy { background-color: #060d1a; } + .bg-brand-navy\/10 { + background-color: color-mix(in oklab, #060d1a 10%, transparent); + } + .bg-brand-navy\/15 { + background-color: color-mix(in oklab, #060d1a 15%, transparent); + } + .bg-brand-navy\/30 { + background-color: color-mix(in oklab, #060d1a 30%, transparent); + } + .bg-brand-navy\/80 { + background-color: color-mix(in oklab, #060d1a 80%, transparent); + } .bg-brand-navy\/\[0\.97\] { background-color: color-mix(in oklab, #060d1a 97%, transparent); } @@ -2033,9 +2156,24 @@ .bg-red-50 { background-color: var(--color-red-50); } + .bg-red-50\/30 { + background-color: color-mix(in srgb, oklch(97.1% 0.013 17.38) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-red-50) 30%, transparent); + } + } .bg-red-100 { background-color: var(--color-red-100); } + .bg-red-100\/60 { + background-color: color-mix(in srgb, oklch(93.6% 0.032 17.717) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-red-100) 60%, transparent); + } + } + .bg-red-400 { + background-color: var(--color-red-400); + } .bg-red-500\/10 { background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2169,6 +2307,14 @@ --tw-gradient-from: var(--color-blue-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)); } + .from-brand-b1\/15 { + --tw-gradient-from: color-mix(in oklab, #0062ff 15%, 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-b1\/\[0\.06\] { + --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-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)); @@ -2208,6 +2354,14 @@ --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-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)); + } + .to-brand-b3\/\[0\.04\] { + --tw-gradient-to: color-mix(in oklab, #00c896 4%, 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)); + } .to-purple-500 { --tw-gradient-to: var(--color-purple-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)); @@ -2292,6 +2446,9 @@ .px-3 { padding-inline: calc(var(--spacing) * 3); } + .px-3\.5 { + padding-inline: calc(var(--spacing) * 3.5); + } .px-4 { padding-inline: calc(var(--spacing) * 4); } @@ -2331,9 +2488,15 @@ .py-4 { padding-block: calc(var(--spacing) * 4); } + .py-5 { + padding-block: calc(var(--spacing) * 5); + } .py-6 { padding-block: calc(var(--spacing) * 6); } + .py-7 { + padding-block: calc(var(--spacing) * 7); + } .py-8 { padding-block: calc(var(--spacing) * 8); } @@ -2515,6 +2678,9 @@ font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); } + .text-\[8px\] { + font-size: 8px; + } .text-\[9px\] { font-size: 9px; } @@ -2524,6 +2690,9 @@ .text-\[11px\] { font-size: 11px; } + .text-\[12px\] { + font-size: 12px; + } .text-\[13px\] { font-size: 13px; } @@ -2606,6 +2775,30 @@ --tw-tracking: 0.12em; letter-spacing: 0.12em; } + .tracking-\[0\.14em\] { + --tw-tracking: 0.14em; + letter-spacing: 0.14em; + } + .tracking-\[0\.16em\] { + --tw-tracking: 0.16em; + letter-spacing: 0.16em; + } + .tracking-\[0\.18em\] { + --tw-tracking: 0.18em; + letter-spacing: 0.18em; + } + .tracking-\[0\.20em\] { + --tw-tracking: 0.20em; + letter-spacing: 0.20em; + } + .tracking-\[0\.22em\] { + --tw-tracking: 0.22em; + letter-spacing: 0.22em; + } + .tracking-\[0\.28em\] { + --tw-tracking: 0.28em; + letter-spacing: 0.28em; + } .tracking-tight { --tw-tracking: var(--tracking-tight); letter-spacing: var(--tracking-tight); @@ -2730,6 +2923,24 @@ .text-brand-b1 { color: #0062ff; } + .text-brand-b1\/45 { + color: color-mix(in oklab, #0062ff 45%, transparent); + } + .text-brand-b1\/55 { + color: color-mix(in oklab, #0062ff 55%, transparent); + } + .text-brand-b1\/60 { + color: color-mix(in oklab, #0062ff 60%, transparent); + } + .text-brand-b1\/65 { + color: color-mix(in oklab, #0062ff 65%, transparent); + } + .text-brand-b1\/70 { + color: color-mix(in oklab, #0062ff 70%, transparent); + } + .text-brand-b1\/80 { + color: color-mix(in oklab, #0062ff 80%, transparent); + } .text-brand-b2 { color: #00bdd8; } @@ -2742,18 +2953,33 @@ .text-brand-navy\/30 { color: color-mix(in oklab, #060d1a 30%, transparent); } + .text-brand-navy\/35 { + color: color-mix(in oklab, #060d1a 35%, transparent); + } .text-brand-navy\/40 { color: color-mix(in oklab, #060d1a 40%, transparent); } + .text-brand-navy\/45 { + color: color-mix(in oklab, #060d1a 45%, transparent); + } .text-brand-navy\/50 { color: color-mix(in oklab, #060d1a 50%, transparent); } + .text-brand-navy\/55 { + color: color-mix(in oklab, #060d1a 55%, transparent); + } .text-brand-navy\/60 { color: color-mix(in oklab, #060d1a 60%, transparent); } + .text-brand-navy\/65 { + color: color-mix(in oklab, #060d1a 65%, transparent); + } .text-brand-navy\/70 { color: color-mix(in oklab, #060d1a 70%, transparent); } + .text-brand-navy\/75 { + color: color-mix(in oklab, #060d1a 75%, transparent); + } .text-brand-navy\/80 { color: color-mix(in oklab, #060d1a 80%, transparent); } @@ -2769,6 +2995,9 @@ .text-emerald-500 { color: var(--color-emerald-500); } + .text-emerald-600 { + color: var(--color-emerald-600); + } .text-gray-200 { color: var(--color-gray-200); } @@ -2832,6 +3061,36 @@ .text-red-500 { color: var(--color-red-500); } + .text-red-500\/60 { + color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-red-500) 60%, transparent); + } + } + .text-red-500\/65 { + color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 65%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-red-500) 65%, transparent); + } + } + .text-red-500\/70 { + color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-red-500) 70%, transparent); + } + } + .text-red-500\/75 { + color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 75%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-red-500) 75%, transparent); + } + } + .text-red-500\/80 { + color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-red-500) 80%, transparent); + } + } .text-red-600 { color: var(--color-red-600); } @@ -2880,6 +3139,12 @@ color: color-mix(in oklab, var(--color-white) 50%, transparent); } } + .text-white\/55 { + color: color-mix(in srgb, #fff 55%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-white) 55%, transparent); + } + } .text-white\/60 { color: color-mix(in srgb, #fff 60%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2938,6 +3203,9 @@ --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,); } + .no-underline { + text-decoration-line: none; + } .underline { text-decoration-line: underline; } @@ -3007,6 +3275,9 @@ .opacity-100 { opacity: 100%; } + .opacity-\[0\.02\] { + opacity: 0.02; + } .shadow { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -3015,6 +3286,10 @@ --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .shadow-\[0_0_6px_\#F59E0B\] { + --tw-shadow: 0 0 6px var(--tw-shadow-color, #F59E0B); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-\[0_0_28px_rgba\(0\,98\,255\,0\.35\)\] { --tw-shadow: 0 0 28px var(--tw-shadow-color, rgba(0,98,255,0.35)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -3095,6 +3370,10 @@ --tw-backdrop-blur: blur(8px); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } + .backdrop-blur-md { + --tw-backdrop-blur: blur(var(--blur-md)); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } .backdrop-blur-sm { --tw-backdrop-blur: blur(var(--blur-sm)); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); @@ -3215,6 +3494,13 @@ } } } + .group-hover\:opacity-60 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + opacity: 60%; + } + } + } .group-hover\:opacity-100 { &:is(:where(.group):hover *) { @media (hover: hover) { @@ -4215,6 +4501,11 @@ outline-offset: 4px; } } + .focus-visible\:outline-amber-500 { + &:focus-visible { + outline-color: var(--color-amber-500); + } + } .focus-visible\:outline-brand-b1 { &:focus-visible { outline-color: #0062ff; @@ -4619,11 +4910,31 @@ margin-bottom: calc(var(--spacing) * 4); } } + .md\:block { + @media (width >= 48rem) { + display: block; + } + } .md\:flex { @media (width >= 48rem) { display: flex; } } + .md\:hidden { + @media (width >= 48rem) { + display: none; + } + } + .md\:h-5 { + @media (width >= 48rem) { + height: calc(var(--spacing) * 5); + } + } + .md\:h-12 { + @media (width >= 48rem) { + height: calc(var(--spacing) * 12); + } + } .md\:h-48 { @media (width >= 48rem) { height: calc(var(--spacing) * 48); @@ -4634,11 +4945,31 @@ min-height: 12rem; } } + .md\:w-5 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 5); + } + } + .md\:w-12 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 12); + } + } .md\:w-48 { @media (width >= 48rem) { width: calc(var(--spacing) * 48); } } + .md\:w-\[42\%\] { + @media (width >= 48rem) { + width: 42%; + } + } + .md\:w-\[58\%\] { + @media (width >= 48rem) { + width: 58%; + } + } .md\:grid-cols-2 { @media (width >= 48rem) { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -4654,6 +4985,11 @@ grid-template-columns: repeat(4, minmax(0, 1fr)); } } + .md\:grid-cols-\[2fr_2fr_3fr\] { + @media (width >= 48rem) { + grid-template-columns: 2fr 2fr 3fr; + } + } .md\:flex-row { @media (width >= 48rem) { flex-direction: row; @@ -4674,6 +5010,18 @@ gap: calc(var(--spacing) * 5); } } + .md\:border-t-0 { + @media (width >= 48rem) { + border-top-style: var(--tw-border-style); + border-top-width: 0px; + } + } + .md\:border-l { + @media (width >= 48rem) { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + } .md\:p-2 { @media (width >= 48rem) { padding: calc(var(--spacing) * 2); @@ -4770,6 +5118,11 @@ line-height: var(--tw-leading, var(--text-sm--line-height)); } } + .md\:text-\[10px\] { + @media (width >= 48rem) { + font-size: 10px; + } + } .lg\:relative { @media (width >= 64rem) { position: relative; diff --git a/templates/marketing/landing.html b/templates/marketing/landing.html index 25831dc..b578278 100644 --- a/templates/marketing/landing.html +++ b/templates/marketing/landing.html @@ -422,6 +422,341 @@ +{# ===== CYCLE — "Trois options. Une seule est conforme." ===== #} +{# Source canonique : InnovA-AI/Website-Sanity/components/sections/dictai-cycle.tsx + Animation : phase reveal phasé (1→4) déclenché par IntersectionObserver natif + Alpine. + 3 colonnes comparatives : 01 Humaine · 02 Cloud US (overlay non-conforme) · 03 DictIA (featured + pulse rings) #} + +
+ {# Subtle dot-grid bg #} + + +
+
+

⚠ CADRE RÉGLEMENTAIRE

+

+ Trois options. Une seule est conforme. +

+

+ La Loi 25, le Cloud Act américain et le Guide IA du Barreau du Québec délimitent vos options — + pas votre volonté. +

+
+ +
+ + {# Nœud source : "Réunion en cours" #} +
+
+ + + + + Réunion en cours — données confidentielles +
+
+ + {# Lignes de connexion SVG — de la source vers les 3 colonnes #} +
+ +
+ + {# Grille 3 colonnes — équivalence du grid-cols-[2fr_2fr_3fr] desktop #} +
+ + {# COL 1 — Retranscription humaine #} +
+
+
01
+
Retranscription humaine
+
+
+ {# Stack de papiers #} + +
+
+ + + + 5 jours +
+
+
+ + ~85 $/h +
+
+
+
+
+ 315 + $ / réunion +
+ + + Lent · Coûteux + +
+
+ + {# COL 2 — IA cloud américaine #} +
+ {# Overlay légal NON CONFORME (phase 3) #} +
+
+ +
+
NON CONFORME
+
Loi 25 · Cloud Act américain
+
+
+
+ +
+
02
+
IA cloud américaine
+
+ +
+
+ +
+ USA +
+
+ +
+ {% for risk in [ + ('cloud', 'Données transférées aux États-Unis'), + ('alert', "Entraînement de l'IA publique"), + ('users', '500–750 $/mois · facturation par utilisateur') + ] %} +
+ {% if risk[0] == 'cloud' %} + + {% elif risk[0] == 'alert' %} + + {% else %} + + {% endif %} + {{ risk[1] | safe }} +
+ {% endfor %} +
+
+ +
+
+
+ Violation légale possible +
+
+
+ + {# COL 3 — DictIA (featured, 3fr) #} +
+ {# Halo ambiant #} + + +
+
+
+
+
+ Solution +
+ +
+ {# Lock + anneaux concentriques #} + + + {# Flux sécurisé #} +
+ + Transcription sécurisée + + +
+ + {# Badges conformité #} +
+ {% for badge_label in ['Loi 25 conforme', '100 % hébergé au Québec', 'Données jamais partagées'] %} +
+ + + + {{ badge_label | safe }} +
+ {% endfor %} +
+
+ +
+
+ + UTILISATEURS ILLIMITÉS + +
+

Zéro frais caché · Du jamais vu

+
+ 173 + $ / mois +
+
+ +

Conforme Loi 25 · 100 % Québec

+
+
+
+
+ + {# Barre d'économies — apparaît avec phase 4 #} +
+
+ + Économies annuelles · 25 utilisateurs +
+ {% for sav in [('3 924 $', 'vs Otter.ai'), ('6 924 $', 'vs MS Teams'), ('2 004 $', 'vs Sténographe')] %} +
+ {{ sav[0] | safe }} + {{ sav[1] }} +
+ {% endfor %} +
+
+
+
+ {# ===== SOLUTION (S de PAS) ===== #}
{# Single subtle orb in solution bg — less busy than hero #} @@ -455,6 +790,245 @@
+{# ===== WAVE — Onde de transformation chaos→ordre ===== #} +{# Source canonique : InnovA-AI/Website-Sanity/components/sections/dictai-wave.tsx + Animation : slider mouse-X interactif morphe les barres rouge→cyan, particules pain→solution. + Mobile : bouton toggle (activé/désactivé) — pas de mousemove. #} + +
+
+
+ Le problème + +
+

+ La transcription manuelle coûte cher. +

+ +

+ Touchez pour basculer entre : aujourd'hui (chaos) et avec DictIA (ordre). +

+ + {# Scène interactive — aspect-ratio 2/1 desktop, 4/3 mobile #} + +
+
+ {# ===== PIPELINE — Comment ça marche : 4 étapes ===== #} {# Source canonique : InnovA-AI/Website-Sanity/components/sections/dictai-pipeline.tsx Animation traduite : Framer Motion auto-advance + sweep ring → Alpine.js setInterval + CSS keyframes #} @@ -1108,6 +1682,234 @@ +{# ===== CADRE RÉGLEMENTAIRE — Moniteur d'Interception ===== #} +{# Source canonique : InnovA-AI/Website-Sanity/components/sections/dictai-contraste.tsx (REGS + MoniteurInterception) + Animation : cycle automatique en 4 étapes (folder QC→US, alerte, HUD log, flash REGS séquentiel) + Alpine + Cartographie 6 textes : Loi 25, Loi 96, Cloud Act, Guide IA Barreau, Cadre IA MCN, CAI #} + +
+ + +
+
+

⚠ CADRE RÉGLEMENTAIRE QUÉBEC

+

+ Ce que vos outils actuels enfreignent en secret. +

+

+ Six textes officiels encadrent l'usage de l'IA et la circulation des données vocales au Québec. + Visualisez en temps réel comment une transcription cloud quitte la province et déclenche les violations. +

+
+ + {# Carte principale — header + body 2 colonnes (animation à gauche, REGS à droite) #} +
+ + {# Header — Moniteur d'Interception #} +
+ + + Moniteur d'Interception + + + ⚠ Alerte Active + +
+ + {# Body — 2 colonnes #} +
+ + {# LEFT — animation track (~42%) #} +
+ {# Grille bg #} + + + {# Labels QC / US #} +
+ + QC + + + US + +
+ + {# Track #} +
+ +
+ + +
+
+ + {# HUD panel #} +
+ + +
+
+ + {# RIGHT — REGS list (~58%) #} +
+

+ Ce que vos outils actuels enfreignent en secret : +

+
    + {% for reg in [ + {'label': 'Loi 25 (P-39.1)', 'detail': 'Renseignements personnels sensibles — pénalité jusqu\'à 25 M$', 'href': 'https://www.legisquebec.gouv.qc.ca/fr/document/lc/P-39.1', 'risk': True}, + {'label': 'Loi 96 (C-11)', 'detail': 'Francisation — PME 25+ employés depuis juin 2025', 'href': 'https://www.legisquebec.gouv.qc.ca/fr/document/lc/C-11', 'risk': True}, + {'label': 'US Cloud Act', 'detail': 'Accès aux données hébergées hors QC par les autorités américaines', 'href': 'https://www.congress.gov/bill/115th-congress/senate-bill/2383', 'risk': True}, + {'label': 'Guide IA — Barreau QC', 'detail': 'Recommande systèmes fermés déployés en interne (publié 2024)', 'href': 'https://www.barreau.qc.ca/', 'risk': False}, + {'label': 'Cadre IA — MCN', 'detail': 'Gouvernance IA pour organismes publics (déc. 2025, conformité 19 juin 2026)', 'href': 'https://www.tresor.gouv.qc.ca/', 'risk': False}, + {'label': 'CAI', 'detail': 'Commission d\'accès à l\'information — application active', 'href': 'https://www.cai.gouv.qc.ca/', 'risk': False} + ] %} +
  • + + +
    +
    + {{ reg.label }} + {{ reg.detail | safe }} +
    +
    + Lire ↗ +
    +
  • + {% endfor %} +
+

+ Les textes officiels font foi. Les informations ci-dessus sont fournies à titre indicatif. +

+
+
+ + {# Footer — verdict #} +
+ + + + +
+
+ +

+ DictIA est conçu pour les secteurs réglementés du Québec — Loi 25, Cloud Act, Barreau, ChAD, AMF. Hébergement OVH Beauharnois, + code source AGPL v3 vérifiable, audit trail intégré. +

+
+
+ {# ===== TÉMOIGNAGES (placeholder pré-lancement) ===== #}
diff --git a/tests/test_marketing_landing_template.py b/tests/test_marketing_landing_template.py index 8933eac..11f8af0 100644 --- a/tests/test_marketing_landing_template.py +++ b/tests/test_marketing_landing_template.py @@ -754,6 +754,125 @@ def test_cta_final_uses_safe_pre_launch_wording(): assert phrase not in body, f"Forbidden pre-launch CTA: {phrase}" +def test_round2_cycle_section_present(): + """Round 2 — Cycle section ('Trois options. Une seule est conforme.') must be on landing. + + Sourced from dictai-cycle.tsx; covers the 3-column comparative narrative + (humaine / cloud US / DictIA) with canonical pricing 315 $ vs 173 $ and savings. + """ + client = app.test_client() + body = client.get('/').data.decode('utf-8') + assert 'cycle-title' in body, "Cycle section H2 id must be present" + assert 'Trois options' in body + assert 'Une seule est conforme' in body + assert 'Retranscription humaine' in body + assert 'IA cloud américaine' in body + assert 'NON CONFORME' in body + assert '315' in body and '173' in body, "Canonical Cycle pricing must appear" + assert 'Loi 25 conforme' in body + assert '100 % hébergé au Québec' in body or '100 % hébergé au Québec' in body + # Phase animation hooks + assert 'cycle-pulse' in body, "Pulse rings keyframe class missing" + assert 'cycle-card-dictia' in body + # Reduced-motion safety + assert 'prefers-reduced-motion' in body + + +def test_round2_wave_section_present(): + """Round 2 — Wave section (chaos→ordre interactive slider) must be on landing. + + Sourced from dictai-wave.tsx; mouse-X morphs 30 bars red→cyan + pain/solution cards. + """ + client = app.test_client() + body = client.get('/').data.decode('utf-8') + assert 'wave-title' in body, "Wave section H2 id must be present" + assert 'La transcription manuelle' in body + assert 'coûte cher' in body + # Canonical pain labels + assert '4 à 6h pour transcrire 1h' in body + assert 'Délais de 48h à 5 jours' in body + # Canonical solution labels (NBSP-aware) + assert '~2 min pour 1h d' in body + assert '173 $/mois' in body or '173 $/mois' in body + # Alpine state for interactive slider + assert 'onMove($event)' in body + assert 'isMobile' in body + # Mobile fallback toggle + assert 'Activer DictIA' in body + assert 'Voir sans DictIA' in body + + +def test_round2_cadre_reglementaire_section_present(): + """Round 2 — Cadre réglementaire (Moniteur d'Interception) with 6 REGS list. + + Sourced from dictai-contraste.tsx (REGS + MoniteurInterception subcomponent). + """ + client = app.test_client() + body = client.get('/').data.decode('utf-8') + assert 'cadre-title' in body, "Cadre réglementaire H2 id must be present" + assert "Moniteur d'Interception" in body + assert 'enfreignent' in body + # 6 REGS — each must appear with its hyperlink + for reg_label in ['Loi 25 (P-39.1)', 'Loi 96 (C-11)', 'US Cloud Act', + 'Guide IA — Barreau QC', 'Cadre IA — MCN', 'CAI']: + assert reg_label in body, f"Missing REG label: {reg_label}" + # Authoritative sources + assert 'legisquebec.gouv.qc.ca' in body + assert 'cai.gouv.qc.ca' in body + assert 'tresor.gouv.qc.ca' in body + # HUD lines + assert 'Interception IA détectée' in body + assert 'NON CONFORME' in body + # Cycle animation hooks + assert 'cadre-folder' in body + assert 'runCycle' in body + + +def test_round2_no_external_js_libs_added(): + """Round 2 must NOT add Framer Motion / GSAP / canvas-confetti / etc.""" + client = app.test_client() + body = client.get('/').data.decode('utf-8') + forbidden_libs = ['framer-motion', 'gsap', 'canvas-confetti', 'three.min.js', + 'lottie-web', 'anime.min.js'] + for lib in forbidden_libs: + assert lib not in body, f"Round 2 must not introduce JS lib: {lib}" + + +def test_round2_preserves_existing_sections(): + """Round 2 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité.""" + client = app.test_client() + body = client.get('/').data.decode('utf-8') + # Hero (round 0) + assert 'hero-title' in body + assert 'sans risquer votre permis' in body + # Pipeline (round 1) — auto-advance + 4 nodes + assert 'pipeline-title' in body + assert 'Du fichier au résumé' in body + # Hub (round 1) + assert 'hub-title' in body + assert 'se connecte à tout' in body + # Bento + ROI calculator + assert 'bento-title' in body + assert 'roiCalculator()' in body + # Comparatif + Conformité + assert 'comparatif-title' in body + assert 'conformite-title' in body + # Trust bar 9 ordres + assert 'Mappé aux 9' in body + + +def test_round2_oqlf_nbsp_in_new_sections(): + """OQLF — non-breaking space before currency $ and % in round 2 sections.""" + client = app.test_client() + body = client.get('/').data.decode('utf-8') + # Cycle section savings + assert '3 924 $' in body or '3 924 $' in body + # Wave solution card pricing + assert '173 $/mois' in body or 'Dès 173' in body + # Cadre — Loi 25 fine + assert '25 M$' in body or '25 M$' in body + + def test_routes_passes_testimonials_and_faq_to_template(): """marketing.routes.landing() must pass testimonials and faq to render_template.""" # Import the module to verify the data lists exist