feat(marketing): round 2 — intègre 3 sections de dictia.ca/solutions/dictai (cycle/wave/cadre)

- 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) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-28 12:21:16 -04:00
parent e49652d85d
commit 69baa1be2f
3 changed files with 1274 additions and 0 deletions

View File

@@ -135,6 +135,7 @@
--leading-tight: 1.25; --leading-tight: 1.25;
--leading-snug: 1.375; --leading-snug: 1.375;
--leading-relaxed: 1.625; --leading-relaxed: 1.625;
--radius-sm: 0.25rem;
--radius-md: 0.375rem; --radius-md: 0.375rem;
--radius-lg: 0.5rem; --radius-lg: 0.5rem;
--radius-xl: 0.75rem; --radius-xl: 0.75rem;
@@ -144,6 +145,7 @@
--animate-spin: spin 1s linear infinite; --animate-spin: spin 1s linear infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--blur-sm: 8px; --blur-sm: 8px;
--blur-md: 12px;
--blur-xl: 24px; --blur-xl: 24px;
--blur-3xl: 64px; --blur-3xl: 64px;
--default-transition-duration: 150ms; --default-transition-duration: 150ms;
@@ -365,6 +367,9 @@
.inset-0 { .inset-0 {
inset: calc(var(--spacing) * 0); inset: calc(var(--spacing) * 0);
} }
.inset-\[-8px\] {
inset: -8px;
}
.inset-x-0 { .inset-x-0 {
inset-inline: calc(var(--spacing) * 0); inset-inline: calc(var(--spacing) * 0);
} }
@@ -461,6 +466,9 @@
.right-4 { .right-4 {
right: calc(var(--spacing) * 4); right: calc(var(--spacing) * 4);
} }
.right-\[3\%\] {
right: 3%;
}
.-bottom-10 { .-bottom-10 {
bottom: calc(var(--spacing) * -10); bottom: calc(var(--spacing) * -10);
} }
@@ -470,6 +478,9 @@
.bottom-1\/4 { .bottom-1\/4 {
bottom: calc(1 / 4 * 100%); bottom: calc(1 / 4 * 100%);
} }
.bottom-3 {
bottom: calc(var(--spacing) * 3);
}
.bottom-4 { .bottom-4 {
bottom: calc(var(--spacing) * 4); bottom: calc(var(--spacing) * 4);
} }
@@ -512,12 +523,18 @@
.left-80 { .left-80 {
left: calc(var(--spacing) * 80); left: calc(var(--spacing) * 80);
} }
.left-\[3\%\] {
left: 3%;
}
.z-10 { .z-10 {
z-index: 10; z-index: 10;
} }
.z-20 { .z-20 {
z-index: 20; z-index: 20;
} }
.z-30 {
z-index: 30;
}
.z-40 { .z-40 {
z-index: 40; z-index: 40;
} }
@@ -674,9 +691,15 @@
.mb-2 { .mb-2 {
margin-bottom: calc(var(--spacing) * 2); margin-bottom: calc(var(--spacing) * 2);
} }
.mb-2\.5 {
margin-bottom: calc(var(--spacing) * 2.5);
}
.mb-3 { .mb-3 {
margin-bottom: calc(var(--spacing) * 3); margin-bottom: calc(var(--spacing) * 3);
} }
.mb-3\.5 {
margin-bottom: calc(var(--spacing) * 3.5);
}
.mb-4 { .mb-4 {
margin-bottom: calc(var(--spacing) * 4); margin-bottom: calc(var(--spacing) * 4);
} }
@@ -821,9 +844,21 @@
.h-72 { .h-72 {
height: calc(var(--spacing) * 72); height: calc(var(--spacing) * 72);
} }
.h-\[1\.5px\] {
height: 1.5px;
}
.h-\[2px\] { .h-\[2px\] {
height: 2px; height: 2px;
} }
.h-\[3px\] {
height: 3px;
}
.h-\[17px\] {
height: 17px;
}
.h-\[52px\] {
height: 52px;
}
.h-\[62px\] { .h-\[62px\] {
height: 62px; height: 62px;
} }
@@ -833,6 +868,9 @@
.h-\[88px\] { .h-\[88px\] {
height: 88px; height: 88px;
} }
.h-\[90px\] {
height: 90px;
}
.h-\[95vh\] { .h-\[95vh\] {
height: 95vh; height: 95vh;
} }
@@ -917,6 +955,12 @@
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\.5 {
width: calc(var(--spacing) * 1.5);
}
.w-1\/2 { .w-1\/2 {
width: calc(1 / 2 * 100%); width: calc(1 / 2 * 100%);
} }
@@ -992,12 +1036,21 @@
.w-80 { .w-80 {
width: calc(var(--spacing) * 80); width: calc(var(--spacing) * 80);
} }
.w-\[3px\] {
width: 3px;
}
.w-\[52px\] {
width: 52px;
}
.w-\[68px\] { .w-\[68px\] {
width: 68px; width: 68px;
} }
.w-\[88px\] { .w-\[88px\] {
width: 88px; width: 88px;
} }
.w-\[100px\] {
width: 100px;
}
.w-\[400px\] { .w-\[400px\] {
width: 400px; width: 400px;
} }
@@ -1016,6 +1069,9 @@
.w-full { .w-full {
width: 100%; width: 100%;
} }
.w-px {
width: 1px;
}
.max-w-2xl { .max-w-2xl {
max-width: var(--container-2xl); max-width: var(--container-2xl);
} }
@@ -1251,6 +1307,9 @@
.grid-cols-7 { .grid-cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr)); 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-cols-\[180px_1fr_170px\] {
grid-template-columns: 180px 1fr 170px; grid-template-columns: 180px 1fr 170px;
} }
@@ -1311,6 +1370,9 @@
.gap-4 { .gap-4 {
gap: calc(var(--spacing) * 4); gap: calc(var(--spacing) * 4);
} }
.gap-5 {
gap: calc(var(--spacing) * 5);
}
.gap-6 { .gap-6 {
gap: calc(var(--spacing) * 6); gap: calc(var(--spacing) * 6);
} }
@@ -1388,6 +1450,10 @@
-moz-column-gap: calc(var(--spacing) * 6); -moz-column-gap: calc(var(--spacing) * 6);
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 { .space-x-1 {
:where(& > :not(:last-child)) { :where(& > :not(:last-child)) {
--tw-space-x-reverse: 0; --tw-space-x-reverse: 0;
@@ -1429,6 +1495,9 @@
.gap-y-2 { .gap-y-2 {
row-gap: calc(var(--spacing) * 2); row-gap: calc(var(--spacing) * 2);
} }
.gap-y-3 {
row-gap: calc(var(--spacing) * 3);
}
.divide-y { .divide-y {
:where(& > :not(:last-child)) { :where(& > :not(:last-child)) {
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
@@ -1480,6 +1549,9 @@
.rounded-none { .rounded-none {
border-radius: 0; border-radius: 0;
} }
.rounded-sm {
border-radius: var(--radius-sm);
}
.rounded-xl { .rounded-xl {
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
} }
@@ -1622,6 +1694,21 @@
.border-brand-b1 { .border-brand-b1 {
border-color: #0062ff; 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-brand-b2\/40 {
border-color: color-mix(in oklab, #00bdd8 40%, transparent); 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-color: color-mix(in oklab, var(--color-purple-500) 30%, transparent);
} }
} }
.border-red-100 {
border-color: var(--color-red-100);
}
.border-red-200 { .border-red-200 {
border-color: var(--color-red-200); border-color: var(--color-red-200);
} }
@@ -1703,6 +1793,12 @@
border-color: color-mix(in oklab, var(--color-white) 10%, transparent); 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-white\/20 {
border-color: color-mix(in srgb, #fff 20%, transparent); border-color: color-mix(in srgb, #fff 20%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -1844,12 +1940,21 @@
.bg-amber-100 { .bg-amber-100 {
background-color: var(--color-amber-100); background-color: var(--color-amber-100);
} }
.bg-amber-500 {
background-color: var(--color-amber-500);
}
.bg-amber-500\/10 { .bg-amber-500\/10 {
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent); background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent); 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 { .bg-amber-500\/20 {
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 20%, transparent); background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -1898,12 +2003,18 @@
.bg-brand-b1 { .bg-brand-b1 {
background-color: #0062ff; background-color: #0062ff;
} }
.bg-brand-b1\/5 {
background-color: color-mix(in oklab, #0062ff 5%, transparent);
}
.bg-brand-b1\/10 { .bg-brand-b1\/10 {
background-color: color-mix(in oklab, #0062ff 10%, transparent); background-color: color-mix(in oklab, #0062ff 10%, transparent);
} }
.bg-brand-b1\/15 { .bg-brand-b1\/15 {
background-color: color-mix(in oklab, #0062ff 15%, transparent); 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 { .bg-brand-b3\/10 {
background-color: color-mix(in oklab, #00c896 10%, transparent); background-color: color-mix(in oklab, #00c896 10%, transparent);
} }
@@ -1925,6 +2036,18 @@
.bg-brand-navy { .bg-brand-navy {
background-color: #060d1a; 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\] { .bg-brand-navy\/\[0\.97\] {
background-color: color-mix(in oklab, #060d1a 97%, transparent); background-color: color-mix(in oklab, #060d1a 97%, transparent);
} }
@@ -2033,9 +2156,24 @@
.bg-red-50 { .bg-red-50 {
background-color: var(--color-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 { .bg-red-100 {
background-color: var(--color-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 { .bg-red-500\/10 {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent); background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -2169,6 +2307,14 @@
--tw-gradient-from: var(--color-blue-600); --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)); --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 { .from-orange-500 {
--tw-gradient-from: var(--color-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)); --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-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)); --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 { .to-purple-500 {
--tw-gradient-to: var(--color-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)); --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 { .px-3 {
padding-inline: calc(var(--spacing) * 3); padding-inline: calc(var(--spacing) * 3);
} }
.px-3\.5 {
padding-inline: calc(var(--spacing) * 3.5);
}
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
} }
@@ -2331,9 +2488,15 @@
.py-4 { .py-4 {
padding-block: calc(var(--spacing) * 4); padding-block: calc(var(--spacing) * 4);
} }
.py-5 {
padding-block: calc(var(--spacing) * 5);
}
.py-6 { .py-6 {
padding-block: calc(var(--spacing) * 6); padding-block: calc(var(--spacing) * 6);
} }
.py-7 {
padding-block: calc(var(--spacing) * 7);
}
.py-8 { .py-8 {
padding-block: calc(var(--spacing) * 8); padding-block: calc(var(--spacing) * 8);
} }
@@ -2515,6 +2678,9 @@
font-size: var(--text-xs); font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height)); line-height: var(--tw-leading, var(--text-xs--line-height));
} }
.text-\[8px\] {
font-size: 8px;
}
.text-\[9px\] { .text-\[9px\] {
font-size: 9px; font-size: 9px;
} }
@@ -2524,6 +2690,9 @@
.text-\[11px\] { .text-\[11px\] {
font-size: 11px; font-size: 11px;
} }
.text-\[12px\] {
font-size: 12px;
}
.text-\[13px\] { .text-\[13px\] {
font-size: 13px; font-size: 13px;
} }
@@ -2606,6 +2775,30 @@
--tw-tracking: 0.12em; --tw-tracking: 0.12em;
letter-spacing: 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 { .tracking-tight {
--tw-tracking: var(--tracking-tight); --tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight); letter-spacing: var(--tracking-tight);
@@ -2730,6 +2923,24 @@
.text-brand-b1 { .text-brand-b1 {
color: #0062ff; 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 { .text-brand-b2 {
color: #00bdd8; color: #00bdd8;
} }
@@ -2742,18 +2953,33 @@
.text-brand-navy\/30 { .text-brand-navy\/30 {
color: color-mix(in oklab, #060d1a 30%, transparent); color: color-mix(in oklab, #060d1a 30%, transparent);
} }
.text-brand-navy\/35 {
color: color-mix(in oklab, #060d1a 35%, transparent);
}
.text-brand-navy\/40 { .text-brand-navy\/40 {
color: color-mix(in oklab, #060d1a 40%, transparent); color: color-mix(in oklab, #060d1a 40%, transparent);
} }
.text-brand-navy\/45 {
color: color-mix(in oklab, #060d1a 45%, transparent);
}
.text-brand-navy\/50 { .text-brand-navy\/50 {
color: color-mix(in oklab, #060d1a 50%, transparent); color: color-mix(in oklab, #060d1a 50%, transparent);
} }
.text-brand-navy\/55 {
color: color-mix(in oklab, #060d1a 55%, transparent);
}
.text-brand-navy\/60 { .text-brand-navy\/60 {
color: color-mix(in oklab, #060d1a 60%, transparent); color: color-mix(in oklab, #060d1a 60%, transparent);
} }
.text-brand-navy\/65 {
color: color-mix(in oklab, #060d1a 65%, transparent);
}
.text-brand-navy\/70 { .text-brand-navy\/70 {
color: color-mix(in oklab, #060d1a 70%, transparent); color: color-mix(in oklab, #060d1a 70%, transparent);
} }
.text-brand-navy\/75 {
color: color-mix(in oklab, #060d1a 75%, transparent);
}
.text-brand-navy\/80 { .text-brand-navy\/80 {
color: color-mix(in oklab, #060d1a 80%, transparent); color: color-mix(in oklab, #060d1a 80%, transparent);
} }
@@ -2769,6 +2995,9 @@
.text-emerald-500 { .text-emerald-500 {
color: var(--color-emerald-500); color: var(--color-emerald-500);
} }
.text-emerald-600 {
color: var(--color-emerald-600);
}
.text-gray-200 { .text-gray-200 {
color: var(--color-gray-200); color: var(--color-gray-200);
} }
@@ -2832,6 +3061,36 @@
.text-red-500 { .text-red-500 {
color: var(--color-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 { .text-red-600 {
color: var(--color-red-600); color: var(--color-red-600);
} }
@@ -2880,6 +3139,12 @@
color: color-mix(in oklab, var(--color-white) 50%, transparent); 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 { .text-white\/60 {
color: color-mix(in srgb, #fff 60%, transparent); color: color-mix(in srgb, #fff 60%, transparent);
@supports (color: color-mix(in lab, red, red)) { @supports (color: color-mix(in lab, red, red)) {
@@ -2938,6 +3203,9 @@
--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,);
} }
.no-underline {
text-decoration-line: none;
}
.underline { .underline {
text-decoration-line: underline; text-decoration-line: underline;
} }
@@ -3007,6 +3275,9 @@
.opacity-100 { .opacity-100 {
opacity: 100%; opacity: 100%;
} }
.opacity-\[0\.02\] {
opacity: 0.02;
}
.shadow { .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)); --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); 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)); --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); 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\)\] { .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)); --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); 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); --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-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 { .backdrop-blur-sm {
--tw-backdrop-blur: blur(var(--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,); 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 { .group-hover\:opacity-100 {
&:is(:where(.group):hover *) { &:is(:where(.group):hover *) {
@media (hover: hover) { @media (hover: hover) {
@@ -4215,6 +4501,11 @@
outline-offset: 4px; outline-offset: 4px;
} }
} }
.focus-visible\:outline-amber-500 {
&:focus-visible {
outline-color: var(--color-amber-500);
}
}
.focus-visible\:outline-brand-b1 { .focus-visible\:outline-brand-b1 {
&:focus-visible { &:focus-visible {
outline-color: #0062ff; outline-color: #0062ff;
@@ -4619,11 +4910,31 @@
margin-bottom: calc(var(--spacing) * 4); margin-bottom: calc(var(--spacing) * 4);
} }
} }
.md\:block {
@media (width >= 48rem) {
display: block;
}
}
.md\:flex { .md\:flex {
@media (width >= 48rem) { @media (width >= 48rem) {
display: flex; 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 { .md\:h-48 {
@media (width >= 48rem) { @media (width >= 48rem) {
height: calc(var(--spacing) * 48); height: calc(var(--spacing) * 48);
@@ -4634,11 +4945,31 @@
min-height: 12rem; 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 { .md\:w-48 {
@media (width >= 48rem) { @media (width >= 48rem) {
width: calc(var(--spacing) * 48); 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 { .md\:grid-cols-2 {
@media (width >= 48rem) { @media (width >= 48rem) {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -4654,6 +4985,11 @@
grid-template-columns: repeat(4, minmax(0, 1fr)); 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 { .md\:flex-row {
@media (width >= 48rem) { @media (width >= 48rem) {
flex-direction: row; flex-direction: row;
@@ -4674,6 +5010,18 @@
gap: calc(var(--spacing) * 5); 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 { .md\:p-2 {
@media (width >= 48rem) { @media (width >= 48rem) {
padding: calc(var(--spacing) * 2); padding: calc(var(--spacing) * 2);
@@ -4770,6 +5118,11 @@
line-height: var(--tw-leading, var(--text-sm--line-height)); line-height: var(--tw-leading, var(--text-sm--line-height));
} }
} }
.md\:text-\[10px\] {
@media (width >= 48rem) {
font-size: 10px;
}
}
.lg\:relative { .lg\:relative {
@media (width >= 64rem) { @media (width >= 64rem) {
position: relative; position: relative;

View File

@@ -422,6 +422,341 @@
</div> </div>
</section> </section>
{# ===== 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) #}
<style>
/* Cycle pulse rings — appliqués au nœud source "Réunion en cours" */
@keyframes cycle-pulse-ring {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(2.1); opacity: 0; }
}
.cycle-pulse { animation: cycle-pulse-ring 2.4s ease-out infinite; }
.cycle-pulse-2 { animation-delay: 0.65s; }
.cycle-pulse-3 { animation-delay: 1.3s; }
/* Cycle col 3 (DictIA) — bordure lumineuse pulsante */
@keyframes cycle-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(0,189,216,0.12); }
50% { box-shadow: 0 0 35px 0 rgba(0,189,216,0.30); }
}
.cycle-card-dictia.is-visible { animation: cycle-glow 3.8s ease-in-out infinite; }
/* Cycle col 3 — anneaux concentriques autour du Lock */
@keyframes cycle-ring-out {
0%, 100% { transform: scale(1); opacity: 0.45; }
50% { transform: scale(1.07); opacity: 0.18; }
}
.cycle-ring-outer.is-visible { animation: cycle-ring-out 3.8s ease-in-out infinite; }
.cycle-ring-inner.is-visible { animation: cycle-ring-out 2.9s ease-in-out infinite 0.5s; }
/* Cycle SVG lignes de connexion — drawn via stroke-dashoffset */
.cycle-line { stroke-dasharray: 50; stroke-dashoffset: 50; transition: stroke-dashoffset 600ms ease-out; }
.cycle-line.is-visible { stroke-dashoffset: 0; }
/* Cycle phase reveal — colonnes 1+2 (phase 2), overlay (phase 3), col 3 (phase 4) */
.cycle-reveal { opacity: 0; transform: translateX(14px); transition: opacity 380ms ease-out, transform 380ms ease-out; }
.cycle-reveal.is-visible { opacity: 1; transform: translateX(0); }
.cycle-reveal-up { opacity: 0; transform: translateY(10px); transition: opacity 500ms ease-out, transform 500ms ease-out; }
.cycle-reveal-up.is-visible { opacity: 1; transform: translateY(0); }
/* Cycle horloge rotation (col 1) */
@keyframes cycle-clock-spin { to { transform: rotate(360deg); } }
.cycle-clock { animation: cycle-clock-spin 8s linear infinite; transform-origin: center; display: inline-block; }
/* Cycle col 2 — fuite particules rouges */
@keyframes cycle-leak {
0% { transform: translate(0,0) scale(1); opacity: 0; }
20% { opacity: 0.75; }
100% { transform: translate(var(--lx,18px), var(--ly,-22px)) scale(0.4); opacity: 0; }
}
.cycle-leak-particle { animation: cycle-leak 1.8s ease-out infinite; }
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.cycle-pulse, .cycle-card-dictia, .cycle-ring-outer, .cycle-ring-inner,
.cycle-clock, .cycle-leak-particle { animation: none !important; }
.cycle-reveal, .cycle-reveal-up { opacity: 1 !important; transform: none !important; }
.cycle-line { stroke-dashoffset: 0 !important; }
}
</style>
<section
class="bg-white py-20 border-y border-brand-border relative overflow-hidden"
aria-labelledby="cycle-title"
x-data="{ phase: 0, observer: null }"
x-init="
observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting && phase === 0) {
setTimeout(() => phase = 1, 250);
setTimeout(() => phase = 2, 1100);
setTimeout(() => phase = 3, 2200);
setTimeout(() => phase = 4, 3000);
observer.disconnect();
}
});
}, { rootMargin: '-60px' });
observer.observe($el);
"
>
{# Subtle dot-grid bg #}
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"
style="background-image: radial-gradient(circle, rgba(11,15,26,0.04) 1px, transparent 1px); background-size: 28px 28px;"></div>
<div class="relative max-w-[1200px] mx-auto px-6">
<div class="max-w-2xl mb-10">
<p class="eyebrow text-amber-600 mb-4">⚠ CADRE RÉGLEMENTAIRE</p>
<h2 id="cycle-title" class="text-[clamp(2rem,3vw,2.75rem)] font-black mb-3 text-brand-navy">
Trois options. <span class="text-brand-navy/30">Une seule est conforme.</span>
</h2>
<p class="text-base text-brand-navy/70">
La Loi&nbsp;25, le Cloud Act américain et le Guide IA du Barreau du Québec délimitent vos options&nbsp;
<span class="font-semibold text-brand-b1">pas votre volonté.</span>
</p>
</div>
<div class="max-w-5xl mx-auto">
{# Nœud source : "Réunion en cours" #}
<div class="flex justify-center mb-2">
<div class="relative inline-flex items-center gap-2.5 px-5 py-3 bg-brand-bg border border-brand-border rounded">
<template x-if="phase >= 1">
<span class="absolute inset-0 cycle-pulse pointer-events-none border border-brand-border rounded"></span>
</template>
<template x-if="phase >= 1">
<span class="absolute inset-0 cycle-pulse cycle-pulse-2 pointer-events-none border border-brand-border rounded"></span>
</template>
<template x-if="phase >= 1">
<span class="absolute inset-0 cycle-pulse cycle-pulse-3 pointer-events-none border border-brand-border rounded"></span>
</template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3.5 h-3.5 shrink-0 text-brand-navy/40" aria-hidden="true"><path d="M3 21h18"/><path d="M5 21V8l7-4 7 4v13"/><path d="M9 21v-6h6v6"/></svg>
<span class="text-[11px] tracking-wide text-brand-navy/55">Réunion en cours — données confidentielles</span>
</div>
</div>
{# Lignes de connexion SVG — de la source vers les 3 colonnes #}
<div class="relative h-10">
<svg class="w-full h-full" viewBox="0 0 100 10" preserveAspectRatio="none" aria-hidden="true">
<line class="cycle-line" :class="phase >= 1 ? 'is-visible' : ''"
x1="50" y1="0" x2="14" y2="10"
stroke="rgba(148,163,184,0.45)" stroke-width="0.3" stroke-dasharray="1.2 0.9" />
<line class="cycle-line" :class="phase >= 1 ? 'is-visible' : ''"
x1="50" y1="0" x2="50" y2="10"
stroke="rgba(239,68,68,0.45)" stroke-width="0.3" stroke-dasharray="1.2 0.9"
style="transition-delay: 80ms;" />
<line class="cycle-line" :class="phase >= 1 ? 'is-visible' : ''"
x1="50" y1="0" x2="86" y2="10"
stroke="rgba(0,189,216,0.55)" stroke-width="0.3" stroke-dasharray="1.2 0.9"
style="transition-delay: 160ms;" />
</svg>
</div>
{# Grille 3 colonnes — équivalence du grid-cols-[2fr_2fr_3fr] desktop #}
<div class="grid grid-cols-1 md:grid-cols-[2fr_2fr_3fr] gap-3 items-stretch">
{# COL 1 — Retranscription humaine #}
<div class="cycle-reveal-up flex flex-col rounded border border-brand-border bg-brand-bg overflow-hidden"
:class="phase >= 2 ? 'is-visible' : ''">
<div class="px-5 py-3 border-b border-brand-border">
<div class="font-mono text-[10px] tracking-[0.28em] uppercase text-brand-navy/40 mb-0.5">01</div>
<div class="font-bold text-sm text-brand-navy/55">Retranscription humaine</div>
</div>
<div class="flex-1 flex flex-col items-center justify-center px-5 py-7 gap-6">
{# Stack de papiers #}
<div class="relative w-[100px] h-[90px]" aria-hidden="true">
{% for offset in [0,1,2,3,4] %}
<div class="absolute left-0 right-0 h-[17px] rounded-sm flex items-center px-2 gap-1.5 bg-white border border-brand-border"
style="bottom: {{ offset * 15 }}px; transform: rotate({{ '1deg' if offset is even else '-1deg' }});">
<div class="w-2.5 h-1.5 rounded-sm bg-brand-navy/15 shrink-0"></div>
<div class="flex-1 h-[1.5px] rounded-full bg-brand-navy/10"></div>
</div>
{% endfor %}
</div>
<div class="flex items-center gap-4 w-full">
<div class="flex flex-col items-center gap-1.5 flex-1">
<span class="cycle-clock">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5 text-brand-navy/45" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</span>
<span class="font-mono text-[10px] text-brand-navy/45">5&nbsp;jours</span>
</div>
<div class="w-px h-8 bg-brand-border"></div>
<div class="flex flex-col items-center gap-1.5 flex-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5 text-brand-navy/45" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
<span class="font-mono text-[10px] text-brand-navy/45">~85&nbsp;$/h</span>
</div>
</div>
</div>
<div class="px-5 pb-5 pt-4 border-t border-brand-border">
<div class="flex items-baseline gap-1.5 mb-2">
<span class="font-black text-3xl leading-none text-brand-navy/65">315</span>
<span class="text-xs text-brand-navy/45">$ / réunion</span>
</div>
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded bg-red-50 border border-red-100">
<span class="w-1 h-1 rounded-full bg-red-400"></span>
<span class="font-mono text-[9px] tracking-[0.16em] uppercase text-red-500/80">Lent · Coûteux</span>
</span>
</div>
</div>
{# COL 2 — IA cloud américaine #}
<div class="cycle-reveal-up relative flex flex-col rounded border border-red-200 bg-red-50/30 overflow-hidden"
:class="phase >= 2 ? 'is-visible' : ''"
style="transition-delay: 120ms;">
{# Overlay légal NON CONFORME (phase 3) #}
<div class="absolute inset-0 z-30 flex flex-col items-center justify-center pointer-events-none cycle-reveal-up"
:class="phase >= 3 ? 'is-visible' : ''"
style="backdrop-filter: blur(6px); background: rgba(255,255,255,0.78);">
<div class="flex flex-col items-center gap-3 px-6 py-5 rounded bg-white border border-red-300 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" class="w-7 h-7 text-red-500" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<div class="text-center">
<div class="font-mono font-bold tracking-[0.14em] text-sm uppercase text-red-600">NON CONFORME</div>
<div class="font-mono text-[10px] tracking-[0.20em] uppercase mt-1.5 text-red-500/70">Loi 25 · Cloud Act américain</div>
</div>
</div>
</div>
<div class="px-5 py-3 border-b border-red-100">
<div class="font-mono text-[10px] tracking-[0.28em] uppercase text-red-500/60 mb-0.5">02</div>
<div class="font-bold text-sm text-red-500/75">IA cloud américaine</div>
</div>
<div class="relative flex-1 flex flex-col items-center justify-center px-5 py-6 gap-5">
<div class="relative flex flex-col items-center gap-2">
<div class="relative w-14 h-14 rounded flex items-center justify-center bg-red-100/60 border border-red-200" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6 text-red-500/70"><path d="M2 12s3-7 10-7c2.5 0 4.7.9 6.4 2.4M22 12s-3 7-10 7c-2.5 0-4.7-.9-6.4-2.4"/><line x1="2" y1="2" x2="22" y2="22"/></svg>
{# Particules de fuite #}
{% for i in range(6) %}
<span class="cycle-leak-particle absolute w-[3px] h-[3px] rounded-full bg-red-400 pointer-events-none"
style="left: 50%; top: 50%; --lx: {{ (-30 + i*12) }}px; --ly: -{{ 14 + (i % 3) * 8 }}px; animation-delay: {{ i * 0.20 }}s;"></span>
{% endfor %}
</div>
<div class="px-2 py-0.5 rounded bg-red-50 border border-red-100">
<span class="font-mono text-[9px] tracking-[0.22em] uppercase text-red-500/65">USA</span>
</div>
</div>
<div class="flex flex-col gap-2.5 w-full">
{% for risk in [
('cloud', 'Données transférées aux États-Unis'),
('alert', "Entraînement de l'IA publique"),
('users', '500&ndash;750&nbsp;$/mois · facturation par utilisateur')
] %}
<div class="flex items-start gap-2.5">
{% if risk[0] == 'cloud' %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 mt-0.5 shrink-0 text-red-500/60" aria-hidden="true"><path d="M2 12s3-7 10-7c2.5 0 4.7.9 6.4 2.4M22 12s-3 7-10 7c-2.5 0-4.7-.9-6.4-2.4"/><line x1="2" y1="2" x2="22" y2="22"/></svg>
{% elif risk[0] == 'alert' %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 mt-0.5 shrink-0 text-red-500/60" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 mt-0.5 shrink-0 text-red-500/60" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/></svg>
{% endif %}
<span class="text-[12px] leading-snug text-brand-navy/55">{{ risk[1] | safe }}</span>
</div>
{% endfor %}
</div>
</div>
<div class="px-5 pb-5 pt-4 border-t border-red-100">
<div class="flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full bg-red-400"></div>
<span class="font-mono text-[9px] tracking-[0.20em] uppercase text-red-500/70">Violation légale possible</span>
</div>
</div>
</div>
{# COL 3 — DictIA (featured, 3fr) #}
<div class="cycle-card-dictia cycle-reveal-up relative flex flex-col rounded border border-brand-b1/35 bg-gradient-to-br from-brand-b1/[0.06] to-brand-b3/[0.04] overflow-hidden"
:class="phase >= 4 ? 'is-visible' : ''"
style="transition-delay: 240ms;">
{# Halo ambiant #}
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"
style="background: radial-gradient(ellipse 80% 35% at 50% 0%, rgba(0,189,216,0.12) 0%, transparent 65%);"></div>
<div class="relative px-5 py-3 border-b border-brand-b1/15 flex items-center gap-2.5">
<div class="relative w-4 h-4 flex items-center justify-center shrink-0">
<div class="absolute inset-0 rounded-full bg-brand-b1/15 border border-brand-b1/30"></div>
<div class="w-1.5 h-1.5 rounded-full bg-brand-b1"></div>
</div>
<span class="text-[11px] uppercase tracking-[0.22em] font-semibold text-brand-b1/80">Solution</span>
</div>
<div class="relative flex-1 flex flex-col items-center justify-center px-5 py-7 gap-5">
{# Lock + anneaux concentriques #}
<div class="relative flex items-center justify-center" aria-hidden="true">
<span class="cycle-ring-outer absolute rounded-full border border-brand-b1/15"
:class="phase >= 4 ? 'is-visible' : ''"
style="width: 96px; height: 96px;"></span>
<span class="cycle-ring-inner absolute rounded-full border border-brand-b1/25"
:class="phase >= 4 ? 'is-visible' : ''"
style="width: 68px; height: 68px;"></span>
<div class="relative w-[52px] h-[52px] rounded flex items-center justify-center bg-gradient-to-br from-brand-b1/15 to-brand-b3/10 border border-brand-b1/35">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6 text-brand-b1"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
</div>
{# Flux sécurisé #}
<div class="cycle-reveal flex items-center gap-2 px-3.5 py-1.5 rounded bg-brand-b1/5 border border-brand-b1/15"
:class="phase >= 4 ? 'is-visible' : ''"
style="transition-delay: 350ms;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 shrink-0 text-brand-b1/55" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/></svg>
<span class="text-[11px] text-brand-b1/70">Transcription sécurisée</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 text-brand-b1/45" aria-hidden="true"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 shrink-0 text-brand-b1/55" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
</div>
{# Badges conformité #}
<div class="flex flex-col gap-2 w-full">
{% for badge_label in ['Loi 25 conforme', '100&nbsp;% hébergé au Québec', 'Données jamais partagées'] %}
<div class="cycle-reveal flex items-center gap-2.5 px-3.5 py-2.5 rounded bg-brand-b1/[0.06] border border-brand-b1/20"
:class="phase >= 4 ? 'is-visible' : ''"
style="transition-delay: {{ 180 + loop.index0 * 170 }}ms;">
<span class="w-4 h-4 rounded-full flex items-center justify-center shrink-0 bg-brand-b1/15 border border-brand-b1/30">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-2.5 h-2.5 text-brand-b1" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>
</span>
<span class="text-[12px] font-medium text-brand-navy/75">{{ badge_label | safe }}</span>
</div>
{% endfor %}
</div>
</div>
<div class="relative px-5 pb-5 pt-4 border-t border-brand-b1/15">
<div class="flex items-center gap-2 mb-2.5">
<span class="flex-1 h-px bg-brand-b1/15"></span>
<span class="font-mono font-bold text-[10px] uppercase tracking-[0.20em] text-brand-b1">UTILISATEURS ILLIMITÉS</span>
<span class="flex-1 h-px bg-brand-b1/15"></span>
</div>
<p class="text-center text-[10px] mb-3.5 text-brand-b1/55">Zéro frais caché · Du jamais vu</p>
<div class="flex items-baseline gap-2">
<span class="font-black leading-none text-5xl text-brand-b1">173</span>
<span class="text-sm text-brand-navy/45 mb-0.5">$ / mois</span>
</div>
<div class="flex items-center gap-1.5 mt-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 text-brand-b1/65" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<p class="text-[10px] text-brand-b1/60">Conforme Loi&nbsp;25 · 100&nbsp;% Québec</p>
</div>
</div>
</div>
</div>
{# Barre d'économies — apparaît avec phase 4 #}
<div class="cycle-reveal-up mt-10 flex flex-wrap items-center gap-x-8 gap-y-3 justify-center"
:class="phase >= 4 ? 'is-visible' : ''"
style="transition-delay: 700ms;">
<div class="flex items-center gap-2 text-[12px] text-brand-navy/55">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3.5 h-3.5 text-emerald-500" aria-hidden="true"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/></svg>
Économies annuelles · 25 utilisateurs
</div>
{% for sav in [('3&nbsp;924&nbsp;$', 'vs Otter.ai'), ('6&nbsp;924&nbsp;$', 'vs MS Teams'), ('2&nbsp;004&nbsp;$', 'vs Sténographe')] %}
<div class="flex items-baseline gap-1.5">
<span class="font-black text-xl text-emerald-600">{{ sav[0] | safe }}</span>
<span class="text-xs text-brand-navy/55">{{ sav[1] }}</span>
</div>
{% endfor %}
</div>
</div>
</div>
</section>
{# ===== SOLUTION (S de PAS) ===== #} {# ===== SOLUTION (S de PAS) ===== #}
<section class="relative bg-brand-navy text-white py-20 overflow-hidden" aria-labelledby="solution-title"> <section class="relative bg-brand-navy text-white py-20 overflow-hidden" aria-labelledby="solution-title">
{# Single subtle orb in solution bg — less busy than hero #} {# Single subtle orb in solution bg — less busy than hero #}
@@ -455,6 +790,245 @@
</div> </div>
</section> </section>
{# ===== 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. #}
<style>
/* Wave bars — chaque barre est composée d'un wrapper + 2 spans (rouge et cyan) qu'on fait fader */
.wave-bar { transition: opacity 60ms linear, transform 60ms linear; }
/* Wave pain labels — flottent légèrement */
@keyframes wave-pain-float {
0%, 100% { transform: translate(-50%, calc(-50% - 2px)); opacity: 0.7; }
50% { transform: translate(-50%, calc(-50% + 3px)); opacity: 0.45; }
}
.wave-pain { animation: wave-pain-float 3.4s ease-in-out infinite; }
.wave-pain:nth-child(2) { animation-delay: 0.4s; }
.wave-pain:nth-child(3) { animation-delay: 0.8s; }
.wave-pain:nth-child(4) { animation-delay: 1.2s; }
/* Wave fall particles — chute des $ et h */
@keyframes wave-fall {
0% { transform: translate(-50%, 0) scale(1); opacity: 0.85; }
50% { transform: translate(-50%, 22px) scale(0.7); opacity: 0.30; }
100% { transform: translate(-50%, 42px) scale(0.3); opacity: 0; }
}
.wave-fall { animation: wave-fall 2.4s ease-out infinite; }
/* Wave flow particles — flux cyan gauche→droite (état ordre) */
@keyframes wave-flow {
0% { left: 12%; opacity: 0; }
20% { opacity: 0.85; }
100% { left: 84%; opacity: 0; }
}
.wave-flow { animation: wave-flow 1.7s linear infinite; }
/* Wave hint — chevron CTA hover droite */
@keyframes wave-hint-pulse {
0%, 100% { transform: translateX(0); opacity: 0.55; }
50% { transform: translateX(7px); opacity: 1; }
}
.wave-hint { animation: wave-hint-pulse 1.5s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.wave-pain, .wave-fall, .wave-flow, .wave-hint { animation: none !important; }
}
</style>
<section class="bg-brand-navy text-white py-20 border-t border-white/[0.06] overflow-hidden" aria-labelledby="wave-title"
x-data="{
p: 0,
isMobile: false,
activated: false,
init() {
this.isMobile = window.innerWidth < 768;
window.addEventListener('resize', () => { this.isMobile = window.innerWidth < 768; });
},
onMove(e) {
if (this.isMobile) return;
const r = e.currentTarget.getBoundingClientRect();
this.p = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
},
onLeave() { if (!this.isMobile) this.p = 0; },
toggle() { this.activated = !this.activated; this.p = this.activated ? 1 : 0; },
clamp(v) { return Math.max(0, Math.min(1, v)); },
get chaosOp() { return this.clamp(1 - (this.p - 0.12) / 0.42); },
get orderOp() { return this.clamp((this.p - 0.42) / 0.42); }
}">
<div class="max-w-[1200px] mx-auto px-6">
<div class="flex items-center gap-3 mb-5">
<span class="eyebrow text-brand-b3">Le problème</span>
<span class="flex-1 h-px bg-white/10 max-w-[60px]"></span>
</div>
<h2 id="wave-title" class="text-[clamp(2rem,3vw,2.75rem)] font-black leading-[1.05] mb-3">
La transcription manuelle <span class="grad-text">coûte cher</span>.
</h2>
<p class="text-base text-white/55 mb-8 max-w-lg hidden md:block">
Déplacez votre curseur vers la droite pour transcrire l'avenir.
</p>
<p class="text-base text-white/55 mb-8 max-w-lg md:hidden">
Touchez pour basculer entre&nbsp;: aujourd'hui (chaos) et avec DictIA (ordre).
</p>
{# Scène interactive — aspect-ratio 2/1 desktop, 4/3 mobile #}
<div
class="relative w-full max-w-5xl mx-auto overflow-hidden rounded border border-white/[0.06]"
style="aspect-ratio: 2 / 1; min-height: 280px; cursor: ew-resize;"
:style="isMobile ? 'cursor: pointer; aspect-ratio: 4/3' : ''"
@mousemove="onMove($event)"
@mouseleave="onLeave()"
@click="if (isMobile) toggle()"
role="img"
aria-label="Visualisation interactive : transcription manuelle (chaos rouge) vs DictIA (ordre cyan)"
>
{# Fond dégradé radial dynamique #}
<div class="absolute inset-0"
:style="`background: radial-gradient(ellipse 75% 75% at 50% 50%, rgba(${Math.round(45 + (11-45)*p)},${Math.round(10 + (15-10)*p)},${Math.round(12 + (40-12)*p)},0.55) 0%, rgba(11,15,26,1) 100%);`"
aria-hidden="true"></div>
{# Grille subtile #}
<div class="absolute inset-0 opacity-[0.02]"
style="background-image: linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px); background-size: 40px 40px;"
aria-hidden="true"></div>
{# SVG : 30 barres d'onde — calculées par macro Jinja (chaos vs order par lerp p) #}
<svg class="absolute inset-0 w-full h-full pointer-events-none z-10" viewBox="0 0 100 50" preserveAspectRatio="none" aria-hidden="true">
<defs>
<filter id="dw-glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="0.9" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
{# Fixed bar geometry — nous générons 30 barres avec hauteur min/max statique;
l'opacité chaos vs order est gérée par 2 groupes superposés contrôlés par x-attr style. #}
{% set NUM_BARS = 30 %}
{% set chaos_heights = [10.5, 13.2, 15.8, 17.4, 18.1, 17.6, 15.9, 13.6, 11.2, 9.5, 8.7, 9.4, 11.7, 14.5, 17.2, 18.8, 18.6, 16.9, 14.3, 11.5, 9.2, 8.4, 9.8, 12.6, 15.5, 17.5, 17.8, 16.2, 13.4, 10.1] %}
{% set order_heights = [0.85, 0.95, 1.05, 1.15, 1.10, 1.00, 0.92, 0.88, 1.00, 1.12, 1.18, 1.14, 1.05, 0.95, 0.88, 0.92, 1.05, 1.15, 1.18, 1.10, 1.00, 0.92, 0.88, 0.95, 1.10, 1.18, 1.15, 1.05, 0.95, 0.90] %}
{% for i in range(NUM_BARS) %}
{% set spacing = 84 / NUM_BARS %}
{% set bw = spacing * 0.52 %}
{% set cx = 8 + i * spacing + spacing / 2 %}
{# Chaos bar — visible quand p ≈ 0 #}
<g class="wave-bar" :style="`opacity: ${1 - p}`">
<rect x="{{ cx - bw / 2 }}" y="{{ 25 - chaos_heights[i] / 2 }}" width="{{ bw }}" height="{{ chaos_heights[i] }}" rx="{{ bw / 2 }}" fill="rgba(255,75,75,0.78)"/>
</g>
{# Order bar — visible quand p ≈ 1 #}
<g class="wave-bar" :style="`opacity: ${p}`">
<rect x="{{ cx - bw / 2 }}" y="{{ 25 - order_heights[i] / 2 }}" width="{{ bw }}" height="{{ order_heights[i] }}" rx="{{ bw / 2 }}" fill="rgba(107,159,255,0.6)"/>
</g>
{% endfor %}
{# Bouclier holographique (état ordre) #}
<path d="M 50 8 L 68 14 L 68 27 Q 68 38 50 43 Q 32 38 32 27 L 32 14 Z"
fill="none" stroke="rgba(107,159,255,0.55)" stroke-width="0.55" stroke-dasharray="2.5 2"
:style="`opacity: ${orderOp * 0.4}`"/>
</svg>
{# Icône Microphone (gauche) #}
<div class="absolute z-20 left-[3%] top-1/2 -translate-y-1/2 flex flex-col items-center gap-1.5"
:style="`opacity: ${Math.max(0.25, 1 - p * 0.55)}`" aria-hidden="true">
<div class="w-10 h-10 md:w-12 md:h-12 rounded flex items-center justify-center backdrop-blur-md border"
:style="`background: rgba(${Math.round(255 + (107-255)*p)},${Math.round(60 + (130-60)*p)},${Math.round(60 + (159-60)*p)},0.12); border-color: rgba(${Math.round(255 + (107-255)*p)},${Math.round(80 + (159-80)*p)},${Math.round(80 + (255-80)*p)},0.28);`">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 md:w-5 md:h-5"
:style="`color: ${p < 0.5 ? '#FF6B6B' : '#6B9FFF'}`"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>
</div>
<span class="text-[8px] font-medium" :style="`color: ${p < 0.5 ? 'rgba(255,107,107,0.65)' : 'rgba(107,159,255,0.65)'}`">Audio</span>
</div>
{# Icône Texte transcrit (droite) #}
<div class="absolute z-20 right-[3%] top-1/2 -translate-y-1/2 flex flex-col items-center gap-1.5"
:style="`opacity: ${orderOp}`" aria-hidden="true">
<div class="w-10 h-10 md:w-12 md:h-12 rounded flex items-center justify-center backdrop-blur-md border border-brand-b1/30"
style="background: linear-gradient(135deg, rgba(107,159,255,0.15), rgba(45,127,249,0.07)); box-shadow: 0 0 18px rgba(107,159,255,0.18);">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 md:w-5 md:h-5 text-brand-b1"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/></svg>
</div>
<span class="text-[8px] font-medium text-brand-b1/70">Texte</span>
</div>
{# Particules tombantes chaos ($, h) #}
<div class="absolute inset-0 z-10 pointer-events-none" :style="`opacity: ${chaosOp}`" aria-hidden="true">
{% for fp in [
{'x': 22, 'd': 0, 'type': 'money'},
{'x': 36, 'd': 0.9, 'type': 'time'},
{'x': 50, 'd': 1.5, 'type': 'money'},
{'x': 63, 'd': 0.4, 'type': 'time'},
{'x': 77, 'd': 1.2, 'type': 'money'},
{'x': 30, 'd': 1.9, 'type': 'time'},
{'x': 56, 'd': 0.7, 'type': 'money'},
{'x': 43, 'd': 2.1, 'type': 'time'}
] %}
<span class="wave-fall absolute font-bold text-[9px] md:text-[10px]"
style="left: {{ fp.x }}%; top: 30%; color: {{ '#FF6B6B' if fp.type == 'money' else '#FFD93D' }}; text-shadow: 0 0 8px rgba({{ '255,80,80' if fp.type == 'money' else '255,210,60' }},0.6); animation-delay: {{ fp.d }}s;">{{ '-$' if fp.type == 'money' else '-1h' }}</span>
{% endfor %}
</div>
{# Flux cyan (ordre) #}
<div class="absolute inset-0 z-10 pointer-events-none" :style="`opacity: ${orderOp}`" aria-hidden="true">
{% for fy in [18, 21, 24, 27, 30] %}
<span class="wave-flow absolute w-1 h-1 rounded-full bg-brand-b1"
style="top: {{ fy }}%; left: 12%; box-shadow: 0 0 5px rgba(107,159,255,0.7); animation-delay: {{ loop.index0 * 0.32 }}s;"></span>
{% endfor %}
</div>
{# Étiquettes douleur (chaos) #}
<div class="absolute inset-0 pointer-events-none" :style="`opacity: ${chaosOp}`" aria-hidden="true">
{% for pain in [
{'text': '4 à 6h pour transcrire 1h', 'x': 20, 'y': 12},
{'text': '600 à 1&nbsp;800&nbsp;$ / enregistrement', 'x': 64, 'y': 10},
{'text': 'Délais de 48h à 5 jours', 'x': 74, 'y': 38},
{'text': 'Risques de confidentialité', 'x': 18, 'y': 40}
] %}
<div class="wave-pain absolute text-[8px] md:text-[10px] font-medium px-2 py-0.5 rounded whitespace-nowrap"
style="left: {{ pain.x }}%; top: {{ pain.y }}%; transform: translate(-50%, -50%); background: rgba(255,50,50,0.1); border: 1px solid rgba(255,80,80,0.18); color: rgba(255,150,130,0.85);">
{{ pain.text | safe }}
</div>
{% endfor %}
</div>
{# Cartes solution (ordre) #}
<div class="absolute inset-0 pointer-events-none" :style="`opacity: ${orderOp}`" aria-hidden="true">
{% for sol in [
{'text': '~2 min pour 1h d\'audio', 'x': 20, 'y': 12},
{'text': 'Dès 173&nbsp;$/mois — illimité', 'x': 64, 'y': 10},
{'text': '90&nbsp;%+ d\'économies', 'x': 74, 'y': 38},
{'text': '100&nbsp;% Confidentiel — Hébergé au Qc', 'x': 18, 'y': 40}
] %}
<div class="absolute flex items-center gap-1.5 px-2 py-1 rounded backdrop-blur-sm"
style="left: {{ sol.x }}%; top: {{ sol.y }}%; transform: translate(-50%, -50%); background: rgba(107,159,255,0.10); border: 1px solid rgba(107,159,255,0.20);">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-2.5 h-2.5 text-brand-b1 shrink-0" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>
<span class="text-[8px] md:text-[10px] text-white/80 whitespace-nowrap">{{ sol.text | safe }}</span>
</div>
{% endfor %}
</div>
{# Séparateur central + hint "→" #}
<div class="absolute left-1/2 top-0 bottom-0 w-px z-10 hidden md:block pointer-events-none"
:style="`opacity: ${Math.max(0, 1 - p * 2.8)}; background: linear-gradient(to bottom, transparent 10%, rgba(255,255,255,0.06) 30%, rgba(255,255,255,0.06) 70%, transparent 90%);`"
aria-hidden="true">
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 whitespace-nowrap"
:style="`opacity: ${Math.max(0, 1 - p * 3.2)}`">
<span class="wave-hint inline-block text-[9px] text-white/40 tracking-wider uppercase px-3 py-1 rounded-full border border-white/[0.08] bg-brand-navy/80 backdrop-blur-sm">
Survolez vers la droite →
</span>
</div>
</div>
{# Toggle mobile #}
<button type="button"
x-show="isMobile"
@click.stop="toggle()"
class="absolute bottom-3 left-1/2 -translate-x-1/2 z-30 inline-flex items-center gap-2 px-5 py-2.5 rounded text-xs font-bold transition-all border focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2"
:class="activated ? 'border-brand-b1/30 text-brand-b1' : 'border-white/15 text-white/70'"
:style="activated ? 'background: linear-gradient(135deg, rgba(107,159,255,0.2), rgba(45,127,249,0.1));' : 'background: linear-gradient(135deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));'"
x-cloak>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3.5 h-3.5"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/></svg>
<span x-text="activated ? 'Voir sans DictIA' : 'Activer DictIA'"></span>
</button>
</div>
</div>
</section>
{# ===== PIPELINE — Comment ça marche : 4 étapes ===== #} {# ===== PIPELINE — Comment ça marche : 4 étapes ===== #}
{# Source canonique : InnovA-AI/Website-Sanity/components/sections/dictai-pipeline.tsx {# Source canonique : InnovA-AI/Website-Sanity/components/sections/dictai-pipeline.tsx
Animation traduite : Framer Motion auto-advance + sweep ring → Alpine.js setInterval + CSS keyframes #} Animation traduite : Framer Motion auto-advance + sweep ring → Alpine.js setInterval + CSS keyframes #}
@@ -1108,6 +1682,234 @@
</div> </div>
</section> </section>
{# ===== 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 #}
<style>
/* Cadre — folder qui glisse de QC vers US */
.cadre-folder { transition: left 1.4s cubic-bezier(0.4,0,0.2,1), color 0.4s, filter 0.4s; }
/* Cadre — pulse halo autour du folder en alerte */
@keyframes cadre-folder-halo {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.8); opacity: 0; }
}
.cadre-halo { animation: cadre-folder-halo 1.2s ease-out infinite; }
/* Cadre — REGS row flash amber pendant le sweep */
.cadre-reg { transition: border-color 180ms, background-color 180ms; }
.cadre-reg.is-flash { border-color: rgba(245,158,11,0.50) !important; background-color: rgba(245,158,11,0.06) !important; }
.cadre-reg.is-flash .cadre-reg-label { color: #d97706 !important; }
/* Cadre — alerte clignotante footer */
@keyframes cadre-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.cadre-blink { animation: cadre-blink 0.85s ease-in-out infinite; }
/* Cadre — caret HUD blink */
@keyframes cadre-caret {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.cadre-caret { animation: cadre-caret 0.7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.cadre-folder, .cadre-halo, .cadre-reg, .cadre-blink, .cadre-caret {
animation: none !important; transition: none !important;
}
}
</style>
<section
class="bg-white py-20 border-b border-brand-border relative overflow-hidden"
aria-labelledby="cadre-title"
x-data="{
step: 0,
hudLines: [],
flashIdx: -1,
timers: [],
observer: null,
HUD: ['&gt; Interception IA détectée.', '&gt; Données utilisées pour entraînement.', '&gt; Statut : Violation Légale.'],
runCycle() {
this.step = 0; this.hudLines = []; this.flashIdx = -1;
this.timers.push(setTimeout(() => this.step = 1, 900));
this.timers.push(setTimeout(() => this.step = 2, 2300));
this.timers.push(setTimeout(() => {
this.step = 3;
this.HUD.forEach((line, i) => {
this.timers.push(setTimeout(() => this.hudLines.push(line), i * 650));
});
}, 2800));
for (let i = 0; i < 6; i++) {
this.timers.push(setTimeout(() => this.flashIdx = i, 4700 + i * 200));
}
this.timers.push(setTimeout(() => this.flashIdx = -1, 4700 + 6 * 200 + 500));
this.timers.push(setTimeout(() => this.runCycle(), 8500));
},
init() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting && this.step === 0 && this.timers.length === 0) {
this.timers.push(setTimeout(() => this.runCycle(), 300));
this.observer.disconnect();
}
});
}, { rootMargin: '-100px' });
this.observer.observe($el);
}
}"
>
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"
style="background-image: radial-gradient(circle, rgba(11,15,26,0.04) 1px, transparent 1px); background-size: 28px 28px;"></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 text-amber-600 mb-4">⚠ CADRE RÉGLEMENTAIRE QUÉBEC</p>
<h2 id="cadre-title" class="text-[clamp(2rem,3vw,2.75rem)] font-black mb-4 text-brand-navy">
Ce que vos outils actuels enfreignent <span class="grad-text">en secret</span>.
</h2>
<p class="text-base text-brand-navy/70">
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.
</p>
</div>
{# Carte principale — header + body 2 colonnes (animation à gauche, REGS à droite) #}
<div class="max-w-5xl mx-auto rounded border border-brand-border bg-brand-bg overflow-hidden">
{# Header — Moniteur d'Interception #}
<div class="flex items-center gap-2.5 px-5 pt-4 pb-3 border-b border-brand-border bg-white">
<span class="w-1.5 h-1.5 rounded-full shrink-0 transition-all duration-300"
:class="step >= 2 ? 'bg-amber-500 shadow-[0_0_6px_#F59E0B]' : 'bg-brand-navy/30'"
aria-hidden="true"></span>
<span class="font-medium text-[10px] uppercase tracking-[0.22em] text-brand-navy/55">
Moniteur d'Interception
</span>
<span x-show="step >= 2"
class="ml-auto font-bold text-[9px] uppercase tracking-[0.18em] text-amber-600 cadre-blink"
x-cloak>
⚠ Alerte Active
</span>
</div>
{# Body — 2 colonnes #}
<div class="flex flex-col md:flex-row">
{# LEFT — animation track (~42%) #}
<div class="relative md:w-[42%] px-5 py-5 overflow-hidden bg-brand-bg" style="min-height: 200px;">
{# Grille bg #}
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"
style="background-image: linear-gradient(rgba(11,15,26,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(11,15,26,0.04) 1px, transparent 1px); background-size: 20px 20px;"></div>
{# Labels QC / US #}
<div class="relative flex items-center justify-between mb-3">
<span class="font-bold text-[9px] uppercase tracking-[0.16em] transition-colors duration-300"
:class="step >= 2 ? 'text-brand-navy/30' : 'text-brand-b3'">
QC
</span>
<span class="font-bold text-[9px] uppercase tracking-[0.16em] text-brand-navy/30">
US
</span>
</div>
{# Track #}
<div class="relative h-10">
<div class="absolute top-0 bottom-0 transition-colors duration-300"
:style="`left: 50%; width: 0; border-left: 1.5px dashed ${step >= 2 ? 'rgba(245,158,11,0.6)' : 'rgba(11,15,26,0.18)'};`"
aria-hidden="true"></div>
<div class="cadre-folder absolute top-1/2 -translate-y-1/2 -translate-x-1/2"
:style="`left: ${step === 0 ? '6%' : '54%'}`">
<span x-show="step >= 2" class="cadre-halo absolute inset-[-8px] rounded-full bg-amber-500/15" x-cloak></span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"
class="relative w-5 h-5 transition-colors duration-300"
:class="step >= 2 ? 'text-amber-500' : 'text-brand-b1'"
:style="`filter: drop-shadow(0 0 ${step >= 2 ? '6px rgba(245,158,11,0.7)' : '4px rgba(0,98,255,0.5)'})`"
aria-hidden="true">
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
</svg>
</div>
</div>
{# HUD panel #}
<div x-show="step >= 3"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
class="mt-3 rounded px-3 py-2.5 font-mono"
style="background: rgba(11,15,26,0.92); border: 1px solid rgba(245,158,11,0.30); font-size: 10px; color: #F59E0B; line-height: 1.65;"
x-cloak>
<template x-for="(line, i) in hudLines" :key="i">
<div x-html="line"></div>
</template>
<span x-show="hudLines.length < HUD.length" class="cadre-caret"></span>
</div>
</div>
{# RIGHT — REGS list (~58%) #}
<div class="md:w-[58%] px-5 py-5 border-t md:border-t-0 md:border-l border-brand-border bg-white">
<p class="font-semibold text-[10px] uppercase tracking-[0.18em] mb-3 text-brand-navy/45">
Ce que vos outils actuels enfreignent en secret&nbsp;:
</p>
<ul class="flex flex-col gap-1.5" role="list">
{% 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.&nbsp;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}
] %}
<li role="listitem">
<a href="{{ reg.href }}" target="_blank" rel="noopener noreferrer"
class="cadre-reg group flex items-start gap-2 rounded px-2.5 py-1.5 no-underline border focus-visible:outline-2 focus-visible:outline-amber-500 focus-visible:outline-offset-2"
:class="flashIdx === {{ loop.index0 }} ? 'is-flash' : ''"
style="border-color: {{ 'rgba(239,68,68,0.20)' if reg.risk else 'rgba(0,98,255,0.15)' }};">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="w-3.5 h-3.5 mt-0.5 shrink-0"
style="color: {{ 'rgba(239,68,68,0.65)' if reg.risk else 'rgba(0,98,255,0.55)' }};"
aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<div class="min-w-0 flex-1">
<div class="flex items-start gap-1.5 flex-wrap">
<span class="cadre-reg-label font-semibold text-[11px] leading-tight shrink-0 transition-colors"
style="color: {{ '#dc2626' if reg.risk else '#1d4ed8' }};">{{ reg.label }}</span>
<span class="text-[10px] leading-tight text-brand-navy/55">{{ reg.detail | safe }}</span>
</div>
</div>
<span class="text-[9px] shrink-0 mt-0.5 opacity-0 group-hover:opacity-60 transition-opacity text-brand-navy/70">Lire ↗</span>
</a>
</li>
{% endfor %}
</ul>
<p class="mt-3 text-right text-[9px] text-brand-navy/30">
Les textes officiels font foi. Les informations ci-dessus sont fournies à titre indicatif.
</p>
</div>
</div>
{# Footer — verdict #}
<div class="px-5 py-3 flex items-center gap-2.5 border-t border-brand-border bg-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"
class="w-4 h-4 transition-colors"
:class="step >= 2 ? 'text-amber-500 cadre-blink' : 'text-brand-navy/35'"
aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span class="font-bold text-[10px] uppercase tracking-[0.20em] transition-colors"
:class="step >= 2 ? 'text-amber-600 cadre-blink' : 'text-brand-navy/35'"
style="text-shadow: 0 0 6px rgba(245,158,11,0.20);"
aria-live="polite">
<span x-text="step >= 2 ? 'NON CONFORME — Loi 25 · Cloud Act' : 'Surveillance active...'"></span>
</span>
</div>
</div>
<p class="text-center text-xs text-brand-navy/55 mt-6 max-w-2xl mx-auto">
DictIA est conçu pour les secteurs réglementés du Québec — Loi&nbsp;25, Cloud Act, Barreau, ChAD, AMF. Hébergement OVH&nbsp;Beauharnois,
code source AGPL&nbsp;v3 vérifiable, audit trail intégré.
</p>
</div>
</section>
{# ===== TÉMOIGNAGES (placeholder pré-lancement) ===== #} {# ===== TÉMOIGNAGES (placeholder pré-lancement) ===== #}
<section class="bg-brand-bg py-20" aria-labelledby="testimonials-title"> <section class="bg-brand-bg py-20" aria-labelledby="testimonials-title">
<div class="max-w-[1200px] mx-auto px-6"> <div class="max-w-[1200px] mx-auto px-6">

View File

@@ -754,6 +754,125 @@ def test_cta_final_uses_safe_pre_launch_wording():
assert phrase not in body, f"Forbidden pre-launch CTA: {phrase}" 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&nbsp;% 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&nbsp;$/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&nbsp;924&nbsp;$' in body or '3 924 $' in body
# Wave solution card pricing
assert '173&nbsp;$/mois' in body or 'Dès 173' in body
# Cadre — Loi 25 fine
assert '25 M$' in body or '25&nbsp;M$' in body
def test_routes_passes_testimonials_and_faq_to_template(): def test_routes_passes_testimonials_and_faq_to_template():
"""marketing.routes.landing() must pass testimonials and faq to render_template.""" """marketing.routes.landing() must pass testimonials and faq to render_template."""
# Import the module to verify the data lists exist # Import the module to verify the data lists exist