@@ -519,23 +604,25 @@
Réunion en cours — données confidentielles
+
+ Live
- {# 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) #}
@@ -576,8 +663,8 @@
- 315
- $ / réunion
+ 315
+ $ / réunion
@@ -587,18 +674,20 @@
{# COL 2 — IA cloud américaine #}
-
- {# Overlay légal NON CONFORME (phase 3) #}
-
-
+ {# Overlay légal NON CONFORME (phase 3) — round 4 : STAMP huissier qui claque #}
+
+
NON CONFORME
-
Loi 25 · Cloud Act américain
+
Loi 25 · Cloud Act américain
@@ -612,10 +701,10 @@
- {# Particules de fuite #}
- {% for i in range(6) %}
+ {# Particules de fuite (round 4 : 10 particules, plus dense) #}
+ {% for i in range(10) %}
+ 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);">
{% endfor %}
@@ -660,11 +749,18 @@
style="background: radial-gradient(ellipse 80% 35% at 50% 0%, rgba(0,189,216,0.12) 0%, transparent 65%);">
diff --git a/tests/test_marketing_landing_template.py b/tests/test_marketing_landing_template.py
index 8aab7c2..f63f3db 100644
--- a/tests/test_marketing_landing_template.py
+++ b/tests/test_marketing_landing_template.py
@@ -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 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' 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()