second commit

This commit is contained in:
Wazadriano 2026-06-08 10:31:03 +02:00
parent c2c1101dee
commit 23a819e089
16 changed files with 10709 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1003
PROJECT_BRIEF.md Normal file

File diff suppressed because it is too large Load diff

557
SYNTHESIS_UPDATE.md Normal file
View file

@ -0,0 +1,557 @@
# UPDATE BRIEF — Page de Synthèse "6 piliers de sécurité IoT"
> Document complémentaire à `PROJECT_BRIEF.md`. Décrit la **6ème et dernière page** du site, ainsi que les modifications mineures à apporter aux pages existantes pour la préparer.
> À donner à Claude Code après que le site initial (5 pages) est en place.
---
## 0. Contexte
Le cours impose un framework canonique de **6 piliers de sécurité IoT**. Le site doit se conclure sur une page de synthèse qui :
1. Présente ce framework officiel
2. Mappe chaque pilier aux échecs documentés dans les 5 pages précédentes (R1-R5, AMD-01 à 05, CVE-2015-5611, etc.)
3. Tire les leçons générales : la synthèse Jeep n'est qu'un cas particulier d'un modèle de raisonnement réutilisable.
Important : **les contenus textuels existants ne changent pas**. On ajoute uniquement :
- Un champ `pillars[]` à plusieurs entités (risques, AMDEC, problèmes, solutions)
- Des **badges piliers** discrets dans les pages existantes
- Une **nouvelle page** `/synthese` (ou onglet 06)
---
## 1. Référentiel canonique — Les 6 piliers
À utiliser **verbatim**, sans paraphraser, car c'est le framework du cours.
```ts
type RiskFamily = "Réseau" | "Sécurité" | "Humain" | "Logiciel";
type PillarKey =
| "chiffrement"
| "identites"
| "segmentation"
| "hardening"
| "maj"
| "supervision";
interface Pillar {
key: PillarKey;
label: string;
icon: string; // nom lucide-react
color: string; // hex
riskFamilies: RiskFamily[];
isTransversal: boolean;
description: string;
}
const PILLARS: Pillar[] = [
{
key: "chiffrement",
label: "Chiffrement",
icon: "Lock",
color: "#facc15", // amber-400
riskFamilies: ["Réseau", "Sécurité"],
isTransversal: false,
description:
"Protection de la confidentialité et de l'intégrité des données en transit et au repos. TLS, chiffrement disque, gestion de clés (HSM/TPM), cryptographie à l'état de l'art.",
},
{
key: "identites",
label: "Identités et permissions",
icon: "KeyRound",
color: "#a78bfa", // violet-400
riskFamilies: ["Sécurité", "Humain"],
isTransversal: false,
description:
"Identité unique et vérifiable pour chaque device, utilisateur et service. Authentification forte (mTLS, MFA, certificats), autorisations granulaires, suppression des credentials par défaut.",
},
{
key: "segmentation",
label: "Segmentation réseau",
icon: "Network",
color: "#22d3ee", // cyan-400
riskFamilies: ["Réseau", "Sécurité"],
isTransversal: false,
description:
"Séparation logique des domaines de criticité. VLAN, zero-trust, gateways filtrantes, architecture en zones et conduits (IEC 62443 / ISO 21434). Empêcher la latéralité d'un attaquant après compromission.",
},
{
key: "hardening",
label: "Hardening (durcissement)",
icon: "Shield",
color: "#60a5fa", // blue-400
riskFamilies: ["Sécurité", "Humain", "Logiciel"],
isTransversal: false,
description:
"Configuration sécurisée par défaut. Secure boot, signature de firmware, suppression des services non essentiels, principle of least functionality, désactivation des ports inutiles, durcissement OS et applications.",
},
{
key: "maj",
label: "Mises à jour sécurisées",
icon: "RefreshCw",
color: "#34d399", // emerald-400
riskFamilies: ["Logiciel", "Sécurité"],
isTransversal: false,
description:
"Capacité à déployer rapidement et de manière vérifiable des patches sur l'ensemble de la flotte. OTA résistante (Uptane / TUF), signature des updates, rollback protection, gestion du cycle de vie.",
},
{
key: "supervision",
label: "Supervision",
icon: "Eye",
color: "#fb923c", // orange-400
riskFamilies: ["Réseau", "Sécurité", "Humain", "Logiciel"],
isTransversal: true, // ← SEULE COUCHE TRANSVERSALE
description:
"Détection, journalisation, monitoring et réponse à incident. SOC / V-SOC, IDS embarqué, télémétrie sécurité, threat intelligence partagée (ISAC), procédures d'incident testées. Couche transversale qui adresse toutes les familles de risques.",
},
];
```
**Familles de risques** (à utiliser comme tags) :
- `Réseau` — vulnérabilités liées à la connectivité, au transport, à l'isolation
- `Sécurité` — vulnérabilités au sens cybersécurité technique stricte
- `Humain` — erreurs de configuration, ingénierie sociale, faute opérationnelle
- `Logiciel` — bugs, vulnérabilités applicatives, défauts de conception
---
## 2. Modifications des structures de données existantes
### 2.1 Ajout du champ `pillars` à 3 types
Modifier les interfaces TypeScript existantes :
```ts
interface Risk {
// ... champs existants
pillars: PillarKey[]; // ← AJOUT (1 à 3 piliers par risque)
}
interface AMDECRow {
// ... champs existants
pillars: PillarKey[]; // ← AJOUT (1 à 3 piliers par mode)
}
interface NamedItem { // utilisé pour les solutions du Dossier
// ... champs existants
pillars?: PillarKey[]; // ← AJOUT optionnel
}
```
### 2.2 Tagging des risques (Page 2)
Modifier le fichier `src/data/jeep.ts` pour ajouter `pillars` à chaque risque :
| Risque | Pillars | Justification |
|--------|---------|---------------|
| **R1** Énumération flotte Sprint | `["segmentation"]` | L'attaque est rendue possible par l'absence d'isolation réseau Sprint |
| **R2** RCE D-Bus port 6667 | `["identites", "hardening"]` | Absence d'authentification (identités) + service exposé sur 0.0.0.0 (hardening) |
| **R3** Reflashing V850 non signé | `["hardening"]` | Absence de secure boot et de signature firmware |
| **R4** Contrôle freins/direction | `["segmentation", "identites"]` | Pas de gateway CAN (segmentation) + trames CAN non authentifiées (identités) |
| **R5** Désactivation frein parking | `["hardening", "supervision"]` | Persistance non détectée + firmware non protégé |
### 2.3 Tagging des modes AMDEC (Page 3)
| ID | Composant | Pillars |
|----|-----------|---------|
| **AMD-01** APN Sprint sans isolation | `["segmentation"]` |
| **AMD-02** Service D-Bus port 6667 | `["hardening", "identites", "chiffrement"]` |
| **AMD-03** V850 firmware non signé | `["hardening"]` |
| **AMD-04** Architecture réseau interne plate | `["segmentation"]` |
| **AMD-05** CAN bus sans authentification | `["identites", "segmentation"]` |
### 2.4 Tagging optionnel des solutions du dossier (Page 1)
| Solution | Pillars |
|----------|---------|
| Segmentation réseau interne (gateway CAN) | `["segmentation"]` |
| Firmware signé partout | `["hardening"]` |
| OTA sécurisées (Uptane) | `["maj"]` |
| Isolation au niveau opérateur | `["segmentation"]` |
| ISO/SAE 21434 & UNECE R155 | `["supervision", "hardening"]` |
---
## 3. Modifications mineures dans les pages existantes
### 3.1 Ajout d'un composant `<PillarBadge>` réutilisable
```tsx
// src/components/ui/PillarBadge.tsx
"use client";
import { Lock, KeyRound, Network, Shield, RefreshCw, Eye } from "lucide-react";
import { PILLARS, PillarKey } from "@/data/pillars";
const ICONS = { Lock, KeyRound, Network, Shield, RefreshCw, Eye };
interface PillarBadgeProps {
pillar: PillarKey;
size?: "xs" | "sm";
showLabel?: boolean;
}
export function PillarBadge({ pillar, size = "xs", showLabel = false }: PillarBadgeProps) {
const p = PILLARS.find((x) => x.key === pillar);
if (!p) return null;
const Icon = ICONS[p.icon as keyof typeof ICONS];
return (
<span
className="inline-flex items-center gap-1 px-1.5 py-0.5 border font-mono tracking-wider"
style={{
borderColor: p.color + "60",
color: p.color,
background: p.color + "12",
fontSize: size === "xs" ? "9px" : "10px",
}}
title={p.label}
>
<Icon size={size === "xs" ? 9 : 11} />
{showLabel && <span>{p.label.toUpperCase()}</span>}
</span>
);
}
```
### 3.2 Affichage des badges piliers
**Page 2 — Risques** : afficher les `<PillarBadge>` à côté du titre de chaque risque (à droite de R1, R2…), sans label (icône uniquement, tooltip au hover).
**Page 3 — AMDEC** : idem dans l'en-tête de chaque `AMDECRow`, à côté de l'ID `AMD-01`.
**Page 1 — Dossier (solutions)** : badge pilier en bas à droite de chaque `<SolutionCard>`.
Ces badges sont **purement informatifs** dans les pages existantes — ils annoncent la grille de lecture qui sera développée dans la synthèse finale.
---
## 4. NOUVELLE PAGE 6 — Synthèse 6 piliers
### 4.1 Route
```
/synthese ou /pilliers (au choix, je préfère /synthese)
```
### 4.2 Onglet de navigation
Ajouter en 6ème position dans la `<NavTabs>` :
```
06 · Synthèse · [6 piliers]
```
### 4.3 Structure de la page
```
┌────────────────────────────────────────────────────────┐
│ BRIEFING : "Synthèse 6 piliers et familles de risques"│
│ Phrase intro + 4 familles de risques en cartes │
├────────────────────────────────────────────────────────┤
│ DIAGRAMME : les 6 piliers (visualisation) │
│ 5 piliers en cercle + Supervision au centre/à part │
├────────────────────────────────────────────────────────┤
│ POUR CHAQUE PILIER (6 sections expandables) : │
│ - Titre + icône + couleur │
│ - Familles de risques adressées (chips) │
│ - "Statut chez Jeep" : verdict + score qualitatif │
│ - Items liés (R1-R5, AMD-01..05) en chips cliquables│
│ - Leçon retenue │
├────────────────────────────────────────────────────────┤
│ TABLEAU DE COUVERTURE │
│ Une matrice piliers × items (X marque l'échec) │
├────────────────────────────────────────────────────────┤
│ ENCADRÉ FINAL : Supervision est transversale │
│ Pourquoi c'est important pédagogiquement │
└────────────────────────────────────────────────────────┘
```
### 4.4 Contenu narratif — verbatim à utiliser
#### Briefing intro
> **Synthèse — 6 piliers de sécurité IoT et familles de risques**
>
> Chaque pilier adresse des familles de risques spécifiques. La **supervision est la seule couche transversale** du modèle : elle adresse les 4 familles à la fois (Réseau, Sécurité, Humain, Logiciel).
>
> Cette page tire les leçons générales du cas Jeep Cherokee 2015 en mappant chaque vulnérabilité identifiée (dans les 5 pages précédentes) à l'un des 6 piliers manquants. C'est la grille de lecture qui transforme une étude de cas particulière en outil d'analyse réutilisable.
#### Familles de risques (4 cartes en haut)
```
┌─ RÉSEAU ─────────┐ ┌─ SÉCURITÉ ────────┐
│ Connectivité, │ │ Cybersécurité │
│ transport, RFC, │ │ technique stricte │
│ isolation L2/L3 │ │ (auth, crypto, │
│ │ │ exploits) │
└──────────────────┘ └───────────────────┘
┌─ HUMAIN ──────────┐ ┌─ LOGICIEL ────────┐
│ Erreurs de config,│ │ Bugs, vulns │
│ ingénierie sociale│ │ applicatives, │
│ faute opérationnel│ │ défauts conception│
└──────────────────┘ └───────────────────┘
```
#### Pilier 1 — Chiffrement
**Familles adressées** : Réseau, Sécurité
**Statut chez Jeep** : ❌ **Défaillant**
Le service D-Bus exposé sur le port 6667 communiquait sans aucun chiffrement ni authentification depuis le réseau cellulaire Sprint. Les communications internes au véhicule (CAN bus, SPI vers le V850) étaient également en clair, par conception. La cryptographie n'était utilisée nulle part dans la chaîne d'attaque exploitée.
**Items liés** : `R2` · `AMD-02`
**Leçon retenue** : Tout port ouvert sur Internet doit imposer du TLS mutuel. Toute communication entre composants critiques doit être chiffrée et authentifiée, même au sein d'un système supposé fermé (le CAN bus n'est plus fermé dès qu'il y a un modem cellulaire à proximité).
---
#### Pilier 2 — Identités et permissions
**Familles adressées** : Sécurité, Humain
**Statut chez Jeep** : ❌ **Défaillant**
Le CVE-2015-5611 est littéralement intitulé *"Missing Authorization"*. Le service D-Bus accepte des connexions non authentifiées (CVSS v2 : `Au:N`, v3 : `PR:N`). Le V850 accepte le reflashing sans vérifier l'identité du flasher. Les trames CAN ne portent aucune authentification — n'importe quel ECU compromis peut envoyer n'importe quelle commande à n'importe quel autre ECU.
**Items liés** : `R2` · `R4` · `AMD-02` · `AMD-05` · **CVE-2015-5611**
**Leçon retenue** : Le principe de moindre privilège n'est pas un concept SI seulement — il s'applique au hardware embarqué. Chaque actuateur du véhicule devrait n'accepter de commandes que de l'ECU légitimement habilité à le piloter, et cette habilitation doit être cryptographiquement vérifiable (cf. SecOC dans AUTOSAR).
---
#### Pilier 3 — Segmentation réseau
**Familles adressées** : Réseau, Sécurité
**Statut chez Jeep** : ❌ **Défaillant** (le plus défaillant de tous)
Aucune isolation L2/L3 entre clients Sprint : 1.4M véhicules mutuellement adressables depuis n'importe quel téléphone du même opérateur. Architecture réseau interne au véhicule strictement plate, sans gateway filtrante entre l'infotainment (Uconnect) et le CAN bus de sécurité fonctionnelle (freins, direction). C'est ce pilier dont l'absence rend l'attaque exploitable à l'échelle.
**Items liés** : `R1` · `R4` · `AMD-01` · `AMD-04` · `AMD-05`
**Leçon retenue** : La segmentation est la mesure qui contient le rayon d'explosion d'une compromission. Sa rentabilité défensive est exceptionnelle : un seul investissement architectural neutralise potentiellement des dizaines d'attaques. C'est aussi la mesure que les frameworks récents (ISO 21434, IEC 62443) mettent au cœur de l'architecture.
---
#### Pilier 4 — Hardening (durcissement)
**Familles adressées** : Sécurité, Humain, Logiciel
**Statut chez Jeep** : ❌ **Défaillant**
Service D-Bus configuré pour écouter sur `0.0.0.0` au lieu de `127.0.0.1` (configuration par défaut conservée en production). Microcontrôleur V850 acceptant n'importe quel firmware via SPI, sans secure boot ni vérification cryptographique. Les défauts de hardening sont *cumulatifs* — chacun ouvre une porte, et leur addition forme la chaîne d'attaque.
**Items liés** : `R2` · `R3` · `R5` · `AMD-02` · `AMD-03`
**Leçon retenue** : Le hardening est un travail ingrat mais payant : il consiste à appliquer le *principle of least functionality* partout. Désactiver chaque service non utilisé, fermer chaque port non requis, signer chaque binaire, n'accepter aucune configuration par défaut. Un système non durci offre 100 chemins d'attaque ; un système durci en offre 5 — et chacun nécessite réellement une vulnérabilité.
---
#### Pilier 5 — Mises à jour sécurisées
**Familles adressées** : Logiciel, Sécurité
**Statut chez Jeep** : ❌ **Critique — Absent**
Pas de capacité OTA chez FCA en 2015. La remediation a nécessité un rappel physique de 1.4M véhicules via clés USB postales (juillet → septembre 2015). C'est ce pilier dont l'absence a transformé une vulnérabilité technique en désastre opérationnel — sans OTA, le RTO de la flotte explose à ~6 mois.
**Items liés** : Page Continuité (composant critique = infrastructure OTA)
**Leçon retenue** : Un système IoT sans capacité de mise à jour à distance sécurisée est, à terme, un système vulnérable. La capacité de patcher rapidement est une *propriété de continuité d'activité* avant d'être une propriété de sécurité. Standards à adopter : Uptane (automotive), TUF (générique).
---
#### Pilier 6 — Supervision
**Familles adressées** : Réseau, Sécurité, Humain, Logiciel (*les 4*)
**Statut chez Jeep** : ❌ **Absent**
**Particularité** : 🌐 **Couche transversale**
C'est le **seul pilier qui adresse toutes les familles de risques à la fois**. En 2015, aucune télémétrie de sécurité côté véhicule, aucun V-SOC chez FCA, aucune procédure de détection. Les chercheurs Miller & Valasek ont prévenu FCA volontairement — sans cette divulgation responsable, l'industrie aurait peut-être attendu un incident mortel pour réagir. L'Auto-ISAC (plateforme de partage d'IoC entre constructeurs) a été créé en août 2015, *en réaction* à cet incident.
**Items liés** : Tous (transversal). En particulier, scores AMDEC élevés en `D` (détection) : `AMD-03` (D=8), `AMD-04` (D=7), `AMD-05` (D=8) — autant de défaillances *invisibles* avant impact.
**Leçon retenue** : La supervision est la *condition de possibilité* de tous les autres piliers. Sans elle, on ne sait pas que les autres piliers échouent — et donc on ne corrige rien. C'est aussi pourquoi elle est transversale dans le modèle : elle compense partiellement la défaillance ponctuelle des autres piliers en permettant la détection rapide et la réponse à incident.
### 4.5 Tableau de couverture (visualisation matricielle)
À afficher en bas de la page, avant la conclusion. Matrice **piliers × items**, où une croix indique que cet item *illustre l'échec* de ce pilier chez Jeep.
| | R1 | R2 | R3 | R4 | R5 | AMD-01 | AMD-02 | AMD-03 | AMD-04 | AMD-05 | CVE |
|--------------|:--:|:--:|:--:|:--:|:--:|:------:|:------:|:------:|:------:|:------:|:---:|
| Chiffrement | | ✕ | | | | | ✕ | | | | |
| Identités | | ✕ | | ✕ | | | ✕ | | | ✕ | ✕ |
| Segmentation | ✕ | | | ✕ | | ✕ | | | ✕ | ✕ | |
| Hardening | | ✕ | ✕ | | ✕ | | ✕ | ✕ | | | |
| MAJ | | | | | | | | | | | |
| Supervision | ✕ | ✕ | ✕ | ✕ | ✕ | ✕ | ✕ | ✕ | ✕ | ✕ | ✕ |
**Observation visuelle attendue** : la ligne *Supervision* est entièrement cochée (couche transversale = défaillance générale). La ligne *MAJ* est vide dans ce tableau parce que c'est un pilier organisationnel constructeur, pas un défaut visible dans un risque ou un mode de défaillance précis — il est traité séparément dans la page Continuité.
### 4.6 Conclusion finale (encadré marquant)
> **Pourquoi 5 piliers + 1 transversal, et pas 6 piliers à plat ?**
>
> Le modèle place volontairement la *Supervision* à part : c'est le seul pilier qui adresse simultanément les 4 familles de risques (Réseau, Sécurité, Humain, Logiciel). C'est aussi le seul pilier *réactif* — les cinq autres sont des mesures *préventives*. La supervision ne prévient pas une attaque, mais elle la *détecte* et permet d'y *répondre*.
>
> Dans le cas Jeep, les 6 piliers étaient simultanément défaillants — c'est pourquoi l'attaque a pu réussir, persister 9 mois sans détection (entre la divulgation responsable à FCA et la publication à Black Hat), et coûter ~100M$ à remedier. Une organisation qui investirait correctement dans ne serait-ce qu'un seul des piliers — *par exemple la supervision seule* — aurait probablement détecté Miller & Valasek dès leurs premiers scans Sprint et stoppé l'incident bien avant 1.4M de véhicules rappelés.
>
> **Grille de lecture portable** : ce modèle à 6 piliers se réapplique à n'importe quel système IoT — capteurs industriels, objets connectés grand public, infrastructure smart city, IoMT médical. C'est l'aboutissement pédagogique du dossier Jeep : un cas particulier transformé en méthode d'analyse réutilisable.
---
## 5. Composants UI à créer pour la page Synthèse
### 5.1 Composants principaux
```tsx
// src/components/pillars/PillarsDiagram.tsx
// SVG hexagonal — 5 piliers autour, Supervision au centre (transversale)
// Hover sur un pilier → highlight + tooltip
// Recommandé: SVG inline avec viewBox, animation simple au hover
```
```tsx
// src/components/pillars/PillarSection.tsx
// Section expandable par pilier
// En-tête: icône colorée + titre + chips familles + statut
// Body: narratif + chips items liés (R1, AMD-XX) + leçon
```
```tsx
// src/components/pillars/CoverageMatrix.tsx
// Tableau piliers × items
// Cellules cochées (✕) avec couleur du pilier en background
// Possibilité de hover sur une cellule pour voir le détail
```
```tsx
// src/components/pillars/RiskFamilyCard.tsx
// Carte d'une famille de risques (Réseau, Sécurité, Humain, Logiciel)
// 4 cartes en grille en haut de page
```
### 5.2 Architecture de fichiers
```
src/
├── app/
│ └── synthese/page.tsx ← nouvelle route
├── components/
│ ├── pillars/
│ │ ├── PillarsDiagram.tsx
│ │ ├── PillarSection.tsx
│ │ ├── CoverageMatrix.tsx
│ │ └── RiskFamilyCard.tsx
│ └── ui/
│ └── PillarBadge.tsx ← réutilisable dans toutes les pages
├── data/
│ ├── pillars.ts ← PILLARS array + types
│ └── jeep.ts ← ajout du champ pillars[]
└── types/
└── pillars.ts ← PillarKey, RiskFamily, Pillar
```
### 5.3 Suggestion pour le diagramme central
Approche recommandée : **hexagone SVG** avec les 5 piliers préventifs en sommets (5 sur 6 sommets occupés), et la *Supervision* matérialisée par un anneau extérieur entourant l'ensemble — visuellement, elle « englobe » les autres.
Alternative plus simple : **grille de 6 cartes** (3×2), la carte Supervision marquée d'un badge spécial `TRANSVERSAL` et reliée visuellement aux autres par des lignes pointillées.
Choisis selon la complexité acceptable. La grille fonctionne très bien et reste lisible sur mobile ; le SVG hexagonal est plus impressionnant mais plus de boulot à rendre propre en responsive.
---
## 6. Données supplémentaires à ajouter dans `jeep.ts`
```ts
// Ajouter à l'objet CASES.jeep
synthesis: {
pillarsStatus: {
chiffrement: {
status: "failed", // "failed" | "partial" | "ok"
severity: "major",
relatedItems: ["R2", "AMD-02"],
lesson: "Tout port ouvert sur Internet doit imposer du TLS mutuel...",
},
identites: {
status: "failed",
severity: "critical",
relatedItems: ["R2", "R4", "AMD-02", "AMD-05", "CVE-2015-5611"],
lesson: "Le principe de moindre privilège n'est pas un concept SI seulement...",
},
segmentation: {
status: "failed",
severity: "critical",
relatedItems: ["R1", "R4", "AMD-01", "AMD-04", "AMD-05"],
lesson: "La segmentation est la mesure qui contient le rayon d'explosion...",
},
hardening: {
status: "failed",
severity: "major",
relatedItems: ["R2", "R3", "R5", "AMD-02", "AMD-03"],
lesson: "Le hardening est un travail ingrat mais payant...",
},
maj: {
status: "absent", // ce n'est pas "défaillant", c'est carrément absent
severity: "critical",
relatedItems: [], // pas d'item AMDEC/Risk, traité dans la page Continuité
relatedPage: "/continuite",
lesson: "Un système IoT sans capacité de mise à jour à distance...",
},
supervision: {
status: "failed",
severity: "critical",
isTransversal: true,
relatedItems: "all", // ← cas spécial, voir la matrice
lesson: "La supervision est la condition de possibilité des autres piliers...",
},
},
}
```
---
## 7. Mise à jour de la checklist finale
Ajouter ces vérifications à la checklist du `PROJECT_BRIEF.md` :
- [ ] Route `/synthese` fonctionnelle (6ème onglet)
- [ ] `PILLARS` constant exporté avec les 6 piliers exacts du cours
- [ ] `supervision` marqué `isTransversal: true` dans les données
- [ ] Champ `pillars` ajouté à chaque risque (R1-R5) avec les bonnes valeurs (voir §2.2)
- [ ] Champ `pillars` ajouté à chaque AMDEC row (AMD-01 à 05) avec les bonnes valeurs (voir §2.3)
- [ ] `<PillarBadge>` affiché à côté de chaque risque (page 2) et chaque ligne AMDEC (page 3)
- [ ] Page Synthèse : briefing intro + 4 familles de risques en cartes
- [ ] Page Synthèse : diagramme central des 6 piliers (avec Supervision distinguée)
- [ ] Page Synthèse : 6 sections expandables (une par pilier) avec contenu narratif verbatim
- [ ] Page Synthèse : tableau de couverture piliers × items (matrice)
- [ ] Page Synthèse : conclusion finale dans un encadré marquant
- [ ] Chips items (R1, AMD-XX) cliquables → renvoient vers la page correspondante avec ancre
- [ ] Tooltip au survol de chaque PillarBadge (label complet)
---
## 8. Notes finales pour Claude Code
### À faire en priorité
- Implémenter d'abord `pillars.ts` (data + types) et `PillarBadge.tsx` (composant réutilisable)
- Puis injecter le tagging dans `jeep.ts` pour les risques et AMDEC
- Ajouter le `<PillarBadge>` dans les pages existantes (page 2 et 3) — minimaliste, pas d'invasion visuelle
- En dernier, créer la page `/synthese` avec son contenu complet
### À ne pas faire
- ❌ Ne pas modifier les hypothèses, mitigations, scores existants — on ne fait qu'**ajouter** les piliers
- ❌ Ne pas inventer d'autre pilier — il y en a exactement 6, ceux du cours
- ❌ Ne pas traduire les libellés des piliers en anglais (le contenu est en français)
- ❌ Ne pas ajouter de pilier en sus type "Privacy" ou "Supply chain" — ce n'est pas le framework du cours
- ❌ Ne pas marquer la MAJ comme défaillante sur la matrice de couverture page 2/3 — elle est absente, pas défaillante. Le tableau de couverture le reflète (ligne MAJ vide)
### Si Claude Code propose des améliorations
Réponses pré-cadrées que tu peux donner :
- *"On peut ajouter d'autres piliers ?"* → Non, le framework est figé à 6.
- *"On peut faire la diagramme animé ?"* → Animation simple ok (fade-in, hover), mais pas de framework type Framer Motion.
- *"On change la couleur d'un pilier ?"* → Tu peux ajuster les hex pour que ça matche un design system existant, mais conserve une couleur distincte par pilier.
- *"On fusionne Hardening et Chiffrement ?"* → Non, ce sont deux piliers distincts dans le framework du cours.
---
> Fin de l'update brief. Une fois ces ajouts terminés, le site couvre l'intégralité du framework pédagogique du cours (Dossier → Risques → AMDEC → CVE → Continuité → Synthèse 6 piliers).

21
eslint.config.js Normal file
View file

@ -0,0 +1,21 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
])

16
index.html Normal file
View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IoT Risk Briefing — Jeep Cherokee 2015</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2050
iot_risk_briefing.jsx Normal file

File diff suppressed because it is too large Load diff

2737
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "temp-vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
"lucide-react": "^1.16.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"vite": "^8.0.12"
}
}

6
public/favicon.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="#09090b"/>
<path d="M16 6l-8 4.5v7L16 22l8-4.5v-7L16 6z" fill="none" stroke="#fbbf24" stroke-width="1.5" stroke-linejoin="round"/>
<circle cx="16" cy="14" r="3" fill="none" stroke="#ef4444" stroke-width="1.5"/>
<line x1="16" y1="17" x2="16" y2="22" stroke="#ef4444" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 405 B

24
public/icons.svg Normal file
View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

141
src/App.jsx Normal file
View file

@ -0,0 +1,141 @@
import { useMemo } from 'react'
import { useMousePosition } from './hooks.js'
import IoTRiskBriefing from './IoTRiskBriefing.jsx'
function MouseSpotlight() {
const { x, y } = useMousePosition()
return (
<div
className="mouse-spotlight"
style={{ left: x, top: y, opacity: x === 0 && y === 0 ? 0 : 1 }}
/>
)
}
function Particles({ count = 18 }) {
const particles = useMemo(() =>
Array.from({ length: count }, (_, i) => ({
id: i,
left: `${5 + Math.random() * 90}%`,
delay: `${Math.random() * 15}s`,
duration: `${10 + Math.random() * 18}s`,
size: `${1 + Math.random() * 2}px`,
opacity: 0.15 + Math.random() * 0.3,
driftX: `${-20 + Math.random() * 40}px`,
})),
[count]
)
return (
<>
{particles.map((p) => (
<div
key={p.id}
className="particle"
style={{
left: p.left,
bottom: '-10px',
width: p.size,
height: p.size,
opacity: p.opacity,
animationDelay: p.delay,
animationDuration: p.duration,
'--drift-x': p.driftX,
}}
/>
))}
</>
)
}
function MorphBlobs() {
return (
<>
<div
className="morph-blob"
style={{
top: '5%',
right: '-8%',
width: '450px',
height: '450px',
background: 'rgba(251, 191, 36, 0.02)',
}}
/>
<div
className="morph-blob"
style={{
bottom: '15%',
left: '-10%',
width: '400px',
height: '400px',
background: 'rgba(239, 68, 68, 0.015)',
animationDelay: '-7s',
animationDuration: '22s',
}}
/>
<div
className="morph-blob"
style={{
top: '45%',
left: '35%',
width: '350px',
height: '350px',
background: 'rgba(99, 102, 241, 0.012)',
animationDelay: '-12s',
animationDuration: '25s',
}}
/>
</>
)
}
function CircuitTraces() {
const traces = useMemo(() =>
Array.from({ length: 5 }, (_, i) => ({
id: i,
left: `${15 + Math.random() * 70}%`,
width: `${0.5 + Math.random() * 0.5}px`,
height: `${150 + Math.random() * 250}px`,
delay: `${Math.random() * 5}s`,
duration: `${5 + Math.random() * 5}s`,
top: `${Math.random() * 70}%`,
})),
[]
)
return (
<div className="circuit-traces">
{traces.map((t) => (
<div
key={t.id}
className="circuit-trace"
style={{
left: t.left,
top: t.top,
width: t.width,
height: t.height,
animationDelay: t.delay,
animationDuration: t.duration,
}}
/>
))}
</div>
)
}
export default function App() {
return (
<>
<div className="bg-grid" />
<MorphBlobs />
<CircuitTraces />
<div className="scan-line" />
<Particles count={18} />
<MouseSpotlight />
<div className="noise-overlay" />
<div className="relative z-10">
<IoTRiskBriefing />
</div>
</>
)
}

3433
src/IoTRiskBriefing.jsx Normal file

File diff suppressed because it is too large Load diff

159
src/hooks.js Normal file
View file

@ -0,0 +1,159 @@
import { useState, useEffect, useRef, useCallback } from "react";
export function useScrollReveal(options = {}) {
const ref = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(el);
}
},
{ threshold: options.threshold ?? 0.15, rootMargin: options.rootMargin ?? "0px" }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return [ref, isVisible];
}
export function useTilt(intensity = 8) {
const ref = useRef(null);
const handleMove = useCallback(
(e) => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
el.style.transform = `perspective(800px) rotateY(${x * intensity}deg) rotateX(${-y * intensity}deg) scale3d(1.02, 1.02, 1.02)`;
el.style.transition = "transform 0.1s ease-out";
},
[intensity]
);
const handleLeave = useCallback(() => {
const el = ref.current;
if (!el) return;
el.style.transform = "perspective(800px) rotateY(0deg) rotateX(0deg) scale3d(1, 1, 1)";
el.style.transition = "transform 0.5s ease-out";
}, []);
useEffect(() => {
const el = ref.current;
if (!el) return;
el.addEventListener("mousemove", handleMove);
el.addEventListener("mouseleave", handleLeave);
return () => {
el.removeEventListener("mousemove", handleMove);
el.removeEventListener("mouseleave", handleLeave);
};
}, [handleMove, handleLeave]);
return ref;
}
export function useCountUp(target, duration = 1800, startOnVisible = true) {
const [value, setValue] = useState(0);
const [started, setStarted] = useState(!startOnVisible);
const ref = useRef(null);
useEffect(() => {
if (!startOnVisible) return;
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setStarted(true);
observer.unobserve(el);
}
},
{ threshold: 0.3 }
);
observer.observe(el);
return () => observer.disconnect();
}, [startOnVisible]);
useEffect(() => {
if (!started) return;
const num = parseFloat(String(target).replace(/[^0-9.]/g, ""));
if (isNaN(num) || num === 0) {
setValue(target);
return;
}
const startTime = performance.now();
const step = (now) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setValue(Math.floor(num * eased));
if (progress < 1) requestAnimationFrame(step);
else setValue(num);
};
requestAnimationFrame(step);
}, [started, target, duration]);
return [ref, value];
}
export function useMousePosition() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return pos;
}
export function useRipple() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const handler = (e) => {
const rect = el.getBoundingClientRect();
const ripple = document.createElement("span");
const size = Math.max(rect.width, rect.height) * 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${e.clientX - rect.left - size / 2}px;
top: ${e.clientY - rect.top - size / 2}px;
background: radial-gradient(circle, rgba(251,191,36,0.3) 0%, transparent 70%);
border-radius: 50%;
transform: scale(0);
animation: ripple-expand 0.6s ease-out forwards;
pointer-events: none;
z-index: 0;
`;
el.style.position = "relative";
el.style.overflow = "hidden";
el.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
};
el.addEventListener("click", handler);
return () => el.removeEventListener("click", handler);
}, []);
return ref;
}

491
src/index.css Normal file
View file

@ -0,0 +1,491 @@
@import "tailwindcss";
@theme {
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-mono: 'DM Mono', ui-monospace, Consolas, monospace;
--font-display: 'Space Grotesk', system-ui, sans-serif;
}
/*
DESIGN TOKENS SIGINT Terminal
*/
:root {
/* Base surfaces — deep navy, NOT pure black */
--surface-0: #060810;
--surface-1: #0b0d18;
--surface-2: #111422;
--surface-3: #171a2c;
--surface-4: #1e2238;
/* Card surfaces — VISIBLE elevation */
--card-bg: #131628;
--card-bg-strong: #161a30;
--card-bg-hover: #1a1e36;
--card-border: rgba(140, 150, 255, 0.08);
--card-border-hover: rgba(140, 150, 255, 0.16);
/* Borders */
--border-subtle: rgba(140, 150, 255, 0.06);
--border-default: rgba(140, 150, 255, 0.10);
--border-strong: rgba(140, 150, 255, 0.18);
/* Accent glow colors — VISIBLE */
--glow-amber: rgba(251, 191, 36, 0.35);
--glow-amber-soft: rgba(251, 191, 36, 0.12);
--glow-red: rgba(248, 80, 80, 0.30);
--glow-green: rgba(52, 211, 153, 0.30);
}
/*
KEYFRAMES
*/
@keyframes grid-scroll {
0% { transform: translateY(0); }
100% { transform: translateY(60px); }
}
@keyframes pulse-glow {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
@keyframes scan-line {
0% { top: -2px; }
100% { top: 100%; }
}
@keyframes fadein {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadein-scale {
from { opacity: 0; transform: scale(0.96) translateY(10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes slide-up-big {
from { opacity: 0; transform: translateY(28px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes border-glow {
0%, 100% { box-shadow: 0 0 0 rgba(251, 191, 36, 0), 0 4px 24px rgba(0,0,0,0.4); }
50% { box-shadow: 0 0 28px rgba(251, 191, 36, 0.12), 0 4px 24px rgba(0,0,0,0.4); }
}
@keyframes radar-sweep {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes particle-drift {
0% { transform: translateY(0) translateX(0) scale(0); opacity: 0; }
5% { transform: scale(1); opacity: 0.7; }
50% { opacity: 0.4; }
100% { transform: translateY(-100vh) translateX(var(--drift-x, 20px)) scale(0); opacity: 0; }
}
@keyframes ripple-expand {
from { transform: scale(0); opacity: 1; }
to { transform: scale(1); opacity: 0; }
}
@keyframes glitch-1 {
0%, 100% { clip-path: inset(0); transform: translate(0); }
20% { clip-path: inset(20% 0 60% 0); transform: translate(-2px, 1px); }
40% { clip-path: inset(40% 0 30% 0); transform: translate(2px, -1px); }
60% { clip-path: inset(70% 0 10% 0); transform: translate(-1px, 1px); }
80% { clip-path: inset(10% 0 80% 0); transform: translate(1px, -1px); }
}
@keyframes glitch-2 {
0%, 100% { clip-path: inset(0); transform: translate(0); }
20% { clip-path: inset(60% 0 10% 0); transform: translate(2px, -1px); }
40% { clip-path: inset(10% 0 70% 0); transform: translate(-2px, 1px); }
60% { clip-path: inset(30% 0 40% 0); transform: translate(1px, -1px); }
80% { clip-path: inset(80% 0 5% 0); transform: translate(-1px, 1px); }
}
@keyframes glitch-skew {
0%, 100% { transform: skewX(0deg); }
30% { transform: skewX(-0.7deg); }
70% { transform: skewX(0.4deg); }
}
@keyframes pulse-ring {
0% { transform: scale(0.95); opacity: 0.5; }
100% { transform: scale(1.5); opacity: 0; }
}
@keyframes morph-blob {
0%, 100% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; transform: rotate(0deg) scale(1); }
25% { border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; transform: rotate(90deg) scale(1.05); }
50% { border-radius: 50% 60% 30% 60% / 30% 60% 70% 40%; transform: rotate(180deg) scale(0.97); }
75% { border-radius: 60% 30% 60% 40% / 70% 40% 50% 60%; transform: rotate(270deg) scale(1.03); }
}
@keyframes data-flow {
0% { background-position: 0% 0%; }
100% { background-position: 0% 100%; }
}
@keyframes scroll-reveal {
from { opacity: 0; transform: translateY(20px); filter: blur(2px); }
to { opacity: 1; transform: translateY(0); filter: blur(0); }
}
@keyframes score-pop {
0% { transform: scale(0.5); opacity: 0; }
60% { transform: scale(1.08); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes warning-flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes accent-line-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
/*
BASE & BODY
*/
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
background: var(--surface-0);
color: #c8cdd8;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
letter-spacing: 0.01em;
line-height: 1.6;
}
::selection {
background: rgba(251, 191, 36, 0.25);
color: #fff;
}
/* Scrollbar — amber-tinted */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--surface-0); }
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(251,191,36,0.15), rgba(251,191,36,0.06));
border-radius: 99px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(251,191,36,0.3), rgba(251,191,36,0.12));
}
/*
TYPOGRAPHY
*/
.font-display {
font-family: 'Space Grotesk', system-ui, sans-serif;
letter-spacing: -0.02em;
}
/*
ANIMATION UTILITIES
*/
.animate-fadein { animation: fadein 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; }
.animate-fadein-scale { animation: fadein-scale 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; }
.animate-slide-up-big { animation: slide-up-big 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
.animate-float { animation: float 3.5s ease-in-out infinite; }
.animate-score-pop { animation: score-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
.animate-warning-flash { animation: warning-flash 2.5s ease-in-out infinite; }
.animate-shimmer {
background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.05), transparent);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
.animate-border-glow { animation: border-glow 3s ease-in-out infinite; }
.scroll-hidden { opacity: 0; transform: translateY(20px); filter: blur(2px); }
.scroll-visible { animation: scroll-reveal 0.65s cubic-bezier(0.16, 1, 0.3, 1) both; }
.stagger-children > * { animation: fadein 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; }
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
.stagger-children > *:nth-child(2) { animation-delay: 60ms; }
.stagger-children > *:nth-child(3) { animation-delay: 120ms; }
.stagger-children > *:nth-child(4) { animation-delay: 180ms; }
.stagger-children > *:nth-child(5) { animation-delay: 240ms; }
.stagger-children > *:nth-child(6) { animation-delay: 300ms; }
.stagger-children > *:nth-child(7) { animation-delay: 360ms; }
.stagger-children > *:nth-child(8) { animation-delay: 420ms; }
.stagger-children > *:nth-child(9) { animation-delay: 480ms; }
.stagger-children > *:nth-child(10) { animation-delay: 540ms; }
/*
BACKGROUND LAYERS
*/
.bg-grid {
position: fixed; inset: 0; z-index: 0;
pointer-events: none; overflow: hidden;
}
.bg-grid::before {
content: '';
position: absolute; inset: -60px;
background-image:
linear-gradient(rgba(160, 170, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(160, 170, 255, 0.025) 1px, transparent 1px);
background-size: 60px 60px;
animation: grid-scroll 12s linear infinite;
mask-image: radial-gradient(ellipse at 50% 25%, black 20%, transparent 60%);
-webkit-mask-image: radial-gradient(ellipse at 50% 25%, black 20%, transparent 60%);
}
.bg-grid::after {
content: '';
position: absolute; inset: 0;
background:
radial-gradient(ellipse at 15% 0%, rgba(251, 191, 36, 0.08) 0%, transparent 40%),
radial-gradient(ellipse at 85% 90%, rgba(248, 80, 80, 0.04) 0%, transparent 35%),
radial-gradient(ellipse at 50% 40%, rgba(100, 110, 240, 0.03) 0%, transparent 45%);
animation: pulse-glow 8s ease-in-out infinite;
}
.mouse-spotlight {
position: fixed;
width: clamp(250px, 35vw, 500px);
height: clamp(250px, 35vw, 500px);
border-radius: 50%;
pointer-events: none; z-index: 1;
transition: left 0.3s cubic-bezier(0.16, 1, 0.3, 1), top 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.5s;
background: radial-gradient(circle, rgba(251, 191, 36, 0.05) 0%, rgba(251, 191, 36, 0.015) 35%, transparent 65%);
transform: translate(-50%, -50%);
}
.scan-line {
position: fixed; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.14) 40%, rgba(251, 191, 36, 0.14) 60%, transparent);
z-index: 1; pointer-events: none;
animation: scan-line 7s linear infinite;
}
.particle {
position: fixed; border-radius: 50%;
pointer-events: none; z-index: 1;
animation: particle-drift linear infinite;
background: rgba(251, 191, 36, 0.5);
box-shadow: 0 0 8px rgba(251, 191, 36, 0.25);
}
.morph-blob {
position: fixed; pointer-events: none; z-index: 0;
animation: morph-blob 20s ease-in-out infinite;
filter: blur(100px);
}
.circuit-traces { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
.circuit-trace {
position: absolute;
background: linear-gradient(180deg, transparent, rgba(251, 191, 36, 0.05), transparent);
animation: data-flow 6s linear infinite;
background-size: 100% 200%;
}
.noise-overlay {
position: fixed; inset: 0; z-index: 2;
pointer-events: none; opacity: 0.025;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 200px 200px;
}
/*
SURFACE SYSTEM THE KEY CHANGE
Cards are CLEARLY ELEVATED above background.
Navy-indigo card on deep space background = visible.
*/
.glass {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
box-shadow:
0 2px 12px rgba(0,0,0,0.35),
0 0 0 1px rgba(0,0,0,0.15) inset,
0 1px 0 rgba(255,255,255,0.04) inset;
transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
.glass:hover {
background: var(--card-bg-hover);
border-color: var(--card-border-hover);
}
.glass-strong {
background: linear-gradient(160deg, var(--card-bg-strong) 0%, var(--card-bg) 100%);
border: 1px solid var(--border-strong);
border-radius: 20px;
box-shadow:
0 10px 50px rgba(0,0,0,0.5),
0 1px 0 rgba(255,255,255,0.06) inset,
0 0 80px rgba(100, 110, 240, 0.02);
}
.glass-subtle {
background: rgba(15, 17, 28, 0.8);
border: 1px solid var(--border-subtle);
border-radius: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
/*
INTERACTIVE
*/
.hover-lift {
border-radius: 16px;
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.35s ease, border-color 0.3s ease, background 0.3s ease;
}
.hover-lift:hover {
transform: translateY(-3px);
box-shadow:
0 20px 60px rgba(0,0,0,0.5),
0 0 0 1px rgba(251, 191, 36, 0.1),
0 0 40px rgba(251, 191, 36, 0.04);
border-color: rgba(251, 191, 36, 0.15) !important;
}
.tilt-card {
transform-style: preserve-3d;
will-change: transform;
border-radius: 16px;
}
.glow-hover { transition: text-shadow 0.4s ease, color 0.3s ease; }
.glow-hover:hover {
text-shadow: 0 0 20px rgba(251, 191, 36, 0.4), 0 0 50px rgba(251, 191, 36, 0.12);
color: #fef3c7;
}
.press-effect {
transition: transform 0.15s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease, box-shadow 0.2s ease;
position: relative; overflow: hidden;
border-radius: 12px;
}
.press-effect:active { transform: scale(0.97); }
.glitch-text { position: relative; display: inline-block; }
.glitch-text::before, .glitch-text::after {
content: attr(data-text);
position: absolute; top: 0; left: 0;
width: 100%; height: 100%; opacity: 0;
}
.glitch-text:hover::before { animation: glitch-1 0.25s ease-in-out; color: #ff4444; opacity: 0.7; z-index: -1; }
.glitch-text:hover::after { animation: glitch-2 0.25s ease-in-out 0.03s; color: #34d399; opacity: 0.7; z-index: -1; }
.glitch-text:hover { animation: glitch-skew 0.25s ease-in-out; }
.pulse-ring { position: relative; }
.pulse-ring::after {
content: ''; position: absolute; inset: -4px;
border: 1px solid currentColor; border-radius: inherit;
opacity: 0; animation: pulse-ring 3s ease-out infinite;
}
.line-reveal { position: relative; overflow: hidden; }
.line-reveal::after {
content: ''; position: absolute; bottom: 0; left: 0;
width: 100%; height: 2px;
background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.5), transparent);
transform: translateX(-100%);
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.line-reveal:hover::after { transform: translateX(0); }
.radar-container {
position: relative; overflow: hidden; border-radius: 16px;
background: var(--card-bg);
}
.radar-container::before {
content: ''; position: absolute; top: 50%; left: 50%;
width: 150%; height: 150%; transform-origin: 0 0;
background: conic-gradient(from 0deg, transparent 0deg, rgba(251, 191, 36, 0.06) 25deg, transparent 50deg);
animation: radar-sweep 10s linear infinite;
pointer-events: none; z-index: 0;
}
/*
ACCENT EDGES VISIBLE luminous lines
*/
.accent-edge { position: relative; overflow: hidden; }
.accent-edge::before {
content: '';
position: absolute; top: 0; left: 5%; right: 5%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--glow-amber), var(--glow-amber), transparent);
z-index: 2;
border-radius: 0 0 2px 2px;
}
.accent-edge::after {
content: '';
position: absolute; top: 0; left: 15%; right: 15%;
height: 12px;
background: radial-gradient(ellipse at 50% 0%, rgba(251, 191, 36, 0.08) 0%, transparent 100%);
z-index: 1;
pointer-events: none;
}
.accent-edge-red::before {
background: linear-gradient(90deg, transparent, var(--glow-red), var(--glow-red), transparent);
}
.accent-edge-red::after {
background: radial-gradient(ellipse at 50% 0%, rgba(248, 80, 80, 0.06) 0%, transparent 100%);
}
.accent-edge-green::before {
background: linear-gradient(90deg, transparent, var(--glow-green), var(--glow-green), transparent);
}
.accent-edge-green::after {
background: radial-gradient(ellipse at 50% 0%, rgba(52, 211, 153, 0.06) 0%, transparent 100%);
}
/*
PAGE LAYOUT Rich background with depth
*/
.page-wrapper {
min-height: 100vh;
background:
radial-gradient(ellipse at 50% -5%, rgba(251, 191, 36, 0.05) 0%, transparent 30%),
radial-gradient(ellipse at 0% 30%, rgba(80, 90, 200, 0.025) 0%, transparent 30%),
radial-gradient(ellipse at 100% 70%, rgba(248, 80, 80, 0.015) 0%, transparent 30%),
linear-gradient(180deg, #0c0e1a 0%, var(--surface-0) 12%, var(--surface-0) 100%);
}
.page-inner {
max-width: 78rem;
margin: 0 auto;
padding: 1rem;
}
@media (min-width: 640px) { .page-inner { padding: 1.25rem 1.5rem; } }
@media (min-width: 768px) { .page-inner { padding: 2rem 2.5rem; } }
@media (min-width: 1024px) { .page-inner { padding: 2.5rem 3.5rem; } }
@media (min-width: 1280px) { .page-inner { padding: 3rem 4rem; } }

10
src/main.jsx Normal file
View file

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

7
vite.config.js Normal file
View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})