feat(marketing): round 4 — Cadre + Cycle cinématiques (radar, data packet flight, stamp impact, savings counter)

Round 4 transforme les 2 sections "Cadre réglementaire" en expériences cinématiques :

CADRE (Moniteur d'Interception)
- Radar sweep circulaire vert continu en background HUD (4s loop, SVG + @keyframes)
- 6 paquets data "voice.wav" en flight QC→US via offset-path bezier (stagger 420ms, glow rouge)
- Console typewriter char-by-char 3 lignes (28ms/char + caret blink, 3e ligne rouge glow)
- 6 REGS reveal cascadé via revealRegsCascade (stagger 120ms) + hover red glow + border-left
- Verdict NON CONFORME : pulse glow rouge + scan-line traversante 3s
- Decorative grid 40×40 console-style + grid existant 20×20
- Eyebrow ⚠ remplacé par SVG warning-triangle inline

CYCLE (Trois options)
- Phase reveal 1→4 séquentiel (déjà existant) avec animations renforcées
- Col 1 horloge accélérée 1 tour/3s (au lieu de 8s)
- Col 1 prix counter Alpine 0→315 (easeOutCubic 1.4s) via priceHumain + countTo
- Col 2 stamp NON CONFORME impact (rotate -22→-3deg + scale 2.4→1, cubic-bezier 1.6 ease)
- Col 2 flash rouge background à l'impact (cycle-col-flash) + 10 particules de fuite (au lieu de 6)
- Col 3 checkmark draw via stroke-dashoffset 24→0
- Col 3 glow border vert pulsant (cycle-conforme-glow, double-couche emerald + cyan)
- Col 3 badge "Loi 25 conforme" top-right avec pulse subtil (cycle-conforme-badge)
- Connecting lines avec dash flow continu (cycle-line-flow @keyframes)
- Live red dot "Réunion en cours" avec pulse box-shadow
- Section "Économies annuelles · 25 utilisateurs" : 3 cards avec counter Alpine
  (sav1=3924, sav2=6924, sav3=2004) + hover lift + emerald shadow
- Eyebrow ⚠ remplacé par SVG warning-triangle

Accessibilité & performance
- prefers-reduced-motion désactive TOUT (radar, packets, typewriter, stamp, glow, counter)
- Mobile (<768px) cache radar + packets + leak particles (CPU-intensive)
- Counter helper countTo respecte reduced-motion via matchMedia
- Tous les SVG ont aria-hidden, scènes ont role=img/listitem appropriés
- HUD console role=log + aria-live=polite
- OQLF NBSP préservé (315 $/réunion, Loi 25, 100 % Québec, 25 utilisateurs, 3 924 $)

Tests : 4 tests round 4 ajoutés (cadre cinematic, cycle cinematic, no-emoji warning,
reduced-motion guards). 65/68 landing tests passent (3 failures pré-existantes
unrelated : nav /blog, footer /blog, trust-bar phrasing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-28 13:11:02 -04:00
parent 529bd2263b
commit 680df39089
3 changed files with 596 additions and 120 deletions

View File

@@ -49,7 +49,8 @@
--color-green-700: oklch(52.7% 0.154 150.069);
--color-green-800: oklch(44.8% 0.119 151.328);
--color-green-900: oklch(39.3% 0.095 152.535);
--color-emerald-300: oklch(84.5% 0.143 164.978);
--color-emerald-50: oklch(97.9% 0.021 166.113);
--color-emerald-100: oklch(95% 0.052 163.051);
--color-emerald-500: oklch(69.6% 0.17 162.48);
--color-emerald-600: oklch(59.6% 0.145 163.225);
--color-emerald-700: oklch(50.8% 0.118 165.612);
@@ -128,6 +129,7 @@
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-black: 900;
--tracking-tighter: -0.05em;
--tracking-tight: -0.025em;
--tracking-wide: 0.025em;
--tracking-wider: 0.05em;
@@ -147,7 +149,6 @@
--blur-sm: 8px;
--blur-md: 12px;
--blur-xl: 24px;
--blur-3xl: 64px;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -391,6 +392,9 @@
.-top-3 {
top: calc(var(--spacing) * -3);
}
.-top-8 {
top: calc(var(--spacing) * -8);
}
.-top-12 {
top: calc(var(--spacing) * -12);
}
@@ -439,6 +443,9 @@
.-right-1\.5 {
right: calc(var(--spacing) * -1.5);
}
.-right-8 {
right: calc(var(--spacing) * -8);
}
.-right-12 {
right: calc(var(--spacing) * -12);
}
@@ -526,6 +533,9 @@
.left-\[3\%\] {
left: 3%;
}
.left-\[6\%\] {
left: 6%;
}
.z-10 {
z-index: 10;
}
@@ -829,6 +839,9 @@
.h-32 {
height: calc(var(--spacing) * 32);
}
.h-40 {
height: calc(var(--spacing) * 40);
}
.h-48 {
height: calc(var(--spacing) * 48);
}
@@ -958,6 +971,9 @@
.w-1\/2 {
width: calc(1 / 2 * 100%);
}
.w-1\/3 {
width: calc(1 / 3 * 100%);
}
.w-2 {
width: calc(var(--spacing) * 2);
}
@@ -1445,10 +1461,6 @@
-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;
@@ -1490,9 +1502,6 @@
.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;
@@ -1590,6 +1599,10 @@
border-style: var(--tw-border-style);
border-width: 2px;
}
.border-\[3px\] {
border-style: var(--tw-border-style);
border-width: 3px;
}
.border-y {
border-block-style: var(--tw-border-style);
border-block-width: 1px;
@@ -1719,6 +1732,21 @@
.border-brand-border {
border-color: #e6ebf2;
}
.border-emerald-100 {
border-color: var(--color-emerald-100);
}
.border-emerald-500\/40 {
border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 40%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-emerald-500) 40%, transparent);
}
}
.border-emerald-500\/45 {
border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 45%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-emerald-500) 45%, transparent);
}
}
.border-gray-300 {
border-color: var(--color-gray-300);
}
@@ -1956,12 +1984,6 @@
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)) {
@@ -2064,6 +2086,21 @@
.bg-brand-navy2 {
background-color: #0b1525;
}
.bg-emerald-50 {
background-color: var(--color-emerald-50);
}
.bg-emerald-500\/12 {
background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 12%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-emerald-500) 12%, transparent);
}
}
.bg-emerald-500\/15 {
background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-emerald-500) 15%, transparent);
}
}
.bg-emerald-600 {
background-color: var(--color-emerald-600);
}
@@ -2166,6 +2203,12 @@
background-color: color-mix(in oklab, var(--color-red-50) 30%, transparent);
}
}
.bg-red-50\/40 {
background-color: color-mix(in srgb, oklch(97.1% 0.013 17.38) 40%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-50) 40%, transparent);
}
}
.bg-red-100 {
background-color: var(--color-red-100);
}
@@ -2178,6 +2221,9 @@
.bg-red-400 {
background-color: var(--color-red-400);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
.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)) {
@@ -2190,6 +2236,18 @@
background-color: color-mix(in oklab, var(--color-red-500) 20%, transparent);
}
}
.bg-red-500\/40 {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 40%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-500) 40%, transparent);
}
}
.bg-red-500\/85 {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 85%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-500) 85%, transparent);
}
}
.bg-red-600 {
background-color: var(--color-red-600);
}
@@ -2229,6 +2287,12 @@
background-color: color-mix(in oklab, var(--color-white) 30%, transparent);
}
}
.bg-white\/80 {
background-color: color-mix(in srgb, #fff 80%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 80%, transparent);
}
}
.bg-white\/95 {
background-color: color-mix(in srgb, #fff 95%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -2812,6 +2876,10 @@
--tw-tracking: var(--tracking-tight);
letter-spacing: var(--tracking-tight);
}
.tracking-tighter {
--tw-tracking: var(--tracking-tighter);
letter-spacing: var(--tracking-tighter);
}
.tracking-wide {
--tw-tracking: var(--tracking-wide);
letter-spacing: var(--tracking-wide);
@@ -2995,6 +3063,9 @@
.text-brand-navy\/80 {
color: color-mix(in oklab, #060d1a 80%, transparent);
}
.text-brand-navy\/85 {
color: color-mix(in oklab, #060d1a 85%, transparent);
}
.text-brand-navy\/90 {
color: color-mix(in oklab, #060d1a 90%, transparent);
}
@@ -3007,6 +3078,15 @@
.text-emerald-600 {
color: var(--color-emerald-600);
}
.text-emerald-600\/70 {
color: color-mix(in srgb, oklch(59.6% 0.145 163.225) 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-emerald-600) 70%, transparent);
}
}
.text-emerald-700 {
color: var(--color-emerald-700);
}
.text-gray-200 {
color: var(--color-gray-200);
}
@@ -3097,6 +3177,12 @@
color: color-mix(in oklab, var(--color-red-500) 80%, transparent);
}
}
.text-red-500\/85 {
color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 85%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-red-500) 85%, transparent);
}
}
.text-red-600 {
color: var(--color-red-600);
}
@@ -3288,10 +3374,18 @@
--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_6px_rgba\(239\,68\,68\,0\.6\)\] {
--tw-shadow: 0 0 6px var(--tw-shadow-color, rgba(239,68,68,0.6));
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);
}
.shadow-\[0_8px_30px_-6px_rgba\(239\,68\,68\,0\.55\)\] {
--tw-shadow: 0 8px 30px -6px var(--tw-shadow-color, rgba(239,68,68,0.55));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-cta {
--tw-shadow: 0 4px 20px var(--tw-shadow-color, rgba(0, 98, 255, 0.28));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);

View File

@@ -411,10 +411,10 @@
{# ===== 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) #}
Round 4 cinématique : phase reveal séquentiel + horloge accélérée + prix counter + stamp impact NON CONFORME
+ checkmark draw + glow vert + connecting line dash flow + section "Économies annuelles" avec 3 counters animés #}
<style>
/* Cycle pulse rings — appliqués au nœud source "Réunion en cours" */
/* Cycle pulse rings — 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; }
@@ -423,12 +423,19 @@
.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); }
/* Live red dot — pulse plus marqué pour signal "réunion en cours" */
@keyframes cycle-live-dot {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(239,68,68,0.55); }
50% { opacity: 0.6; box-shadow: 0 0 0 5px rgba(239,68,68,0); }
}
.cycle-card-dictia.is-visible { animation: cycle-glow 3.8s ease-in-out infinite; }
.cycle-live-dot { animation: cycle-live-dot 1.4s ease-in-out infinite; }
/* Cycle col 3 (DictIA) — bordure lumineuse pulsante VERT (round 4 : conformité) */
@keyframes cycle-conforme-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.18), 0 0 0 0 rgba(0,189,216,0.10); }
50% { box-shadow: 0 0 32px 2px rgba(34,197,94,0.32), 0 0 60px 0 rgba(0,189,216,0.14); }
}
.cycle-card-dictia.is-visible { animation: cycle-conforme-glow 3.4s ease-in-out infinite; }
/* Cycle col 3 — anneaux concentriques autour du Lock */
@keyframes cycle-ring-out {
@@ -438,48 +445,123 @@
.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 SVG lignes de connexion — drawn via stroke-dashoffset, dash flow continu (round 4) */
.cycle-line { stroke-dasharray: 1.2 0.9; stroke-dashoffset: 50; transition: stroke-dashoffset 600ms ease-out; }
.cycle-line.is-visible { stroke-dashoffset: 0; }
@keyframes cycle-dash-flow { to { stroke-dashoffset: -42; } }
.cycle-line.is-visible.cycle-line-flow { animation: cycle-dash-flow 12s linear infinite; }
/* Cycle phase reveal — colonnes 1+2 (phase 2), overlay (phase 3), col 3 (phase 4) */
/* Cycle phase reveal */
.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) */
/* Cycle horloge rotation (col 1) — accélérée 1 tour / 3s pour rendre "lent" PALPABLE */
@keyframes cycle-clock-spin { to { transform: rotate(360deg); } }
.cycle-clock { animation: cycle-clock-spin 8s linear infinite; transform-origin: center; display: inline-block; }
.cycle-clock { animation: cycle-clock-spin 3s linear infinite; transform-origin: center; display: inline-block; }
/* Cycle col 2 — fuite particules rouges */
/* Cycle col 2 — fuite particules rouges (continu, plus dense round 4) */
@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; }
20% { opacity: 0.85; }
100% { transform: translate(var(--lx,18px), var(--ly,-22px)) scale(0.35); opacity: 0; }
}
.cycle-leak-particle { animation: cycle-leak 1.8s ease-out infinite; }
/* Reduced motion */
/* Cycle col 2 — STAMP NON CONFORME impact (round 4 : tampon huissier qui claque) */
@keyframes cycle-stamp-impact {
0% { transform: translate(-50%,-50%) rotate(-22deg) scale(2.4); opacity: 0; filter: blur(2px); }
55% { transform: translate(-50%,-50%) rotate(-7deg) scale(0.92); opacity: 1; filter: blur(0); }
70% { transform: translate(-50%,-50%) rotate(-3deg) scale(1.06); }
100% { transform: translate(-50%,-50%) rotate(-3deg) scale(1); opacity: 1; }
}
.cycle-stamp.is-visible { animation: cycle-stamp-impact 720ms cubic-bezier(0.5,1.6,0.4,1) forwards; }
/* Cycle col 2 — flash background rouge à l'impact stamp */
@keyframes cycle-flash-red {
0% { background-color: rgba(239,68,68,0); }
35% { background-color: rgba(239,68,68,0.18); }
100% { background-color: rgba(239,68,68,0); }
}
.cycle-col-flash.is-visible { animation: cycle-flash-red 700ms ease-out 350ms forwards; }
/* Cycle col 3 — checkmark draw (stroke-dashoffset) */
.cycle-check-svg path { stroke-dasharray: 24; stroke-dashoffset: 24; transition: stroke-dashoffset 350ms ease-out 200ms; }
.cycle-check-svg.is-visible path { stroke-dashoffset: 0; }
/* Cycle col 3 — badge "Loi 25 conforme" pulse subtil */
@keyframes cycle-badge-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(34,197,94,0.4); }
50% { transform: scale(1.04); box-shadow: 0 0 0 6px rgba(34,197,94,0); }
}
.cycle-conforme-badge.is-visible { animation: cycle-badge-pulse 2.4s ease-in-out infinite 600ms; }
/* Cycle "Économies annuelles" cards — hover lift + glow */
.cycle-savings-card {
transition: transform 220ms ease-out, box-shadow 220ms ease-out, border-color 220ms ease-out;
}
.cycle-savings-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px -8px rgba(34,197,94,0.35), 0 4px 12px -4px rgba(11,15,26,0.08);
border-color: rgba(34,197,94,0.45);
}
/* Mobile — désactiver fuites particules + glow heavy */
@media (max-width: 768px) {
.cycle-leak-particle { display: none; }
.cycle-card-dictia.is-visible { animation: none; }
}
/* Reduced motion — TOUT figé */
@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-clock, .cycle-leak-particle, .cycle-live-dot, .cycle-conforme-badge,
.cycle-line.cycle-line-flow, .cycle-stamp, .cycle-col-flash { animation: none !important; }
.cycle-reveal, .cycle-reveal-up { opacity: 1 !important; transform: none !important; }
.cycle-line { stroke-dashoffset: 0 !important; }
.cycle-check-svg path { stroke-dashoffset: 0 !important; }
.cycle-savings-card { transition: none !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-data="{
phase: 0,
observer: null,
priceHumain: 0,
sav1: 0, sav2: 0, sav3: 0,
/* easeOutCubic counter helper */
countTo(prop, target, duration) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { this[prop] = target; return; }
const start = performance.now();
const tick = (now) => {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3);
this[prop] = Math.round(target * eased);
if (t < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
},
fmt(n) { return n.toLocaleString('fr-CA').replace(//g, ' '); }
}"
x-init="
observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting && phase === 0) {
setTimeout(() => phase = 1, 250);
setTimeout(() => phase = 2, 1100);
setTimeout(() => { phase = 1; }, 250);
setTimeout(() => { phase = 2; countTo('priceHumain', 315, 1400); }, 1100);
setTimeout(() => phase = 3, 2200);
setTimeout(() => phase = 4, 3000);
setTimeout(() => {
phase = 4;
/* Savings counters fire 600ms after col 3 reveal (round 4) */
setTimeout(() => {
countTo('sav1', 3924, 1500);
countTo('sav2', 6924, 1500);
countTo('sav3', 2004, 1500);
}, 700);
}, 3000);
observer.disconnect();
}
});
@@ -493,7 +575,10 @@
<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>
<p class="eyebrow text-amber-600 mb-4 inline-flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" class="w-3.5 h-3.5" 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>
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>
@@ -519,23 +604,25 @@
</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>
<span class="cycle-live-dot inline-block w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" aria-hidden="true"></span>
<span class="font-mono font-bold text-[8px] tracking-[0.22em] uppercase text-red-500/85">Live</span>
</div>
</div>
{# Lignes de connexion SVG — de la source vers les 3 colonnes #}
{# Lignes de connexion SVG — de la source vers les 3 colonnes (round 4 : dash flow continu) #}
<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' : ''"
<line class="cycle-line cycle-line-flow" :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' : ''"
stroke="rgba(148,163,184,0.55)" stroke-width="0.3" />
<line class="cycle-line cycle-line-flow" :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' : ''"
stroke="rgba(239,68,68,0.55)" stroke-width="0.3"
style="transition-delay: 80ms; animation-delay: 0.4s;" />
<line class="cycle-line cycle-line-flow" :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;" />
stroke="rgba(34,197,94,0.6)" stroke-width="0.3"
style="transition-delay: 160ms; animation-delay: 0.8s;" />
</svg>
</div>
@@ -576,8 +663,8 @@
</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>
<span class="font-black text-3xl leading-none text-brand-navy/65 tabular-nums" x-text="priceHumain">315</span>
<span class="text-xs text-brand-navy/45">$&nbsp;/&nbsp;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>
@@ -587,18 +674,20 @@
</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"
<div class="cycle-reveal-up cycle-col-flash 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"
{# Overlay légal NON CONFORME (phase 3) — round 4 : STAMP huissier qui claque #}
<div class="absolute inset-0 z-30 flex flex-col items-center justify-center pointer-events-none"
:class="phase >= 3 ? 'opacity-100' : 'opacity-0'"
style="transition: opacity 220ms ease-out; backdrop-filter: blur(6px); background: rgba(255,255,255,0.78);">
<div class="cycle-stamp absolute top-1/2 left-1/2 flex flex-col items-center gap-3 px-6 py-5 rounded bg-white border-[3px] border-red-500 shadow-[0_8px_30px_-6px_rgba(239,68,68,0.55)]"
: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">
style="transform: translate(-50%,-50%) scale(0); opacity: 0;">
<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 class="font-mono text-[10px] tracking-[0.20em] uppercase mt-1.5 text-red-500/70">Loi&nbsp;25 · Cloud Act américain</div>
</div>
</div>
</div>
@@ -612,10 +701,10 @@
<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) %}
{# Particules de fuite (round 4 : 10 particules, plus dense) #}
{% for i in range(10) %}
<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>
style="left: 50%; top: 50%; --lx: {{ (-32 + i*8) }}px; --ly: -{{ 12 + (i % 4) * 7 }}px; animation-delay: {{ i * 0.16 }}s; box-shadow: 0 0 4px rgba(239,68,68,0.55);"></span>
{% endfor %}
</div>
<div class="px-2 py-0.5 rounded bg-red-50 border border-red-100">
@@ -660,11 +749,18 @@
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>
{# Numéro 03 → checkmark vert (round 4) #}
<span class="relative w-5 h-5 rounded-full flex items-center justify-center shrink-0 bg-emerald-500/15 border border-emerald-500/45" aria-hidden="true">
<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="cycle-check-svg w-3 h-3 text-emerald-600"
:class="phase >= 4 ? 'is-visible' : ''"><path d="M5 13l4 4L19 7"/></svg>
</span>
<span class="text-[11px] uppercase tracking-[0.22em] font-semibold text-brand-b1/80">Solution</span>
{# Badge top-right : Loi 25 conforme (round 4) #}
<span class="cycle-conforme-badge ml-auto inline-flex items-center gap-1 rounded-full px-2 py-0.5 bg-emerald-500/12 border border-emerald-500/40"
:class="phase >= 4 ? 'is-visible' : ''" aria-label="Loi 25 conforme">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-2.5 h-2.5 text-emerald-600" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<span class="font-mono font-bold text-[8px] tracking-[0.16em] uppercase text-emerald-700">Loi&nbsp;25 conforme</span>
</span>
</div>
<div class="relative flex-1 flex flex-col items-center justify-center px-5 py-7 gap-5">
@@ -725,23 +821,49 @@
</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"
{# Section "Économies annuelles · 25 utilisateurs" — round 4 : 3 cards avec counter animation #}
<div class="cycle-reveal-up mt-12"
: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 class="flex items-center justify-center gap-2.5 mb-5">
<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 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>
<span class="font-mono font-bold text-[10px] uppercase tracking-[0.22em] text-brand-navy/65">
Économies annuelles · 25&nbsp;utilisateurs
</span>
<span class="flex-1 h-px bg-brand-border max-w-[60px]" aria-hidden="true"></span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3" role="list" aria-label="Trois comparaisons d'économies annuelles">
{% for sav in [
{'icon': 'otter', 'val_prop': 'sav1', 'val_static': '3&nbsp;924', 'label': 'vs Otter.ai', 'sub': 'IA cloud US'},
{'icon': 'teams', 'val_prop': 'sav2', 'val_static': '6&nbsp;924', 'label': 'vs MS Teams', 'sub': 'Copilot premium'},
{'icon': 'scribe','val_prop': 'sav3', 'val_static': '2&nbsp;004', 'label': 'vs Sténographe','sub': 'Service humain'}
] %}
<div class="cycle-savings-card flex items-center gap-3 px-4 py-4 rounded border border-brand-border bg-white" role="listitem">
<span class="shrink-0 w-10 h-10 rounded flex items-center justify-center bg-emerald-50 border border-emerald-100 text-emerald-600" aria-hidden="true">
{% if sav.icon == 'otter' %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5"><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"/><circle cx="12" cy="12" r="3"/></svg>
{% elif sav.icon == 'teams' %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5"><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"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5"><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="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
{% endif %}
</span>
<div class="flex-1 min-w-0">
<div class="flex items-baseline">
{# OQLF NBSP entre nombre et $ — préservé en placeholder statique pour SEO/no-JS, JS écrase via x-html #}
<span class="font-black text-2xl leading-none text-emerald-600 tabular-nums"
x-html="fmt({{ sav.val_prop }}) + '&nbsp;<span class=&quot;text-sm text-emerald-600/70 font-bold&quot;>$</span>'">{{ sav.val_static | safe }}&nbsp;$</span>
</div>
<div class="text-[11px] font-semibold text-brand-navy/85 mt-1">{{ sav.label }}</div>
<div class="text-[10px] text-brand-navy/45">{{ sav.sub }}</div>
</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>
</div>
</section>
{# ===== SOLUTION (S de PAS) ===== #}
@@ -1768,11 +1890,13 @@
{# ===== 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 #}
Round 4 cinématique : radar sweep continu, 6 paquets data en flight QC→US (offset-path bezier),
typewriter 3 lignes char-par-char, REGS reveal cascadé + glow rouge hover, verdict pulse + scan-line,
grid pattern bg console. #}
<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 — RADAR SWEEP circulaire continu (round 4) */
@keyframes cadre-radar-sweep { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.cadre-radar { animation: cadre-radar-sweep 4s linear infinite; transform-origin: center; }
/* Cadre — pulse halo autour du folder en alerte */
@keyframes cadre-folder-halo {
@@ -1781,10 +1905,38 @@
}
.cadre-halo { animation: cadre-folder-halo 1.2s ease-out infinite; }
/* Cadre — DATA PACKET flight QC→US via offset-path bezier (round 4) */
@keyframes cadre-packet-fly {
0% { offset-distance: 0%; opacity: 0; transform: scale(0.7); }
8% { opacity: 1; transform: scale(1); }
85% { opacity: 1; transform: scale(1); }
100% { offset-distance: 100%; opacity: 0; transform: scale(0.4); }
}
.cadre-packet {
offset-path: path('M 0 24 Q 100 -24, 200 24');
offset-rotate: auto;
animation: cadre-packet-fly 2.6s ease-in-out infinite;
will-change: offset-distance, opacity, transform;
}
/* Cadre — packet trail (traînée fade rouge à l'arrivée) */
@keyframes cadre-packet-arrive {
0%, 70% { opacity: 0; transform: translate(-50%,-50%) scale(0.5); }
80% { opacity: 0.85; transform: translate(-50%,-50%) scale(1.4); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(2.2); }
}
.cadre-packet-burst { animation: cadre-packet-arrive 2.6s ease-in-out infinite; }
/* Cadre — REGS row flash amber pendant le sweep */
.cadre-reg { transition: border-color 180ms, background-color 180ms; }
.cadre-reg { transition: border-color 220ms, background-color 220ms, box-shadow 220ms, transform 220ms; }
.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; }
/* Hover red glow (round 4) */
.cadre-reg:hover { box-shadow: 0 0 18px rgba(239,68,68,0.22); border-left-width: 3px !important; border-left-color: rgba(239,68,68,0.65) !important; transform: translateX(2px); }
/* Cadre — REGS reveal cascadé (round 4 : IntersectionObserver via Alpine `revealRegs`) */
.cadre-reg-item { opacity: 0; transform: translateX(12px); transition: opacity 420ms ease-out, transform 420ms ease-out; }
.cadre-reg-item.is-visible { opacity: 1; transform: translateX(0); }
/* Cadre — alerte clignotante footer */
@keyframes cadre-blink {
@@ -1800,10 +1952,37 @@
}
.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;
/* Cadre — VERDICT NON CONFORME pulse glow (round 4) */
@keyframes cadre-verdict-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.50), inset 0 0 0 1px rgba(239,68,68,0.45); }
50% { box-shadow: 0 0 18px 3px rgba(239,68,68,0.30), inset 0 0 0 1px rgba(239,68,68,0.85); }
}
.cadre-verdict-active { animation: cadre-verdict-pulse 2s ease-in-out infinite; }
/* Cadre — VERDICT scan-line traversante (round 4) */
@keyframes cadre-scan-line {
0% { transform: translateX(-110%); opacity: 0; }
20% { opacity: 0.85; }
80% { opacity: 0.85; }
100% { transform: translateX(110%); opacity: 0; }
}
.cadre-scan-line { animation: cadre-scan-line 3s linear infinite; }
/* Cadre — typewriter caret (3e ligne en rouge avec glow) */
.cadre-hud-line-3 { color: #ff5252 !important; text-shadow: 0 0 8px rgba(239,68,68,0.55); }
/* Mobile — désactiver radar + packets (CPU-intensive) */
@media (max-width: 768px) {
.cadre-radar, .cadre-packet, .cadre-packet-burst { display: none; }
}
/* Reduced motion — TOUT figé */
@media (prefers-reduced-motion: reduce) {
.cadre-radar, .cadre-halo, .cadre-blink, .cadre-caret, .cadre-packet,
.cadre-packet-burst, .cadre-verdict-active, .cadre-scan-line {
animation: none !important;
}
.cadre-reg, .cadre-reg-item { transition: none !important; opacity: 1 !important; transform: none !important; }
}
</style>
<section
@@ -1811,32 +1990,62 @@
aria-labelledby="cadre-title"
x-data="{
step: 0,
hudLines: [],
/* hudTyped[i] = sous-chaîne progressive de HUD[i] (typewriter char-by-char) */
hudTyped: ['','',''],
hudCursorLine: -1,
flashIdx: -1,
revealedRegs: [],
timers: [],
observer: null,
HUD: ['&gt; Interception IA détectée.', '&gt; Données utilisées pour entraînement.', '&gt; Statut : Violation Légale.'],
typeLine(idx, cb) {
const full = this.HUD[idx];
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced) { this.hudTyped[idx] = full; if (cb) cb(); return; }
this.hudCursorLine = idx;
let i = 0;
const tick = () => {
if (i > full.length) { if (cb) cb(); return; }
this.hudTyped[idx] = full.slice(0, i);
i++;
this.timers.push(setTimeout(tick, 28));
};
tick();
},
runCycle() {
this.step = 0; this.hudLines = []; this.flashIdx = -1;
this.step = 0; this.hudTyped = ['','','']; this.hudCursorLine = -1; 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));
/* Typewriter chain : ligne 0 → 1 → 2 puis reset cursor */
this.typeLine(0, () => {
this.timers.push(setTimeout(() => this.typeLine(1, () => {
this.timers.push(setTimeout(() => this.typeLine(2, () => {
this.hudCursorLine = -1;
}), 280));
}), 280));
});
}, 2800));
for (let i = 0; i < 6; i++) {
this.timers.push(setTimeout(() => this.flashIdx = i, 4700 + i * 200));
this.timers.push(setTimeout(() => this.flashIdx = i, 5400 + i * 200));
}
this.timers.push(setTimeout(() => this.flashIdx = -1, 4700 + 6 * 200 + 500));
this.timers.push(setTimeout(() => this.runCycle(), 8500));
this.timers.push(setTimeout(() => this.flashIdx = -1, 5400 + 6 * 200 + 500));
this.timers.push(setTimeout(() => this.runCycle(), 9500));
},
revealRegsCascade() {
[0,1,2,3,4,5].forEach((i) => {
this.timers.push(setTimeout(() => {
if (!this.revealedRegs.includes(i)) this.revealedRegs.push(i);
}, i * 120));
});
},
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.timers.push(setTimeout(() => this.revealRegsCascade(), 500));
this.observer.disconnect();
}
});
@@ -1845,12 +2054,16 @@
}
}"
>
{# Round 4 : decorative grid pattern bg console-style 40×40 #}
<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>
style="background-image: linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px), radial-gradient(circle, rgba(11,15,26,0.045) 1px, transparent 1px); background-size: 40px 40px, 40px 40px, 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>
<p class="eyebrow text-amber-600 mb-4 inline-flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" class="w-3.5 h-3.5" 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>
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>
@@ -1872,20 +2085,45 @@
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"
class="ml-auto inline-flex items-center gap-1 font-bold text-[9px] uppercase tracking-[0.18em] text-amber-600 cadre-blink"
x-cloak>
⚠ Alerte Active
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" class="w-2.5 h-2.5" 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>
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 #}
{# LEFT — animation track (~42%) — round 4 : radar + data packets en flight #}
<div class="relative md:w-[42%] px-5 py-5 overflow-hidden bg-brand-bg" style="min-height: 240px;" role="img" aria-label="Visualisation : radar de surveillance et paquets de données voix transférés du Québec aux États-Unis">
{# Grille bg console #}
<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>
style="background-image: linear-gradient(rgba(11,15,26,0.045) 1px, transparent 1px), linear-gradient(90deg, rgba(11,15,26,0.045) 1px, transparent 1px); background-size: 20px 20px;"></div>
{# Round 4 : Radar sweep — cercle vert avec ligne rotative balayant 360° #}
<div class="absolute -top-8 -right-8 w-40 h-40 pointer-events-none opacity-50" aria-hidden="true">
<svg viewBox="0 0 100 100" class="w-full h-full">
<defs>
<linearGradient id="cadre-radar-grad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="rgba(34,197,94,0)"/>
<stop offset="80%" stop-color="rgba(34,197,94,0.55)"/>
<stop offset="100%" stop-color="rgba(34,197,94,0.85)"/>
</linearGradient>
</defs>
<circle cx="50" cy="50" r="48" fill="none" stroke="rgba(34,197,94,0.20)" stroke-width="0.5"/>
<circle cx="50" cy="50" r="32" fill="none" stroke="rgba(34,197,94,0.15)" stroke-width="0.4"/>
<circle cx="50" cy="50" r="16" fill="none" stroke="rgba(34,197,94,0.12)" stroke-width="0.4"/>
<line x1="2" y1="50" x2="98" y2="50" stroke="rgba(34,197,94,0.10)" stroke-width="0.3"/>
<line x1="50" y1="2" x2="50" y2="98" stroke="rgba(34,197,94,0.10)" stroke-width="0.3"/>
{# Sweep arm #}
<g class="cadre-radar" style="transform-origin: 50px 50px;">
<path d="M 50 50 L 98 50 A 48 48 0 0 0 84 16 Z" fill="url(#cadre-radar-grad)" opacity="0.45"/>
<line x1="50" y1="50" x2="98" y2="50" stroke="rgba(34,197,94,0.85)" stroke-width="0.6"/>
</g>
<circle cx="50" cy="50" r="1.6" fill="rgba(34,197,94,0.85)"/>
</svg>
</div>
{# Labels QC / US #}
<div class="relative flex items-center justify-between mb-3">
@@ -1898,14 +2136,15 @@
</span>
</div>
{# Track #}
<div class="relative h-10">
{# Track + folder + DATA PACKETS en flight (round 4) #}
<div class="relative h-12">
<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>
{# Folder source (QC) #}
<div class="absolute top-1/2 -translate-y-1/2 -translate-x-1/2"
style="left: 6%; transition: color 0.4s, filter 0.4s;">
<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'"
@@ -1914,20 +2153,55 @@
<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>
{# Folder destination (US) avec halo alerte #}
<div class="absolute top-1/2 -translate-y-1/2 -translate-x-1/2"
style="left: 94%;">
<span x-show="step >= 2" class="cadre-halo absolute inset-[-8px] rounded-full bg-red-500/20" x-cloak aria-hidden="true"></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-red-500' : 'text-brand-navy/40'"
:style="`filter: drop-shadow(0 0 ${step >= 2 ? '6px rgba(239,68,68,0.7)' : '0'})`"
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>
{# Burst rouge à l'arrivée des paquets #}
<span x-show="step >= 2" class="cadre-packet-burst absolute top-1/2 left-1/2 w-4 h-4 rounded-full bg-red-500/40 pointer-events-none" x-cloak aria-hidden="true"></span>
</div>
{# HUD panel #}
{# 6 PAQUETS DATA en flight QC→US (round 4 : offset-path bezier) #}
<div x-show="step >= 1" class="absolute inset-0 pointer-events-none" x-cloak aria-hidden="true">
{% for pi in range(6) %}
<span class="cadre-packet absolute top-0 left-[6%] inline-flex items-center gap-1 px-1 py-0.5 rounded bg-red-500/85 text-white font-mono text-[8px] tracking-tighter shadow-[0_0_6px_rgba(239,68,68,0.6)]"
style="animation-delay: {{ pi * 0.42 }}s;">
<span class="w-1 h-1 rounded-full bg-white/80"></span>
voice.wav
</span>
{% endfor %}
</div>
</div>
{# HUD panel — typewriter char-by-char (round 4) #}
<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;"
class="relative mt-3 rounded px-3 py-2.5 font-mono"
style="background: rgba(11,15,26,0.94); border: 1px solid rgba(245,158,11,0.30); font-size: 10px; color: #F59E0B; line-height: 1.65; min-height: 70px;"
role="log" aria-live="polite"
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 class="flex items-baseline">
<span x-html="hudTyped[0]"></span>
<span x-show="hudCursorLine === 0" class="cadre-caret ml-0.5"></span>
</div>
<div class="flex items-baseline" x-show="hudTyped[1].length > 0 || hudCursorLine === 1">
<span x-html="hudTyped[1]"></span>
<span x-show="hudCursorLine === 1" class="cadre-caret ml-0.5"></span>
</div>
<div class="flex items-baseline cadre-hud-line-3" x-show="hudTyped[2].length > 0 || hudCursorLine === 2">
<span x-html="hudTyped[2]"></span>
<span x-show="hudCursorLine === 2" class="cadre-caret ml-0.5"></span>
</div>
</div>
</div>
@@ -1945,7 +2219,9 @@
{'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">
<li role="listitem" class="cadre-reg-item"
:class="revealedRegs.includes({{ loop.index0 }}) ? 'is-visible' : ''"
style="transition-delay: {{ loop.index0 * 30 }}ms;">
<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' : ''"
@@ -1972,18 +2248,26 @@
</div>
</div>
{# Footer — verdict #}
<div class="px-5 py-3 flex items-center gap-2.5 border-t border-brand-border bg-white">
{# Footer — verdict (round 4 : pulse glow rouge + scan-line traversante) #}
<div class="relative px-5 py-3 border-t border-brand-border bg-white overflow-hidden">
<div class="relative flex items-center gap-2.5 px-3 py-2 rounded transition-all duration-300"
:class="step >= 2 ? 'cadre-verdict-active bg-red-50/40' : ''"
style="border-radius: 4px;">
<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'"
:class="step >= 2 ? 'text-red-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);"
:class="step >= 2 ? 'text-red-600' : 'text-brand-navy/35'"
style="text-shadow: 0 0 6px rgba(239,68,68,0.30);"
aria-live="polite">
<span x-text="step >= 2 ? 'NON CONFORME — Loi 25 · Cloud Act' : 'Surveillance active...'"></span>
</span>
{# Scan-line horizontale (round 4) #}
<span x-show="step >= 2" x-cloak class="cadre-scan-line absolute inset-y-0 left-0 w-1/3 pointer-events-none"
style="background: linear-gradient(90deg, transparent 0%, rgba(239,68,68,0.18) 50%, transparent 100%);"
aria-hidden="true"></span>
</div>
</div>
</div>

View File

@@ -848,6 +848,104 @@ def test_round2_cadre_reglementaire_section_present():
assert 'runCycle' in body
def test_round4_cadre_cinematic_features():
"""Round 4 — Cadre Moniteur d'Interception cinematic upgrades.
- Radar sweep circulaire continu en background HUD
- 6 paquets data 'voice.wav' en flight QC→US (offset-path bezier)
- Console typewriter char-by-char (3 lignes via hudTyped + caret blink)
- 6 REGS reveal cascadé (revealedRegs IntersectionObserver)
- Verdict 'NON CONFORME' pulse glow + scan-line traversante
- eyebrow ⚠ remplacé par SVG warning-triangle
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Radar sweep
assert 'cadre-radar-sweep' in body, "Round 4 radar sweep keyframe missing"
assert 'cadre-radar' in body
# 6 data packets en flight (voice.wav répété 6×)
assert body.count('voice.wav') >= 6, "Round 4 must have 6 'voice.wav' packets in flight"
assert 'cadre-packet' in body
assert 'offset-path' in body, "Round 4 packets must use offset-path for bezier flight"
# Typewriter
assert 'hudTyped' in body, "Round 4 typewriter state missing"
assert 'typeLine' in body, "Round 4 typewriter function missing"
# REGS cascade reveal
assert 'revealRegsCascade' in body or 'revealedRegs' in body
assert 'cadre-reg-item' in body
# Verdict pulse glow + scan line
assert 'cadre-verdict-active' in body
assert 'cadre-scan-line' in body
# ⚠ remplacé par SVG (le mot WARNING ne doit plus apparaître entouré de ⚠ dans l'eyebrow Cadre)
assert '⚠ CADRE RÉGLEMENTAIRE QUÉBEC' not in body, "⚠ literal must be replaced by SVG"
def test_round4_cycle_cinematic_features():
"""Round 4 — Cycle (Trois options) cinematic upgrades.
- Phase reveal séquentiel + price counter Col 1 (priceHumain 0→315)
- Stamp 'NON CONFORME' impact (cycle-stamp keyframes)
- Col 3 checkmark draw (cycle-check-svg stroke-dashoffset)
- Col 3 glow vert (cycle-conforme-glow)
- Badge 'Loi 25 conforme' pulse (cycle-conforme-badge)
- Section 'Économies annuelles · 25 utilisateurs' avec 3 counters (sav1/sav2/sav3)
- Connecting line dash flow (cycle-line-flow)
- eyebrow ⚠ remplacé par SVG
"""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Price counter
assert 'priceHumain' in body, "Round 4 price counter state missing"
assert 'countTo' in body, "Round 4 counter helper missing"
# Stamp impact + flash
assert 'cycle-stamp' in body
assert 'cycle-stamp-impact' in body or '@keyframes cycle-stamp-impact' in body
assert 'cycle-col-flash' in body
# Checkmark draw
assert 'cycle-check-svg' in body
# Conforme badge + glow
assert 'cycle-conforme-badge' in body
assert 'cycle-conforme-glow' in body
assert 'Loi&nbsp;25 conforme' in body or 'Loi 25 conforme' in body
# Économies annuelles avec 3 counters
assert 'Économies annuelles' in body
assert 'sav1' in body and 'sav2' in body and 'sav3' in body
assert 'cycle-savings-card' in body
# Live dot "Réunion en cours"
assert 'cycle-live-dot' in body
# Dash flow
assert 'cycle-line-flow' in body
# ⚠ remplacé
assert '⚠ CADRE RÉGLEMENTAIRE</p>' not in body, "Cycle eyebrow ⚠ literal must be replaced by SVG"
def test_round4_no_emoji_warning_triangle():
"""Round 4 — aucun ⚠ littéral ne doit subsister dans le HTML rendu."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Le caractère ⚠ U+26A0 ne doit plus apparaître nulle part dans landing.html
# (sauf dans les keyframes/CSS comments qui sont absents)
assert '' not in body, "Round 4 must not contain literal ⚠ character anywhere on landing"
def test_round4_reduced_motion_disables_all_cinematics():
"""Round 4 — prefers-reduced-motion media query must disable ALL new cinematics."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
# Le bloc @media (prefers-reduced-motion: reduce) doit explicitement neutraliser :
# - radar (cadre-radar)
# - packets (cadre-packet)
# - typewriter (typeLine has reduced-motion shortcut)
# - stamp (cycle-stamp)
# - conforme glow + badge
assert 'prefers-reduced-motion: reduce' in body
assert 'cadre-radar' in body and 'cadre-packet' in body
# Round 4 reduced-motion must disable cycle-stamp + cycle-conforme-badge animations
assert 'cycle-stamp' in body and 'cycle-conforme-badge' in body
# Counter helper has explicit reduced-motion guard
assert "matchMedia('(prefers-reduced-motion: reduce)')" 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()