Patch captcha

This commit is contained in:
Esteban
2026-07-02 15:58:39 +02:00
parent 6fb78e0bbb
commit 481fbc9498
2 changed files with 339 additions and 79 deletions
+137 -47
View File
@@ -20,6 +20,11 @@ if (isset($_SESSION['captcha_valid']) && $_SESSION['captcha_valid'] === true) {
// Nombre maximum d'essais // Nombre maximum d'essais
$maxAttempts = 3; $maxAttempts = 3;
// Durée minimale (en secondes) entre l'affichage de la série et la soumission du formulaire.
// Un humain doit lire plusieurs messages avant de répondre : une soumission plus rapide
// que ça trahit presque toujours un robot qui remplit le formulaire instantanément.
$minSolveTime = 1.2;
// Liste des sujets normaux // Liste des sujets normaux
$normalSubjects = [ $normalSubjects = [
'Votre rendez-vous est confirmé', 'Votre rendez-vous est confirmé',
@@ -150,28 +155,29 @@ $normalSenders = [
'Service facturation' 'Service facturation'
]; ];
// Liste des sujets spam // Liste des sujets spam, chacun associé à un indice (raison) qui sera révélé après succès :
// une petite touche pédagogique sur les techniques de phishing les plus courantes
$spamSubjects = [ $spamSubjects = [
'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURDHUI', ['text' => 'URGENT !!! VOTRE COMPTE SERA SUPPRIMÉ AUJOURDHUI', 'reason' => 'Urgence exagérée et abus des majuscules'],
'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT', ['text' => 'FÉLICITATIONS !!! VOUS AVEZ GAGNÉ 5000 € MAINTENANT', 'reason' => 'Gain surprise, trop beau pour être vrai'],
'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE', ['text' => 'CLIQUEZ ICI IMMÉDIATEMENT POUR ÉVITER LE BLOCAGE', 'reason' => 'Appel à laction pressant et menaçant'],
'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE', ['text' => 'VOTRE COLIS EST BLOQUÉ, PAYEZ TOUT DE SUITE', 'reason' => 'Demande de paiement immédiat inhabituelle'],
'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE', ['text' => 'DERNIÈRE ALERTE : CONFIRMEZ VOTRE MOT DE PASSE', 'reason' => 'Un service légitime ne redemande jamais un mot de passe par email'],
'ACTION OBLIGATOIRE DANS LES 5 MINUTES', ['text' => 'ACTION OBLIGATOIRE DANS LES 5 MINUTES', 'reason' => 'Délai artificiellement très court'],
'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE', ['text' => 'VOTRE BANQUE EXIGE UNE VÉRIFICATION IMMÉDIATE', 'reason' => 'Usurpation dun organisme bancaire'],
'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT', ['text' => 'RÉCUPÉREZ VOTRE CADEAU GRATUIT MAINTENANT', 'reason' => 'Cadeau non sollicité'],
'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER', ['text' => 'ALERTE SÉCURITÉ : VOTRE IDENTITÉ EST EN DANGER', 'reason' => 'Peur infondée utilisée pour pousser à agir vite'],
'VOUS ÊTES LE GRAND GAGNANT DU JOUR', ['text' => 'VOUS ÊTES LE GRAND GAGNANT DU JOUR', 'reason' => 'Gain surprise non sollicité'],
'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE', ['text' => 'PAIEMENT REFUSÉ !!! METTEZ À JOUR VOTRE CARTE', 'reason' => 'Fausse alerte de paiement pour voler des données bancaires'],
'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE', ['text' => 'CONFIRMEZ VOS INFORMATIONS SINON FERMETURE', 'reason' => 'Menace de fermeture de compte'],
'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER', ['text' => 'OFFRE SECRÈTE EXCEPTIONNELLE À RÉCUPÉRER', 'reason' => 'Offre “secrète” artificiellement exclusive'],
'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT', ['text' => 'VOTRE SESSION EXPIRE, CLIQUEZ MAINTENANT', 'reason' => 'Fausse urgence de session'],
'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI', ['text' => 'REMBOURSEMENT IMMÉDIAT DISPONIBLE ICI', 'reason' => 'Remboursement non sollicité'],
'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES', ['text' => 'ATTENTION !!! COMPTE BLOQUÉ DANS QUELQUES MINUTES', 'reason' => 'Compte à rebours anxiogène'],
'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE', ['text' => 'DERNIÈRE CHANCE AVANT SUPPRESSION DÉFINITIVE', 'reason' => 'Menace de suppression définitive'],
'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ', ['text' => 'VOTRE MOT DE PASSE A ÉTÉ PIRATÉ, AGISSEZ', 'reason' => 'Fausse alerte de piratage'],
'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE', ['text' => 'VÉRIFICATION URGENTE DE VOTRE CARTE BANCAIRE', 'reason' => 'Demande dinformations bancaires par email'],
'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT' ['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse dargent contre des données personnelles'],
]; ];
// Liste des expéditeurs spam // Liste des expéditeurs spam
@@ -219,6 +225,10 @@ function firstLetterSafe(string $text): string
return strtoupper(substr($text, 0, 1)); return strtoupper(substr($text, 0, 1));
} }
// Construit une nouvelle série de mails pour une manche de captcha.
// Le nombre de mails frauduleux varie entre 1 et 3 : un robot ne peut donc pas se
// contenter de "toujours cocher une seule case au hasard", ce qui réduit fortement
// les chances de réussir par pure chance (voir le calcul dans la doc du projet).
function buildInboxRound( function buildInboxRound(
array $normalSubjects, array $normalSubjects,
array $normalSenders, array $normalSenders,
@@ -236,18 +246,20 @@ function buildInboxRound(
'subject' => $subject, 'subject' => $subject,
'preview' => $normalPreviews[array_rand($normalPreviews)], 'preview' => $normalPreviews[array_rand($normalPreviews)],
'is_spam' => false, 'is_spam' => false,
'reason' => null,
'time' => sprintf('%02d:%02d', random_int(8, 19), random_int(0, 59)) 'time' => sprintf('%02d:%02d', random_int(8, 19), random_int(0, 59))
]; ];
} }
$spamPool = []; $spamPool = [];
foreach ($spamSubjects as $index => $subject) { foreach ($spamSubjects as $index => $spam) {
$spamPool[] = [ $spamPool[] = [
'id' => 'spam_' . $index, 'id' => 'spam_' . $index,
'sender' => $spamSenders[array_rand($spamSenders)], 'sender' => $spamSenders[array_rand($spamSenders)],
'subject' => $subject, 'subject' => $spam['text'],
'preview' => $spamPreviews[array_rand($spamPreviews)], 'preview' => $spamPreviews[array_rand($spamPreviews)],
'is_spam' => true, 'is_spam' => true,
'reason' => $spam['reason'],
'time' => sprintf('%02d:%02d', random_int(0, 23), random_int(0, 59)) 'time' => sprintf('%02d:%02d', random_int(0, 23), random_int(0, 59))
]; ];
} }
@@ -255,15 +267,25 @@ function buildInboxRound(
shuffle($normalPool); shuffle($normalPool);
shuffle($spamPool); shuffle($spamPool);
$selected = array_slice($normalPool, 0, 9); // Entre 1 et 3 mails frauduleux dans les 10 affichés (voir commentaire de la fonction)
$selected[] = $spamPool[0]; $spamCount = random_int(1, 3);
$selectedSpam = array_slice($spamPool, 0, $spamCount);
$selectedNormal = array_slice($normalPool, 0, 10 - $spamCount);
$selected = array_merge($selectedSpam, $selectedNormal);
shuffle($selected); shuffle($selected);
$correctIds = array_map(function ($mail) {
return $mail['id'];
}, $selectedSpam);
sort($correctIds);
return [ return [
'token' => bin2hex(random_bytes(16)), 'token' => bin2hex(random_bytes(16)),
'correct_id' => $spamPool[0]['id'], 'correct_ids' => $correctIds,
'emails' => $selected, 'emails' => $selected,
'attempts_left' => $maxAttempts 'attempts_left' => $maxAttempts,
'started_at' => microtime(true)
]; ];
} }
@@ -283,8 +305,15 @@ $round = $_SESSION['captcha_round'];
$error = ''; $error = '';
$success = false; $success = false;
$locked = false; $locked = false;
$foundReasons = [];
if (!isset($round['emails']) || !is_array($round['emails'])) { // Regénère une série si la structure en session est invalide ou date d'avant cette mise à
// jour (ancienne session sans correct_ids/started_at) : évite tout crash après déploiement
if (
!isset($round['emails']) || !is_array($round['emails']) ||
!isset($round['correct_ids']) || !is_array($round['correct_ids']) ||
!isset($round['started_at'])
) {
$_SESSION['captcha_round'] = buildInboxRound( $_SESSION['captcha_round'] = buildInboxRound(
$normalSubjects, $normalSubjects,
$normalSenders, $normalSenders,
@@ -300,33 +329,66 @@ if (!isset($round['emails']) || !is_array($round['emails'])) {
// Gestion de la soumission du formulaire // Gestion de la soumission du formulaire
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$postedToken = $_POST['captcha_token'] ?? ''; $postedToken = $_POST['captcha_token'] ?? '';
$selectedMail = $_POST['selected_mail'] ?? '';
$selectedMails = (isset($_POST['selected_mails']) && is_array($_POST['selected_mails']))
? array_map('strval', $_POST['selected_mails'])
: [];
sort($selectedMails);
// Honeypot : champ invisible pour un humain (masqué en CSS), que les robots qui
// remplissent tous les champs d'un formulaire ont tendance à compléter malgré tout
$honeypot = trim($_POST['website'] ?? '');
if (!hash_equals($round['token'], $postedToken)) { if (!hash_equals($round['token'], $postedToken)) {
$error = 'Session invalide, recharge la page.'; $error = 'Session invalide, recharge la page.';
} elseif (($round['attempts_left'] ?? 0) <= 0) { } elseif (($round['attempts_left'] ?? 0) <= 0) {
$locked = true; $locked = true;
$error = 'Tu nas plus dessais. Recharge une nouvelle série.'; $error = 'Tu nas plus dessais. Recharge une nouvelle série.';
} elseif ($selectedMail !== $round['correct_id']) { } elseif ($honeypot !== '') {
// On fait échouer la tentative sans révéler la vraie raison, pour ne pas
// donner d'indice à un robot sur la façon dont il a été détecté
$_SESSION['captcha_round']['attempts_left']--;
$round = $_SESSION['captcha_round'];
$locked = ($round['attempts_left'] ?? 0) <= 0;
$error = $locked
? 'Perdu. Tu nas plus dessais. Recharge une nouvelle série.'
: 'Ce nest pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.';
sendLog('Captcha : champ piège rempli (robot probable)');
} elseif ((microtime(true) - (float) $round['started_at']) < $minSolveTime) {
// Une réponse aussi rapide n'est pas plausible pour un humain qui doit lire 10 mails
$_SESSION['captcha_round']['attempts_left']--;
$round = $_SESSION['captcha_round'];
$locked = ($round['attempts_left'] ?? 0) <= 0;
$error = $locked
? 'Perdu. Tu nas plus dessais. Recharge une nouvelle série.'
: 'Ce nest pas la bonne sélection. Prends le temps de lire les messages.';
sendLog('Captcha : soumission trop rapide (robot probable)');
} elseif ($selectedMails !== $round['correct_ids']) {
$_SESSION['captcha_round']['attempts_left']--; $_SESSION['captcha_round']['attempts_left']--;
$round = $_SESSION['captcha_round']; $round = $_SESSION['captcha_round'];
if (($round['attempts_left'] ?? 0) <= 0) { if (($round['attempts_left'] ?? 0) <= 0) {
$locked = true; $locked = true;
$error = 'Perdu. Tu nas plus dessais. Recharge une nouvelle série.'; $error = 'Perdu. Tu nas plus dessais. Recharge une nouvelle série.';
// Log Discord : visiteur bloqué au captcha (possible robot) sendLog('Captcha raté : plus aucun essai restant (possible robot)');
sendLog("Captcha raté : plus aucun essai restant (possible robot)");
} else { } else {
$error = 'Ce nest pas le bon mail. Regarde mieux le message le plus agressif ou trop urgent.'; $error = 'Ce nest pas la bonne sélection. Regarde mieux les messages les plus agressifs ou trop urgents.';
// Log Discord : mauvaise réponse au captcha sendLog('Captcha raté : mauvaise sélection (' . $round['attempts_left'] . ' essai(s) restant(s))');
sendLog("Captcha raté : mauvaise réponse (" . $round['attempts_left'] . " essai(s) restant(s))");
} }
} else { } else {
$_SESSION['captcha_valid'] = true; $_SESSION['captcha_valid'] = true;
// On récupère les explications des mails trouvés pour les afficher sur
// l'écran de succès (petit bonus pédagogique) avant de vider la manche
foreach ($round['emails'] as $mail) {
if ($mail['is_spam'] && !empty($mail['reason'])) {
$foundReasons[] = $mail['reason'];
}
}
unset($_SESSION['captcha_round']); unset($_SESSION['captcha_round']);
$success = true; $success = true;
// Log Discord : captcha validé, le visiteur accède au site sendLog('Captcha réussi : nouveau visiteur sur le site');
sendLog("Captcha réussi : nouveau visiteur sur le site");
} }
} }
@@ -342,6 +404,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<link rel="stylesheet" href="./styles/captcha.css"> <link rel="stylesheet" href="./styles/captcha.css">
</head> </head>
<body class="<?php echo $success ? 'success-mode' : ''; ?>"> <body class="<?php echo $success ? 'success-mode' : ''; ?>">
@@ -351,7 +414,19 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<div class="success-burst"></div> <div class="success-burst"></div>
<div class="success-icon">✅</div> <div class="success-icon">✅</div>
<h1>Vérification réussie</h1> <h1>Vérification réussie</h1>
<p>Bravo, tu as trouvé le mail frauduleux. Redirection en cours...</p> <p>Bravo, tu as repéré le(s) mail(s) frauduleux. Redirection en cours...</p>
<?php if (!empty($foundReasons)): ?>
<div class="success-reasons">
<p class="success-reasons-title">Ce que tu as repéré :</p>
<ul>
<?php foreach ($foundReasons as $reason): ?>
<li><i class="reason-dot"></i><?php echo htmlspecialchars($reason, ENT_QUOTES, 'UTF-8'); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="success-loader"> <div class="success-loader">
<span></span> <span></span>
</div> </div>
@@ -360,7 +435,7 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<script> <script>
setTimeout(function () { setTimeout(function () {
window.location.href = './index.php'; window.location.href = './index.php';
}, 1800); }, 2400);
</script> </script>
<?php else: ?> <?php else: ?>
<form class="captcha-card" method="post" action=""> <form class="captcha-card" method="post" action="">
@@ -370,13 +445,14 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<div> <div>
<h1 class="captcha-title">Vérification humaine</h1> <h1 class="captcha-title">Vérification humaine</h1>
<p class="captcha-subtitle"> <p class="captcha-subtitle">
Pour continuer, cliquez sur le <strong>mail frauduleux</strong> parmi les 10 messages ci-dessous. Sélectionne <strong>tous les emails qui te semblent frauduleux</strong> parmi les messages
ci-dessous. Certains sont plus dangereux que d'autres.
</p> </p>
</div> </div>
<div class="captcha-target"> <div class="captcha-target">
<span>Vérification demandée</span> <span>Vérification demandée</span>
<strong>Trouver le spam</strong> <strong>Repérer le phishing</strong>
</div> </div>
</div> </div>
@@ -384,6 +460,11 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<div class="captcha-attempts"> <div class="captcha-attempts">
<span>Essais restants</span> <span>Essais restants</span>
<strong><?php echo htmlspecialchars((string) $attemptsLeft, ENT_QUOTES, 'UTF-8'); ?> / <?php echo htmlspecialchars((string) $maxAttempts, ENT_QUOTES, 'UTF-8'); ?></strong> <strong><?php echo htmlspecialchars((string) $attemptsLeft, ENT_QUOTES, 'UTF-8'); ?> / <?php echo htmlspecialchars((string) $maxAttempts, ENT_QUOTES, 'UTF-8'); ?></strong>
<div class="attempts-dots">
<?php for ($i = 0; $i < $maxAttempts; $i++): ?>
<span class="<?php echo $i < $attemptsLeft ? 'dot-filled' : 'dot-empty'; ?>"></span>
<?php endfor; ?>
</div>
</div> </div>
<?php if ($locked): ?> <?php if ($locked): ?>
@@ -402,19 +483,21 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
<span></span> <span></span>
<span></span> <span></span>
</div> </div>
<div class="mailbox-label">Test anti-robot : trouvez le mail qui ressemble à une arnaque</div> <div class="mailbox-label">Test anti-robot : cochez les mails qui ressemblent à une arnaque</div>
</div> </div>
<div class="mail-list"> <div class="mail-list">
<?php foreach ($round['emails'] as $mail): ?> <?php foreach ($round['emails'] as $index => $mail): ?>
<label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>"> <label class="mail-option <?php echo $locked ? 'mail-option-disabled' : ''; ?>" style="--mail-index: <?php echo (int) $index; ?>">
<input <input
type="radio" type="checkbox"
name="selected_mail" name="selected_mails[]"
value="<?php echo htmlspecialchars($mail['id'], ENT_QUOTES, 'UTF-8'); ?>" value="<?php echo htmlspecialchars($mail['id'], ENT_QUOTES, 'UTF-8'); ?>"
<?php echo $locked ? 'disabled' : 'required'; ?> <?php echo $locked ? 'disabled' : ''; ?>
> >
<span class="mail-row"> <span class="mail-row">
<span class="mail-check"><i class="fa-solid fa-check"></i></span>
<span class="mail-avatar"> <span class="mail-avatar">
<?php echo htmlspecialchars(firstLetterSafe($mail['sender']), ENT_QUOTES, 'UTF-8'); ?> <?php echo htmlspecialchars(firstLetterSafe($mail['sender']), ENT_QUOTES, 'UTF-8'); ?>
</span> </span>
@@ -447,6 +530,13 @@ $attemptsLeft = $round['attempts_left'] ?? 0;
value="<?php echo htmlspecialchars($round['token'], ENT_QUOTES, 'UTF-8'); ?>" value="<?php echo htmlspecialchars($round['token'], ENT_QUOTES, 'UTF-8'); ?>"
> >
<!-- Champ piège (honeypot) : invisible et exclu du tabulateur pour un humain,
mais souvent rempli automatiquement par les robots qui remplissent tout -->
<div class="captcha-honeypot" aria-hidden="true">
<label for="website">Laisse ce champ vide</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<div class="captcha-footer"> <div class="captcha-footer">
<p class="captcha-note"> <p class="captcha-note">
Tu as plusieurs essais. Les faux mails utilisent souvent lurgence, les cadeaux improbables, les menaces ou les demandes de mot de passe. Tu as plusieurs essais. Les faux mails utilisent souvent lurgence, les cadeaux improbables, les menaces ou les demandes de mot de passe.
+202 -32
View File
@@ -1,7 +1,7 @@
body { body {
background: background:
radial-gradient(circle at top, rgba(108, 99, 255, 0.18), transparent 35%), radial-gradient(circle at top, rgba(255, 122, 26, 0.16), transparent 35%),
#0d0d0d; #0a0a10;
margin: 0; margin: 0;
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif; font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
color: white; color: white;
@@ -21,10 +21,22 @@ body {
.captcha-card, .captcha-card,
.success-screen { .success-screen {
background: linear-gradient(180deg, #111116 0%, #161622 100%); background: linear-gradient(180deg, #111116 0%, #161622 100%);
border: 1px solid #2a2a3e; border: 1px solid #262638;
border-radius: 24px; border-radius: 24px;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28); box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
padding: 30px; padding: 30px;
animation: cardEnter 0.5s ease;
}
@keyframes cardEnter {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.captcha-badge { .captcha-badge {
@@ -33,9 +45,9 @@ body {
gap: 8px; gap: 8px;
padding: 8px 14px; padding: 8px 14px;
border-radius: 999px; border-radius: 999px;
background: rgba(108, 99, 255, 0.14); background: rgba(255, 122, 26, 0.14);
border: 1px solid rgba(108, 99, 255, 0.28); border: 1px solid rgba(255, 122, 26, 0.3);
color: #9d97ff; color: #ffa155;
font-size: 13px; font-size: 13px;
font-weight: bold; font-weight: bold;
margin-bottom: 18px; margin-bottom: 18px;
@@ -63,10 +75,14 @@ body {
max-width: 700px; max-width: 700px;
} }
.captcha-subtitle strong {
color: #ffa155;
}
.captcha-target { .captcha-target {
min-width: 240px; min-width: 240px;
background-color: #1a1a24; background-color: #1a1a24;
border: 1px solid #2a2a3e; border: 1px solid #262638;
border-radius: 18px; border-radius: 18px;
padding: 18px; padding: 18px;
} }
@@ -82,7 +98,7 @@ body {
.captcha-target strong { .captcha-target strong {
font-size: 22px; font-size: 22px;
color: #8f88ff; color: #ffa155;
} }
.captcha-stats { .captcha-stats {
@@ -95,6 +111,9 @@ body {
} }
.captcha-attempts { .captcha-attempts {
display: flex;
align-items: center;
gap: 14px;
background-color: #171b28; background-color: #171b28;
border: 1px solid #283049; border: 1px solid #283049;
border-radius: 14px; border-radius: 14px;
@@ -115,6 +134,28 @@ body {
color: #ffffff; color: #ffffff;
} }
/* Pastilles visuelles représentant les essais restants */
.attempts-dots {
display: flex;
gap: 6px;
}
.attempts-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
transition: background-color 0.3s ease, transform 0.3s ease;
}
.attempts-dots .dot-filled {
background: linear-gradient(180deg, #ff7a1a 0%, #e8630a 100%);
box-shadow: 0 0 8px rgba(255, 122, 26, 0.5);
}
.attempts-dots .dot-empty {
background-color: #2a2f42;
}
.captcha-reset, .captcha-reset,
.captcha-secondary-button { .captcha-secondary-button {
display: inline-flex; display: inline-flex;
@@ -124,14 +165,15 @@ body {
border-radius: 12px; border-radius: 12px;
text-decoration: none; text-decoration: none;
color: white; color: white;
background-color: #2a314a; background-color: #2a2a3a;
border: 1px solid #3d4767; border: 1px solid #3a3a4e;
transition: transform 0.2s ease, background-color 0.2s ease; transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
} }
.captcha-reset:hover, .captcha-reset:hover,
.captcha-secondary-button:hover { .captcha-secondary-button:hover {
background-color: #394362; background-color: #34344a;
border-color: rgba(255, 122, 26, 0.4);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -143,6 +185,15 @@ body {
color: #ffb8bb; color: #ffb8bb;
padding: 14px 16px; padding: 14px 16px;
font-size: 14px; font-size: 14px;
animation: errorShake 0.4s ease;
}
@keyframes errorShake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(5px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(3px); }
} }
.mailbox { .mailbox {
@@ -199,10 +250,25 @@ body {
.mail-option { .mail-option {
position: relative; position: relative;
opacity: 0;
animation: mailEnter 0.4s ease forwards;
animation-delay: calc(var(--mail-index, 0) * 0.04s);
}
@keyframes mailEnter {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.mail-option-disabled { .mail-option-disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5;
} }
.mail-option input { .mail-option input {
@@ -211,9 +277,15 @@ body {
pointer-events: none; pointer-events: none;
} }
/* Anneau de focus visible au clavier (accessibilité) sur la ligne associée à la checkbox */
.mail-option input:focus-visible + .mail-row {
outline: 2px solid #ff7a1a;
outline-offset: -2px;
}
.mail-row { .mail-row {
display: grid; display: grid;
grid-template-columns: 48px minmax(120px, 220px) 1fr 70px; grid-template-columns: 26px 48px minmax(120px, 220px) 1fr 70px;
gap: 14px; gap: 14px;
align-items: center; align-items: center;
padding: 16px 18px; padding: 16px 18px;
@@ -232,16 +304,38 @@ body {
} }
.mail-option input:checked + .mail-row { .mail-option input:checked + .mail-row {
background-color: rgba(108, 99, 255, 0.14); background-color: rgba(255, 122, 26, 0.12);
box-shadow: inset 0 0 0 1px rgba(108, 99, 255, 0.35); box-shadow: inset 0 0 0 1px rgba(255, 122, 26, 0.35);
}
/* Case à cocher personnalisée */
.mail-check {
width: 22px;
height: 22px;
border-radius: 6px;
background-color: #1a1a24;
border: 1px solid #3a3a4e;
display: flex;
align-items: center;
justify-content: center;
color: transparent;
font-size: 12px;
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.15s ease, transform 0.15s ease;
}
.mail-option input:checked + .mail-row .mail-check {
background: linear-gradient(180deg, #ff7a1a 0%, #e8630a 100%);
border-color: #ff7a1a;
color: white;
transform: scale(1.08);
} }
.mail-avatar { .mail-avatar {
width: 42px; width: 42px;
height: 42px; height: 42px;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(180deg, #2f3550 0%, #1f2334 100%); background: linear-gradient(180deg, #2a2a3a 0%, #1a1a24 100%);
border: 1px solid #303650; border: 1px solid #303040;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -287,6 +381,16 @@ body {
text-align: right; text-align: right;
} }
/* Champ piège (honeypot) : invisible pour un humain, exclu du tabulateur,
mais toujours présent dans le HTML pour les robots qui remplissent tout */
.captcha-honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.captcha-footer { .captcha-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -308,7 +412,7 @@ body {
} }
.captcha-submit { .captcha-submit {
background: linear-gradient(180deg, #7a72ff 0%, #5e56eb 100%); background: linear-gradient(180deg, #ff8c3a 0%, #e8630a 100%);
color: white; color: white;
border: none; border: none;
border-radius: 12px; border-radius: 12px;
@@ -317,12 +421,16 @@ body {
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 12px 24px rgba(94, 86, 235, 0.28); box-shadow: 0 12px 24px rgba(232, 99, 10, 0.32);
} }
.captcha-submit:hover { .captcha-submit:hover {
transform: translateY(-1px) scale(1.01); transform: translateY(-1px) scale(1.01);
box-shadow: 0 16px 30px rgba(94, 86, 235, 0.34); box-shadow: 0 16px 30px rgba(232, 99, 10, 0.4);
}
.captcha-submit:active {
transform: translateY(1px) scale(0.99);
} }
.success-mode { .success-mode {
@@ -348,12 +456,60 @@ body {
} }
.success-screen p { .success-screen p {
margin: 0 auto 24px; margin: 0 auto 12px;
max-width: 520px; max-width: 520px;
color: #ced2e6; color: #ced2e6;
font-size: 18px; font-size: 18px;
} }
/* Bloc pédagogique listant les indices de phishing repérés */
.success-reasons {
max-width: 460px;
margin: 18px auto 26px;
padding: 18px 22px;
text-align: left;
background: rgba(255, 122, 26, 0.08);
border: 1px solid rgba(255, 122, 26, 0.25);
border-radius: 16px;
animation: cardEnter 0.5s ease 0.3s both;
}
.success-reasons-title {
margin: 0 0 10px;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #ffa155;
}
.success-reasons ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.success-reasons li {
display: flex;
align-items: flex-start;
gap: 10px;
color: #d8d9e6;
font-size: 14.5px;
line-height: 1.5;
}
.reason-dot {
flex-shrink: 0;
width: 7px;
height: 7px;
margin-top: 6px;
border-radius: 50%;
background: #ff8c3a;
}
.success-loader { .success-loader {
width: 240px; width: 240px;
height: 10px; height: 10px;
@@ -368,8 +524,8 @@ body {
display: block; display: block;
height: 100%; height: 100%;
width: 0; width: 0;
background: linear-gradient(90deg, #62d26f 0%, #9cffaf 100%); background: linear-gradient(90deg, #ffa155 0%, #ff7a1a 100%);
animation: loadingBar 1.8s linear forwards; animation: loadingBar 2.4s linear forwards;
} }
.success-burst { .success-burst {
@@ -377,10 +533,10 @@ body {
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
background: background:
radial-gradient(circle at 20% 30%, rgba(255, 215, 0, 0.25), transparent 18%), radial-gradient(circle at 20% 30%, rgba(255, 197, 92, 0.25), transparent 18%),
radial-gradient(circle at 80% 20%, rgba(98, 210, 111, 0.22), transparent 18%), radial-gradient(circle at 80% 20%, rgba(98, 210, 111, 0.2), transparent 18%),
radial-gradient(circle at 30% 80%, rgba(122, 114, 255, 0.2), transparent 20%), radial-gradient(circle at 30% 80%, rgba(255, 122, 26, 0.22), transparent 20%),
radial-gradient(circle at 75% 70%, rgba(255, 105, 180, 0.16), transparent 18%); radial-gradient(circle at 75% 70%, rgba(255, 145, 77, 0.18), transparent 18%);
animation: burstPulse 1s ease-in-out infinite alternate; animation: burstPulse 1s ease-in-out infinite alternate;
} }
@@ -429,6 +585,22 @@ body {
} }
} }
@media (prefers-reduced-motion: reduce) {
.captcha-card,
.success-screen,
.mail-option,
.success-reasons,
.success-icon,
.captcha-error {
animation: none;
opacity: 1;
}
.success-burst {
animation: none;
}
}
@media (max-width: 900px) { @media (max-width: 900px) {
.captcha-top { .captcha-top {
flex-direction: column; flex-direction: column;
@@ -440,7 +612,7 @@ body {
} }
.mail-row { .mail-row {
grid-template-columns: 42px 1fr 56px; grid-template-columns: 22px 42px 1fr 56px;
} }
.mail-sender { .mail-sender {
@@ -482,15 +654,13 @@ body {
} }
.mail-row { .mail-row {
grid-template-columns: 36px 1fr; grid-template-columns: 20px 36px 1fr;
gap: 10px; gap: 10px;
padding: 14px 14px; padding: 14px 14px;
} }
.mail-time { .mail-time {
grid-column: 2; display: none;
text-align: left;
margin-top: 4px;
} }
.success-screen h1 { .success-screen h1 {