diff --git a/.gitignore b/.gitignore index b56b358..9cc388c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,14 @@ *.jpeg # Exceptions : on autorise les images de documentation !docs/**/*.png +# Exceptions : les gabarits du site (logos Insee, pictogrammes, licences CC) +# ne sont pas des données élèves — sans ça, le site ne se construit plus. +!website/slides/_extensions/**/*.png +!website/slides/_extensions/**/*.jpg +!website/slides/_extensions/**/*.jpeg +!website/slides/img/**/*.png +!website/slides/img/**/*.jpg +!website/img/**/*.png # ─── MLflow (suivi local, ne pas versionner) ─────────────────── mlflow.db diff --git a/CLAUDE.md b/CLAUDE.md index 1fa697e..f6374d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,6 +160,14 @@ uv run mypy src # typage # Lancer un benchmark à partir d'une config uv run scripts/run_benchmark.py --config configs/scoring/dictee_REFERENCE.yaml + +# Densité d'encre de chaque copie (détection des copies vierges, sans appel modèle). +# Documente `data.blank_ink_threshold` et alimente la page « Écarts » du site. +uv run scripts/compute_ink_ratios.py --config configs/scoring/dictee_end2end.yaml --export + +# Rendre le site Quarto (les pages recalculent leurs figures au rendu) +uv sync --extra website +uv run quarto render website ``` ### Runs longs — utiliser screen ou nohup diff --git a/pyproject.toml b/pyproject.toml index 0ee8b3c..8d792e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,14 @@ notebooks = [ "jupyterlab>=4.6.1", "matplotlib>=3.11.1", ] +# Rendu du site Quarto : les pages recalculent leurs figures au rendu, ce qui +# demande matplotlib et un noyau Jupyter (`uv sync --extra website`). +website = [ + "ipykernel>=6.29", + "matplotlib>=3.11.1", + "nbclient>=0.10", + "nbformat>=5.10", +] # Outillage de développement (lint, typage, tests) — NON publié avec le paquet. # Le groupe `dev` est installé automatiquement par un simple `uv sync`. diff --git a/scripts/compute_ink_ratios.py b/scripts/compute_ink_ratios.py new file mode 100644 index 0000000..fc14957 --- /dev/null +++ b/scripts/compute_ink_ratios.py @@ -0,0 +1,168 @@ +"""Mesure la densité d'encre de chaque copie et exporte la distribution. + +À quoi ça sert : le benchmark déclare une copie VIERGE quand sa densité d'encre +passe sous `data.blank_ink_threshold`, et code alors tous ses items « 0 » sans +appeler le modèle. Ce script calcule la même mesure pour **tout le corpus**, sans +appel modèle ni GPU, afin de : + +- documenter le seuil (le site trace la distribution obtenue, voir + `website/ecarts.qmd`) ; +- rejuger ce seuil en quelques minutes, sans relancer un benchmark de 30 h. + +La sortie ne contient **aucune donnée d'élève** : un identifiant de copie et un +scalaire par ligne. Elle vit malgré tout dans `data/processed/` (ignoré par Git) +et sur S3, comme les prédictions. + +Usage : + # Depuis le YAML d'un run (reprend son corpus et son seuil) : + uv run scripts/compute_ink_ratios.py --config configs/scoring/dictee_end2end.yaml + + # Ou en désignant directement les données : + uv run scripts/compute_ink_ratios.py \ + --images-path s3://projet-production-ecrits-depp/dictee_2015/ \ + --labels-path s3://projet-production-ecrits-depp/resultat_dictee_2015.csv + + # Puis pousser sur S3 pour que le site le relise : + uv run scripts/compute_ink_ratios.py --config configs/... --export +""" + +from __future__ import annotations + +import argparse +import csv +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +import fsspec + +from evaluation_dictee.config import ExperimentConfig, Secrets, load_config +from evaluation_dictee.data.loaders import Copy, ink_ratio, load_dataset, load_image +from evaluation_dictee.utils.logging import get_logger + +logger = get_logger(__name__) + +#: Nom du fichier produit, dérivé du corpus (et non du run) : la densité d'encre +#: ne dépend ni du modèle ni de l'approche. +NOM_FICHIER = "{corpus}_ink_ratios.csv" + + +def _mesurer(copy: Copy) -> tuple[str, float | None]: + """Densité d'encre d'une copie ; None si l'image est illisible.""" + try: + return copy.copy_id, ink_ratio(load_image(copy.image_path)) + except Exception as exc: # noqa: BLE001 — une image cassée ne doit pas tout arrêter + logger.warning("%s illisible (%s) : ignorée.", copy.copy_id, type(exc).__name__) + return copy.copy_id, None + + +def mesurer_corpus(copies: list[Copy], workers: int = 16) -> list[tuple[str, float]]: + """Mesure la densité d'encre de toutes les copies, en parallèle. + + Args: + copies: copies à mesurer. + workers: nombre de threads (le coût est dominé par les entrées/sorties S3). + + Returns: + La liste des couples (copy_id, densité), triée par identifiant, privée des + copies dont l'image n'a pas pu être lue. + """ + with ThreadPoolExecutor(max_workers=workers) as pool: + resultats = list(pool.map(_mesurer, copies)) + return sorted((cid, r) for cid, r in resultats if r is not None) + + +def ecrire_csv(mesures: list[tuple[str, float]], chemin: str | Path, seuil: float) -> Path: + """Écrit les mesures en CSV (`copy_id;ink_ratio;blank`). + + Args: + mesures: couples (copy_id, densité d'encre). + chemin: fichier de destination. + seuil: seuil au-dessous duquel la copie est marquée vierge. + + Returns: + Le chemin écrit. + """ + sortie = Path(chemin) + sortie.parent.mkdir(parents=True, exist_ok=True) + with sortie.open("w", encoding="utf-8", newline="") as f: + writer = csv.writer(f, delimiter=";") + writer.writerow(["copy_id", "ink_ratio", "blank"]) + for copy_id, densite in mesures: + writer.writerow([copy_id, f"{densite:.6f}", int(densite < seuil)]) + return sortie + + +def _resoudre_source(args: argparse.Namespace) -> tuple[str, str, str, float, int | None]: + """Résout (images, labels, corpus, seuil, limite) depuis le YAML ou les options.""" + if args.config: + config: ExperimentConfig = load_config(args.config) + return ( + config.data.images_path, + config.data.labels_path, + config.data.corpus, + config.data.blank_ink_threshold, + args.limit if args.limit is not None else config.data.limit, + ) + if not (args.images_path and args.labels_path): + raise SystemExit("Sans --config, --images-path ET --labels-path sont requis.") + return (args.images_path, args.labels_path, args.corpus, args.threshold, args.limit) + + +def main() -> None: + """Mesure la densité d'encre du corpus, écrit le CSV et l'exporte si demandé.""" + parser = argparse.ArgumentParser( + description="Mesure la densité d'encre de chaque copie (détection des copies vierges)." + ) + parser.add_argument("--config", help="YAML d'un run : corpus, chemins et seuil en sont lus.") + parser.add_argument("--images-path", help="Dossier des images (local ou s3://).") + parser.add_argument("--labels-path", help="CSV des codes experts (local ou s3://).") + parser.add_argument("--corpus", default="dictee", help="Nom du corpus. [défaut : dictee]") + parser.add_argument( + "--threshold", + type=float, + default=0.025, + help="Seuil de copie vierge, si absent du YAML. [défaut : 0.025]", + ) + parser.add_argument("--limit", type=int, default=None, help="Limiter le nombre de copies.") + parser.add_argument("--workers", type=int, default=16, help="Threads de lecture. [défaut : 16]") + parser.add_argument( + "--output-dir", + default="data/processed", + help="Dossier local de sortie. [défaut : data/processed]", + ) + parser.add_argument( + "--export", + action="store_true", + help="Pousse aussi le CSV sur S3, où le site Quarto le relit.", + ) + args = parser.parse_args() + + images, labels, corpus, seuil, limite = _resoudre_source(args) + copies = load_dataset(images, labels, limit=limite) + logger.info("%d copies à mesurer (seuil de copie vierge : %.4f).", len(copies), seuil) + + mesures = mesurer_corpus(copies, workers=args.workers) + n_vierges = sum(1 for _, densite in mesures if densite < seuil) + logger.info( + "%d copies mesurées, dont %d vierges (%.2f %%).", + len(mesures), + n_vierges, + n_vierges / len(mesures) * 100 if mesures else 0.0, + ) + + nom = NOM_FICHIER.format(corpus=corpus) + local = ecrire_csv(mesures, Path(args.output_dir) / nom, seuil) + logger.info("Écrit : %s", local) + + if args.export: + dest = Secrets().s3_predictions_prefix.rstrip("/") + "/" + nom + with ( + local.open("rb") as src, + fsspec.open(dest, "wb") as dst, + ): + dst.write(src.read()) + logger.info("OK — disponible sur S3 : %s", dest) + + +if __name__ == "__main__": + main() diff --git a/src/evaluation_dictee/evaluation/multi_model.py b/src/evaluation_dictee/evaluation/multi_model.py index e549c34..396e726 100644 --- a/src/evaluation_dictee/evaluation/multi_model.py +++ b/src/evaluation_dictee/evaluation/multi_model.py @@ -7,9 +7,9 @@ from __future__ import annotations -from collections import Counter from pathlib import Path +import numpy as np import pandas as pd from evaluation_dictee.evaluation.report import load_predictions @@ -83,14 +83,17 @@ def agreement_per_item(df_multi: pd.DataFrame) -> pd.DataFrame: out = df_multi.copy() - def _modal_and_count(row): - counts = Counter(row[c] for c in pred_cols) - modal, n_acc = counts.most_common(1)[0] - return pd.Series({"modal_pred": modal, "n_accord_modeles": n_acc}) - - modal_df = out[pred_cols].apply(_modal_and_count, axis=1) - out["modal_pred"] = modal_df["modal_pred"] - out["n_accord_modeles"] = modal_df["n_accord_modeles"].astype(int) + # Vote majoritaire vectorisé : `effectifs[:, i]` compte les runs qui prédisent + # la même chose que le run i, ligne par ligne. Le premier maximum gagne, ce qui + # départage les ex æquo par ordre d'apparition (comme Counter.most_common). + # Version vectorisée indispensable ici : un apply(axis=1) sur les ~290 000 + # items d'un run complet coûte une minute, contre une fraction de seconde ici. + preds = out[pred_cols].to_numpy(dtype=object) + effectifs = np.column_stack([(preds == preds[:, [i]]).sum(axis=1) for i in range(n_modeles)]) + lignes = np.arange(len(preds)) + gagnant = effectifs.argmax(axis=1) if len(preds) else lignes + out["modal_pred"] = preds[lignes, gagnant] + out["n_accord_modeles"] = effectifs[lignes, gagnant].astype(int) out["n_modeles"] = n_modeles out["unanimite"] = out["n_accord_modeles"] == n_modeles diff --git a/src/evaluation_dictee/evaluation/statistics.py b/src/evaluation_dictee/evaluation/statistics.py index a1c7999..8239fff 100644 --- a/src/evaluation_dictee/evaluation/statistics.py +++ b/src/evaluation_dictee/evaluation/statistics.py @@ -1,19 +1,38 @@ -"""Intervalle de Wilson (proportions) et bootstrap par grappes. +"""Intervalles de confiance tenant compte du regroupement des items par copie. -Le bootstrap rééchantillonne les COPIES entières, pas les items : les 83 items -d'une copie ne sont pas indépendants, un bootstrap par item sous-estimerait -l'incertitude. +Fil directeur : les 83 items d'une copie ne sont pas indépendants (un élève faible +se trompe partout), donc N items portent moins d'information que N observations +indépendantes. Ignorer ce fait produit des intervalles trop étroits. + +Deux façons d'en tenir compte, selon la statistique : + +- **une proportion** (accord, rappel, précision…) : `wilson_interval` avec le + `design_effect` de l'indicatrice concernée. Instantané. Attention, le design + effect appartient à une indicatrice donnée, pas au jeu de données : celui de + l'erreur et celui de l'accord diffèrent nettement sur le même corpus ; +- **le kappa**, qui n'est la moyenne d'aucune indicatrice : `kappa_interval_clustered`, + un bootstrap par grappes exact et rapide. Le corriger par le design effect d'une + proportion voisine le sur-corrige de moitié. + +Cas où AUCUNE correction n'est due : une statistique calculée à raison d'une seule +observation par copie (la prévalence d'un item, vue une fois par élève) — les +observations sont alors indépendantes. `wilson_interval` sans `deff` convient. + +`kappa_interval` (delta method) et `cluster_bootstrap` (générique, plus lent) +restent disponibles pour les cas hors grappes ou les vérifications ponctuelles. """ from __future__ import annotations import math -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass import numpy as np import pandas as pd +_Z = {0.90: 1.645, 0.95: 1.96, 0.99: 2.576} + @dataclass class ConfidenceInterval: @@ -25,32 +44,234 @@ class ConfidenceInterval: level: float = 0.95 -def wilson_interval(successes: int, n: int, level: float = 0.95) -> ConfidenceInterval: - """Intervalle de Wilson pour une proportion. +def wilson_interval( + successes: int, n: int, level: float = 0.95, deff: float = 1.0 +) -> ConfidenceInterval: + """Intervalle de Wilson pour une proportion, éventuellement corrigé du clustering. Args: successes: nombre de succès observés. n: nombre total d'observations. level: niveau de confiance (0.90, 0.95 ou 0.99 ; sinon 0.95 par défaut). + deff: design effect (voir `design_effect`). L'intervalle est alors calculé + sur l'effectif effectif `n / deff`, ce qui l'élargit d'un facteur + √deff. Laisser à 1.0 quand les observations sont indépendantes. Returns: L'intervalle de confiance avec sa proportion ponctuelle, borné à [0, 1]. + La proportion ponctuelle reste celle des données brutes : seul l'intervalle + est affecté par `deff`. Raises: - ValueError: si n vaut 0. + ValueError: si n vaut 0 ou si deff est inférieur à 1. """ if n == 0: raise ValueError("n doit être > 0.") - z = {0.90: 1.645, 0.95: 1.96, 0.99: 2.576}.get(level, 1.96) + if deff < 1.0: + raise ValueError("deff doit être >= 1 (1.0 = observations indépendantes).") + z = _Z.get(level, 1.96) p = successes / n - denom = 1 + z**2 / n - centre = (p + z**2 / (2 * n)) / denom - half = (z / denom) * math.sqrt(p * (1 - p) / n + z**2 / (4 * n**2)) + n_eff = n / deff # effectif effectif : ce que les données pèsent réellement + denom = 1 + z**2 / n_eff + centre = (p + z**2 / (2 * n_eff)) / denom + half = (z / denom) * math.sqrt(p * (1 - p) / n_eff + z**2 / (4 * n_eff**2)) return ConfidenceInterval( estimate=p, lower=max(0.0, centre - half), upper=min(1.0, centre + half), level=level ) +def kappa_interval( + y_true: Sequence[str] | pd.Series | np.ndarray, + y_pred: Sequence[str] | pd.Series | np.ndarray, + level: float = 0.95, +) -> ConfidenceInterval: + """Kappa de Cohen et son IC asymptotique (delta method de Fleiss). + + Alternative instantanée au bootstrap pour les grands effectifs : la variance + est calculée analytiquement sur la matrice des proportions conjointes. + L'approximation normale suppose un effectif suffisant (quelques centaines + d'observations au moins) — en dessous, préférer `cluster_bootstrap`. + + Args: + y_true: codes de référence (annotateur expert). + y_pred: codes prédits (modèle). + level: niveau de confiance de l'intervalle. + + Returns: + Le kappa et ses bornes. L'intervalle est dégénéré (bornes = estimation) + quand l'accord attendu par hasard vaut 1, cas où la variance n'est pas + définie — avec une seule catégorie observée, le kappa lui-même est NaN. + + Raises: + ValueError: si les deux séries sont vides ou de longueurs différentes. + """ + from sklearn.metrics import cohen_kappa_score + + true_arr = np.asarray(y_true, dtype=object) + pred_arr = np.asarray(y_pred, dtype=object) + if len(true_arr) != len(pred_arr): + raise ValueError("y_true et y_pred doivent avoir la même longueur.") + n = len(true_arr) + if n == 0: + raise ValueError("Séries vides : le kappa n'est pas défini.") + + kappa = float(cohen_kappa_score(true_arr, pred_arr)) + + # Matrice des proportions conjointes p_ij (expert en lignes, modèle en colonnes). + cats = sorted(set(true_arr) | set(pred_arr)) + idx = {c: i for i, c in enumerate(cats)} + joint = np.zeros((len(cats), len(cats))) + for t, p in zip(true_arr, pred_arr, strict=True): + joint[idx[t], idx[p]] += 1 + joint /= n + + marge_pred = joint.sum(axis=0) # marges du modèle + marge_true = joint.sum(axis=1) # marges de l'expert + p_e = float((marge_pred * marge_true).sum()) + if p_e >= 1.0: + return ConfidenceInterval(estimate=kappa, lower=kappa, upper=kappa, level=level) + + somme_marges = marge_pred + marge_true # (p_.i + p_i.) indexé par catégorie + terme_diag = np.diag(joint) * (1 - somme_marges * (1 - kappa)) + carres = joint * (marge_pred[np.newaxis, :] + marge_true[:, np.newaxis]) ** 2 + terme_hors_diag = (1 - kappa) ** 2 * (carres.sum() - np.trace(joint * somme_marges**2)) + variance = (terme_diag.sum() + terme_hors_diag - (kappa - p_e * (1 - kappa)) ** 2) / ( + n * (1 - p_e) ** 2 + ) + + z = _Z.get(level, 1.96) + demi = z * math.sqrt(max(0.0, variance)) + return ConfidenceInterval(estimate=kappa, lower=kappa - demi, upper=kappa + demi, level=level) + + +def _kappa_depuis_confusion(matrice: np.ndarray) -> float: + """Kappa de Cohen calculé directement sur une matrice de confusion (comptes).""" + total = matrice.sum() + if total == 0: + return float("nan") + p_o = np.trace(matrice) / total + p_e = float((matrice.sum(axis=0) * matrice.sum(axis=1)).sum()) / total**2 + if p_e >= 1.0: + return float("nan") + return float((p_o - p_e) / (1 - p_e)) + + +def kappa_interval_clustered( + y_true: Sequence[str] | pd.Series | np.ndarray, + y_pred: Sequence[str] | pd.Series | np.ndarray, + clusters: Sequence[str] | pd.Series | np.ndarray, + level: float = 0.95, + n_boot: int = 1000, + seed: int = 42, +) -> ConfidenceInterval: + """Kappa de Cohen et son IC par bootstrap par grappes (copies rééchantillonnées). + + Pourquoi cette fonction plutôt que `kappa_interval` corrigé d'un design effect : + le design effect se dérive d'une **indicatrice** précise (erreur, accord…) et + diffère de l'une à l'autre. Le kappa n'est pas la moyenne d'une indicatrice, et + gonfler sa variance par le design effect d'une proportion voisine le + sur-corrige nettement. Le bootstrap par grappes, lui, ne suppose rien. + + L'implémentation reste rapide malgré les milliers de tirages : la matrice de + confusion de chaque copie est calculée une fois, et un tirage se réduit à + sommer les matrices des copies tirées. C'est exactement équivalent à + rééchantillonner les lignes par grappes, sans reconstruire de DataFrame. + + Args: + y_true: codes de référence (annotateur expert). + y_pred: codes prédits (modèle). + clusters: identifiant de grappe de chaque observation (la copie). + level: niveau de confiance de l'intervalle. + n_boot: nombre de tirages bootstrap. + seed: graine du générateur aléatoire (reproductibilité). + + Returns: + Le kappa observé et les bornes issues des quantiles de la distribution + bootstrap. + + Raises: + ValueError: si les séries sont vides ou de longueurs différentes. + """ + true_arr = np.asarray(y_true, dtype=object) + pred_arr = np.asarray(y_pred, dtype=object) + grappes = np.asarray(clusters, dtype=object) + if not (len(true_arr) == len(pred_arr) == len(grappes)): + raise ValueError("y_true, y_pred et clusters doivent avoir la même longueur.") + if len(true_arr) == 0: + raise ValueError("Séries vides : le kappa n'est pas défini.") + + cats = sorted(set(true_arr) | set(pred_arr)) + idx = {c: i for i, c in enumerate(cats)} + codes_true = np.array([idx[v] for v in true_arr]) + codes_pred = np.array([idx[v] for v in pred_arr]) + + # Une matrice de confusion (k × k) par grappe, empilée : (n_grappes, k, k). + grappes_uniques, position = np.unique(grappes, return_inverse=True) + k = len(cats) + par_grappe = np.zeros((len(grappes_uniques), k, k)) + np.add.at(par_grappe, (position, codes_true, codes_pred), 1) + + observe = _kappa_depuis_confusion(par_grappe.sum(axis=0)) + + rng = np.random.default_rng(seed) + tirages = np.empty(n_boot) + for b in range(n_boot): + choix = rng.integers(0, len(grappes_uniques), len(grappes_uniques)) + tirages[b] = _kappa_depuis_confusion(par_grappe[choix].sum(axis=0)) + + alpha = (1 - level) / 2 + bas, haut = np.nanquantile(tirages, [alpha, 1 - alpha]) + return ConfidenceInterval(estimate=observe, lower=float(bas), upper=float(haut), level=level) + + +def design_effect(indicator: pd.Series | np.ndarray, clusters: pd.Series | np.ndarray) -> float: + """Design effect de Kish d'une indicatrice binaire, dû au regroupement en grappes. + + Les items d'une même copie ne sont pas indépendants (un élève faible se trompe + partout) : la moyenne d'une indicatrice sur N items porte donc moins + d'information que N observations indépendantes. Le design effect chiffre cette + perte — `deff = 1 + (m - 1) x ICC`, où m est la taille moyenne de grappe et + l'ICC la part de variance qui vient des différences *entre* copies. Diviser N + par ce facteur donne l'effectif effectif, ce qui élargit l'IC de √deff. + + **Le design effect appartient à une indicatrice, pas à un jeu de données** : sur + ce corpus, celle de l'erreur donne ≈ 10, celle de l'accord ≈ 7,5, celle du + rappel ≈ 4. Passer l'indicatrice concernée est donc obligatoire, sous peine de + corriger avec le mauvais facteur. Corollaire : une statistique calculée à raison + d'**une observation par grappe** (la prévalence d'un item, mesurée une fois par + copie) n'a aucune corrélation intra-grappe à corriger — son deff vaut 1 et cette + fonction n'a pas lieu d'être appelée. + + Args: + indicator: indicatrice binaire (ou booléenne) dont on estime la moyenne. + clusters: identifiant de grappe de chaque observation (la copie). + + Returns: + Le design effect, dans [1, taille moyenne de grappe]. Les bornes sont + atteintes quand l'ICC estimé sort de [0, 1], ce qui n'a pas de sens ici : + la variance intra étant obtenue par différence, elle peut devenir négative + sur des grappes dégénérées (parfaitement homogènes ou très peu nombreuses). + """ + ind = pd.Series(np.asarray(indicator, dtype=float)).reset_index(drop=True) + grappes = pd.Series(np.asarray(clusters, dtype=object)).reset_index(drop=True) + if len(ind) != len(grappes): + raise ValueError("indicator et clusters doivent avoir la même longueur.") + + taille_moyenne = float(ind.groupby(grappes).size().mean()) if len(ind) else 0.0 + var_totale = float(ind.var(ddof=1)) if len(ind) > 1 else 0.0 + if var_totale == 0 or taille_moyenne <= 1 or len(ind) < 2: + return 1.0 + + # Décomposition ANOVA à un facteur : variance inter-copies vs intra-copie. + var_inter = float(ind.groupby(grappes).mean().var(ddof=1)) + var_intra = (var_totale - var_inter) / (taille_moyenne - 1) + denom = var_inter + (taille_moyenne - 1) * var_intra + if denom <= 0: + return 1.0 + icc = min(1.0, max(0.0, (var_inter - var_intra) / denom)) + return max(1.0, 1 + (taille_moyenne - 1) * icc) + + def cluster_bootstrap( df: pd.DataFrame, metric_fn: Callable[[pd.DataFrame], float], diff --git a/src/evaluation_dictee/pipeline/benchmark.py b/src/evaluation_dictee/pipeline/benchmark.py index 6ec2050..d8788e7 100644 --- a/src/evaluation_dictee/pipeline/benchmark.py +++ b/src/evaluation_dictee/pipeline/benchmark.py @@ -174,10 +174,12 @@ def _score_one(copy: Copy) -> _CopyOutcome: # (seul le pré-imprimé marque quelques % d'encre). Traitée identiquement # pour toutes les méthodes → tous les items codés "0" (absent), sans appel # modèle. Empêche l'end-to-end d'halluciner la référence sur une page vide. + # La densité mesurée est conservée telle quelle dans le JSONL, et pas + # seulement le verdict : c'est ce qui permet de tracer la distribution + # et de rejuger le seuil sans relancer un benchmark de 30 h. try: - is_blank = ( - blank_threshold > 0 and ink_ratio(load_image(copy.image_path)) < blank_threshold - ) + densite_encre = ink_ratio(load_image(copy.image_path)) + is_blank = blank_threshold > 0 and densite_encre < blank_threshold except Exception as exc: # noqa: BLE001 — image illisible : on remonte un échec if trace is not None: trace.update(level="ERROR", status_message=str(exc)) @@ -229,6 +231,7 @@ def _score_one(copy: Copy) -> _CopyOutcome: "raw_transcription": prediction.raw_transcription, "approach": config.approach, "blank": is_blank, + "ink_ratio": densite_encre, } ) if pred_code == true_code: diff --git a/tests/test_benchmark_concurrency.py b/tests/test_benchmark_concurrency.py index 27192e5..f9cb31d 100644 --- a/tests/test_benchmark_concurrency.py +++ b/tests/test_benchmark_concurrency.py @@ -160,9 +160,7 @@ def test_copie_vierge_auto_codee_zero(patched, monkeypatch, tmp_path: Path) -> N assert result.blank_copies == ["c001.png"] assert "c001.png" not in scorer.scored # aucune inférence sur une copie vierge - recs = [ - json.loads(line) for line in _read_lines(tmp_path / "test_run_predictions.jsonl") - ] + recs = [json.loads(line) for line in _read_lines(tmp_path / "test_run_predictions.jsonl")] vierge = [r for r in recs if r["copy_id"] == "c001.png"] assert len(vierge) == 3 assert all(r["y_pred"] == "0" for r in vierge) diff --git a/tests/test_evaluation_avancee.py b/tests/test_evaluation_avancee.py index 8476389..42b371d 100644 --- a/tests/test_evaluation_avancee.py +++ b/tests/test_evaluation_avancee.py @@ -1,5 +1,7 @@ """Tests des outils statistiques et du rapport par item.""" +import math + import pandas as pd import pytest @@ -12,7 +14,13 @@ per_copy_metrics, per_item_metrics, ) -from evaluation_dictee.evaluation.statistics import cluster_bootstrap, wilson_interval +from evaluation_dictee.evaluation.statistics import ( + cluster_bootstrap, + design_effect, + kappa_interval, + kappa_interval_clustered, + wilson_interval, +) # ── Wilson ──────────────────────────────────────────────────────────────────── @@ -35,6 +43,138 @@ def test_wilson_n_nul_leve_erreur() -> None: wilson_interval(0, 0) +# ── Kappa et son IC analytique ──────────────────────────────────────────────── +def test_kappa_interval_encadre_estimation() -> None: + y_true = ["1"] * 60 + ["9"] * 40 + y_pred = ["1"] * 55 + ["9"] * 5 + ["9"] * 35 + ["1"] * 5 + ci = kappa_interval(y_true, y_pred) + assert 0 < ci.estimate < 1 + assert ci.lower < ci.estimate < ci.upper + + +def test_kappa_interval_accord_parfait_interval_serre() -> None: + y = ["1"] * 50 + ["9"] * 50 + ci = kappa_interval(y, y) + assert ci.estimate == pytest.approx(1.0) + assert ci.upper - ci.lower < 1e-9 + + +def test_kappa_interval_une_seule_categorie_degenere() -> None: + # p_e = 1 : la variance n'est pas définie et sklearn renvoie un kappa NaN. + ci = kappa_interval(["1"] * 50, ["1"] * 50) + assert math.isnan(ci.estimate) + assert math.isnan(ci.lower) + assert math.isnan(ci.upper) + + +def test_kappa_interval_longueurs_incoherentes() -> None: + with pytest.raises(ValueError): + kappa_interval(["1", "9"], ["1"]) + + +def test_kappa_interval_series_vides() -> None: + with pytest.raises(ValueError): + kappa_interval([], []) + + +# ── Design effect ───────────────────────────────────────────────────────────── +def test_design_effect_grappes_homogenes_est_maximal() -> None: + # Deux copies parfaitement homogènes : toute la variance est inter-copies, + # l'ICC vaut 1 et le design effect atteint la taille de grappe. + grappes = ["A"] * 10 + ["B"] * 10 + indicatrice = [0] * 10 + [1] * 10 + assert design_effect(pd.Series(indicatrice), pd.Series(grappes)) == pytest.approx( + 10.0, rel=1e-6 + ) + + +def test_design_effect_sans_variance_vaut_un() -> None: + grappes = ["A"] * 5 + ["B"] * 5 + assert design_effect(pd.Series([1] * 10), pd.Series(grappes)) == 1.0 + + +def test_design_effect_minore_a_un() -> None: + # Grappes volontairement mélangées : l'ICC estimé est négatif, on borne à 1. + grappes = ["A", "A", "B", "B", "C", "C"] + assert design_effect(pd.Series([0, 1, 0, 1, 0, 1]), pd.Series(grappes)) == 1.0 + + +def test_design_effect_propre_a_chaque_indicatrice() -> None: + # Deux indicatrices sur les MÊMES grappes n'ont pas le même design effect : + # la première est parfaitement groupée, la seconde alterne dans chaque copie. + grappes = pd.Series(["A"] * 4 + ["B"] * 4) + groupee = pd.Series([0, 0, 0, 0, 1, 1, 1, 1]) + alternee = pd.Series([0, 1, 0, 1, 0, 1, 0, 1]) + assert design_effect(groupee, grappes) > design_effect(alternee, grappes) + assert design_effect(alternee, grappes) == 1.0 + + +def test_design_effect_longueurs_incoherentes() -> None: + with pytest.raises(ValueError): + design_effect(pd.Series([0, 1]), pd.Series(["A"])) + + +# ── Wilson corrigé du design effect ─────────────────────────────────────────── +def test_wilson_deff_elargit_intervalle() -> None: + brut = wilson_interval(500, 1000) + corrige = wilson_interval(500, 1000, deff=4.0) + assert corrige.estimate == brut.estimate # la valeur ponctuelle ne change pas + largeur_brute = brut.upper - brut.lower + largeur_corrigee = corrige.upper - corrige.lower + # deff = 4 → intervalle élargi d'un facteur ≈ √4 = 2. + assert largeur_corrigee / largeur_brute == pytest.approx(2.0, rel=0.02) + + +def test_wilson_deff_neutre_a_un() -> None: + assert wilson_interval(300, 1000, deff=1.0) == wilson_interval(300, 1000) + + +def test_wilson_deff_invalide() -> None: + with pytest.raises(ValueError): + wilson_interval(500, 1000, deff=0.5) + + +# ── Kappa par bootstrap de grappes ──────────────────────────────────────────── +def _df_kappa_groupe() -> pd.DataFrame: + """20 copies de 10 items : la moitié bien codées, l'autre systématiquement ratée.""" + lignes = [] + for c in range(20): + for i in range(10): + vrai = "1" if i % 2 else "9" + predit = vrai if c % 2 == 0 else ("9" if vrai == "1" else "1") + lignes.append({"copy_id": f"c{c}", "y_true": vrai, "y_pred": predit}) + return pd.DataFrame(lignes) + + +def test_kappa_clustered_encadre_estimation() -> None: + df = _df_kappa_groupe() + ci = kappa_interval_clustered(df["y_true"], df["y_pred"], df["copy_id"], n_boot=200) + assert ci.lower <= ci.estimate <= ci.upper + + +def test_kappa_clustered_plus_large_que_delta_method() -> None: + # Les items étant fortement corrélés dans chaque copie, le bootstrap par + # grappes doit produire un intervalle plus large que la delta method, qui + # suppose l'indépendance. + df = _df_kappa_groupe() + grappes = kappa_interval_clustered(df["y_true"], df["y_pred"], df["copy_id"], n_boot=400) + naif = kappa_interval(df["y_true"], df["y_pred"]) + assert (grappes.upper - grappes.lower) > (naif.upper - naif.lower) + + +def test_kappa_clustered_reproductible() -> None: + df = _df_kappa_groupe() + args = (df["y_true"], df["y_pred"], df["copy_id"]) + a = kappa_interval_clustered(*args, n_boot=100, seed=7) + b = kappa_interval_clustered(*args, n_boot=100, seed=7) + assert (a.lower, a.upper) == (b.lower, b.upper) + + +def test_kappa_clustered_longueurs_incoherentes() -> None: + with pytest.raises(ValueError): + kappa_interval_clustered(["1", "9"], ["1", "9"], ["c1"]) + + # ── Bootstrap par grappes ───────────────────────────────────────────────────── def _df_jouet() -> pd.DataFrame: rows = [] diff --git a/uv.lock b/uv.lock index 9d2fb71..3cc1291 100644 --- a/uv.lock +++ b/uv.lock @@ -1414,6 +1414,12 @@ notebooks = [ { name = "jupyterlab" }, { name = "matplotlib" }, ] +website = [ + { name = "ipykernel" }, + { name = "matplotlib" }, + { name = "nbclient" }, + { name = "nbformat" }, +] [package.dev-dependencies] dev = [ @@ -1428,9 +1434,13 @@ dev = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27" }, + { name = "ipykernel", marker = "extra == 'website'", specifier = ">=6.29" }, { name = "jupyterlab", marker = "extra == 'notebooks'", specifier = ">=4.6.1" }, { name = "langfuse", specifier = ">=4,<5" }, { name = "matplotlib", marker = "extra == 'notebooks'", specifier = ">=3.11.1" }, + { name = "matplotlib", marker = "extra == 'website'", specifier = ">=3.11.1" }, + { name = "nbclient", marker = "extra == 'website'", specifier = ">=0.10" }, + { name = "nbformat", marker = "extra == 'website'", specifier = ">=5.10" }, { name = "numpy", specifier = ">=1.26" }, { name = "openai", specifier = ">=1.30" }, { name = "pandas", specifier = ">=2.2" }, @@ -1448,7 +1458,7 @@ requires-dist = [ { name = "typer", specifier = ">=0.12" }, { name = "vllm", marker = "extra == 'gpu'", specifier = ">=0.6" }, ] -provides-extras = ["gpu", "notebooks"] +provides-extras = ["gpu", "notebooks", "website"] [package.metadata.requires-dev] dev = [ diff --git a/website/_analyse.py b/website/_analyse.py new file mode 100644 index 0000000..9daf6c8 --- /dev/null +++ b/website/_analyse.py @@ -0,0 +1,914 @@ +"""Chargement et calculs partagés par les pages « Résultats » et « Écarts ». + +Le site **relit les prédictions déjà exportées** (S3 par défaut, ou un dossier +local via `S3_PREDICTIONS_PREFIX`) et recalcule chiffres et figures au rendu : +aucun pipeline n'est réexécuté, aucune figure n'est figée. Toute défaillance +(paquet non importable, run non exporté, S3 injoignable) est capturée dans +`NOTES` et laisse la page rendable, avec des « — » à la place des chiffres. + +Ce module est préfixé par `_` : Quarto ne le rend pas comme une page, il est +seulement importé par les `.qmd` (dont le répertoire de travail est `website/`). + +Le contenu reprend l'analyse de `notebooks/03_analyse_resultats.ipynb`. Les +briques génériques (Wilson, IC du kappa, design effect, métriques par item et +par copie, comparaison multi-modèles) viennent du paquet `evaluation_dictee` ; +seule la mise en forme propre au site vit ici. +""" + +from __future__ import annotations + +import html +import math +import os +import sys +from dataclasses import dataclass, field +from pathlib import Path + +import pandas as pd + +# ── Import du paquet du projet ──────────────────────────────────────────────── +# Le site peut être rendu hors `uv run` (Quarto lancé avec le Python système) : +# on ajoute alors `src/` au chemin d'import en remontant depuis le dossier courant. +REPO_ROOT: Path | None = None +for _parent in (Path.cwd(), *Path.cwd().parents): + if (_parent / "src" / "evaluation_dictee").is_dir(): + REPO_ROOT = _parent + sys.path.insert(0, str(_parent / "src")) + break + +#: Incidents rencontrés au chargement, affichés en fin de page par `bloc_notes()`. +NOTES: list[str] = [] + +try: + from evaluation_dictee.data.grid import GridItem, load_grid + from evaluation_dictee.evaluation.multi_model import ( + agreement_per_item, + confidence_score, + disagreement_type_summary, + referral_curve_with_ci, + ) + from evaluation_dictee.evaluation.report import ( + copies_by_disagreement, + disagreement_decomposition, + load_predictions, + per_copy_metrics, + per_item_metrics, + ) + from evaluation_dictee.evaluation.statistics import ( + design_effect, + kappa_interval_clustered, + wilson_interval, + ) + + PAQUET_OK = True +except ImportError as exc: # noqa: BLE001 — la page doit rester rendable + PAQUET_OK = False + NOTES.append( + f"Paquet `evaluation_dictee` non importable ({exc}) : aucun chiffre n'a pu " + "être calculé. Rendre le site depuis l'environnement du projet " + "(`uv sync --extra website` puis `uv run quarto render website`)." + ) + +# ── Paramètres du site ──────────────────────────────────────────────────────── +#: Racine des prédictions exportées. Pointer sur un dossier local pour un rendu +#: hors ligne : `export S3_PREDICTIONS_PREFIX=/chemin/vers/predictions`. +PREFIX = os.environ.get("S3_PREDICTIONS_PREFIX", "s3://projet-production-ecrits-depp/predictions") + +#: Runs comparés côte à côte, dans l'ordre d'affichage : libellé → nom de run. +#: Surchargeables pour comparer d'autres runs sans toucher aux pages. +RUNS: dict[str, str] = { + "end-to-end": os.environ.get("RESULTATS_RUN_END_TO_END", "dictee_end2end"), + "two-stage": os.environ.get("RESULTATS_RUN_TWO_STAGE", "dictee_two_stage"), +} + +#: Run servant de référence quand une analyse en exige un seul (classement des +#: pires copies, courbe de renvoi). Par défaut l'approche privilégiée du projet. +RUN_REFERENCE = os.environ.get("RESULTATS_RUN_REFERENCE", "end-to-end") + +#: Chemin de la grille de codage (mots attendus, ordre de la dictée). +GRID_PATH = os.environ.get("RESULTATS_GRID_PATH", "configs/grille_dictee_2015.json") + +#: Codes attendus dans la grille simplifiée. `y_true` contient une minorité de +#: codes hérités de la saisie AGATE (vides, « i », « v ») : on les compte comme +#: des erreurs (convention « erreur = code != 1 » du projet) mais on les signale. +CODES_VALIDES = frozenset({"1", "9", "0"}) + +#: Libellé lisible de chaque code. +LIBELLE_CODE = {"1": "correct", "9": "erreur", "0": "absent", "?": "non lu"} + +# ── Copies vierges, écartées des classements d'écart ────────────────────────── +# Le critère est celui du pipeline, et lui seul. `pipeline/benchmark.py` mesure la +# densité d'encre de l'image (`data/loaders.ink_ratio`) et, sous +# `config.data.blank_ink_threshold` (2,5 % par défaut — seul le pré-imprimé +# marque), déclare la copie VIERGE : elle est codée « 0 » sur tous les items sans +# appel modèle, et chaque ligne du JSONL est estampillée `blank: true`. Le site se +# contente de relire ce marqueur : aucune heuristique concurrente ici, pour que +# site et pipeline ne puissent pas diverger dans leur définition. +# +# Un run produit AVANT ce garde-fou n'a pas la colonne : le site ne peut alors +# écarter aucune copie et le dit explicitement, plutôt que de deviner. +COLONNE_VIERGE = "blank" + +#: Seuil d'encre du pipeline, repris pour l'affichage quand la source de données +#: ne le porte pas. La valeur qui s'applique au run est celle de son YAML +#: (`data.blank_ink_threshold`). +SEUIL_ENCRE_PIPELINE = 0.025 + +#: Colonne portant la densité d'encre mesurée, quand elle est disponible. +COLONNE_ENCRE = "ink_ratio" + +#: Fichier de distribution des densités d'encre, produit par +#: `scripts/compute_ink_ratios.py` et cherché à côté des prédictions. +FICHIER_ENCRE = os.environ.get("RESULTATS_FICHIER_ENCRE", "dictee_ink_ratios.csv") + +# ── Palette ─────────────────────────────────────────────────────────────────── +C_EXPERT = "#1f4e79" +C_MODELE = "#c44536" +C_OK, C_MOYEN, C_PB = "#2e7d32", "#ef6c00", "#c62828" +#: Une couleur par run, dans l'ordre de `RUNS`. +COULEURS_RUN = ["#c44536", "#7b2d8b", "#0b7285", "#946200"] +Z95 = 1.96 + + +def init_matplotlib() -> None: + """Applique le style de figures du projet (à appeler une fois par page).""" + import matplotlib.pyplot as plt + + plt.rcParams.update( + { + "figure.dpi": 110, + "savefig.dpi": 200, + "savefig.bbox": "tight", + "axes.spines.top": False, + "axes.spines.right": False, + "axes.grid": True, + "grid.alpha": 0.25, + "font.size": 9.5, + } + ) + + +# ── Formatage ───────────────────────────────────────────────────────────────── +def _absent(x: object) -> bool: + """Vrai si la valeur est manquante (None ou NaN) et doit s'afficher « — ».""" + return x is None or (isinstance(x, float) and math.isnan(x)) + + +def pct(x: object, d: int = 1) -> str: + """Formate une proportion de [0, 1] en pourcentage, « — » si absente.""" + return "—" if _absent(x) else f"{float(x):.{d}%}".replace("%", " %") + + +def pts(x: object, d: int = 1) -> str: + """Formate un pourcentage déjà exprimé en points (0-100), « — » si absent.""" + return "—" if _absent(x) else f"{float(x):.{d}f} %" + + +def num(x: object, d: int = 3) -> str: + """Formate un nombre décimal, « — » si absent.""" + return "—" if _absent(x) else f"{float(x):.{d}f}" + + +def milliers(n: object) -> str: + """Formate un entier avec une espace fine comme séparateur de milliers.""" + return "—" if _absent(n) else f"{int(n):,}".replace(",", " ") + + +def formateur_unite(unite: str): + """Renvoie le formateur associé à une unité (« pct » pour %, « num » sinon).""" + return pct if unite == "pct" else num + + +def ic(valeur: object, bas: object, haut: object, formateur=pct) -> str: + """Formate « estimation [bas ; haut] », « — » si l'estimation est absente.""" + if _absent(valeur): + return "—" + if _absent(bas) or _absent(haut): + return formateur(valeur) + return f"{formateur(valeur)} [{formateur(bas)} ; {formateur(haut)}]" + + +def table_markdown(entetes: list[str], lignes: list[list[str]], legende: str = "") -> str: + """Assemble un tableau Markdown (aligné à droite sauf la 1re colonne). + + Args: + entetes: libellés de colonnes. + lignes: valeurs, déjà formatées en chaînes. + legende: légende Quarto, ajoutée sous le tableau si non vide. + + Returns: + Le tableau au format Markdown, prêt à `print()` dans une cellule + `output: asis`. + """ + aligns = ["---"] + [":---:"] * (len(entetes) - 1) + out = ["| " + " | ".join(entetes) + " |", "| " + " | ".join(aligns) + " |"] + out += ["| " + " | ".join(ligne) + " |" for ligne in lignes] + if legende: + out += ["", f": {legende} {{.striped .hover}}"] + return "\n".join(out) + + +def bloc_notes() -> str: + """Callout replié listant les incidents de chargement (vide si aucun).""" + if not NOTES: + return "" + lignes = [ + '::: {.callout-warning appearance="simple" collapse="true" ' + 'title="Chiffres non calculés ou incomplets"}' + ] + lignes += [f"- {n}" for n in NOTES] + lignes.append(":::") + return "\n".join(lignes) + + +# ── Chargement ──────────────────────────────────────────────────────────────── +@dataclass +class Run: + """Un run de prédictions et ses agrégats, prêts à être tracés.""" + + label: str + nom: str + couleur: str + df: pd.DataFrame + copies: pd.DataFrame = field(repr=False) + items: pd.DataFrame = field(repr=False) + + @property + def n_copies(self) -> int: + """Nombre de copies évaluées.""" + return int(self.df["copy_id"].nunique()) + + @property + def n_items(self) -> int: + """Nombre de lignes item × copie.""" + return len(self.df) + + +def _chemin(nom_run: str) -> str: + """URI du JSONL de prédictions d'un run.""" + return PREFIX.rstrip("/") + "/" + nom_run + "_predictions.jsonl" + + +def charger_runs() -> dict[str, Run]: + """Charge tous les runs de `RUNS` et calcule leurs agrégats par item et copie. + + Returns: + Les runs chargés, indexés par libellé, dans l'ordre de `RUNS`. Un run + absent ou illisible est omis et l'incident consigné dans `NOTES`. + """ + if not PAQUET_OK: + return {} + runs: dict[str, Run] = {} + for i, (label, nom) in enumerate(RUNS.items()): + if not nom: + continue + try: + df = load_predictions(_chemin(nom)) + except Exception as exc: # noqa: BLE001 — la page doit rester rendable + NOTES.append(f"`{nom}` indisponible ({type(exc).__name__}) : colonne non calculée.") + continue + if df.empty: + NOTES.append(f"`{nom}` est vide : colonne non calculée.") + continue + runs[label] = Run( + label=label, + nom=nom, + couleur=COULEURS_RUN[i % len(COULEURS_RUN)], + df=df, + copies=per_copy_metrics(df), + items=per_item_metrics(df), + ) + if not runs: + NOTES.append( + "Aucun run n'a pu être chargé. Vérifier `S3_PREDICTIONS_PREFIX` et " + "l'export (`uv run scripts/export_predictions.py --config …`)." + ) + return runs + + +def charger_grille() -> tuple[list[GridItem], dict[str, str], dict[str, int]]: + """Charge la grille de codage : items ordonnés, mot attendu et rang par item. + + Returns: + Le triplet (items de la grille, item_id → mot attendu, item_id → rang + dans la dictée). Vide si la grille est introuvable (incident consigné). + """ + if not PAQUET_OK or REPO_ROOT is None: + return [], {}, {} + try: + grille = load_grid(str(REPO_ROOT / GRID_PATH)) + except Exception as exc: # noqa: BLE001 — la page doit rester rendable + NOTES.append(f"Grille `{GRID_PATH}` illisible ({type(exc).__name__}) : items non libellés.") + return [], {}, {} + mots = {it.item_id: it.attendu for it in grille.items} + rangs = {it.item_id: i + 1 for i, it in enumerate(grille.items)} + return grille.items, mots, rangs + + +def libelle_item(item_id: str, mots: dict[str, str], rangs: dict[str, int]) -> str: + """Libellé d'item au format `NN · mot`, qui lève l'ambiguïté des mots répétés.""" + return f"{rangs.get(item_id, 0):02d} · {mots.get(item_id, item_id)}" + + +def note_codes_non_standard(runs: dict[str, Run]) -> str: + """Phrase signalant les codes experts hors grille simplifiée (ou chaîne vide).""" + if not runs: + return "" + ref = next(iter(runs.values())).df + hors = ref[~ref["y_true"].isin(CODES_VALIDES)] + if hors.empty: + return "" + detail = ", ".join( + f"« {code or 'vide'} » ({milliers(n)})" for code, n in hors["y_true"].value_counts().items() + ) + return ( + f"{milliers(len(hors))} items sur {milliers(len(ref))} " + f"({len(hors) / len(ref):.2%}) portent un code expert hors grille simplifiée : " + f"{detail}. Ils sont conservés et comptés comme des erreurs (convention " + "« erreur = code ≠ 1 »), ce qui pèse de façon négligeable sur les agrégats." + ) + + +# ── Synthèse globale ────────────────────────────────────────────────────────── +def _proportion_groupee(indicatrice: pd.Series, grappes: pd.Series) -> tuple[object, float]: + """Proportion et IC de Wilson corrigés du design effect propre à l'indicatrice. + + Args: + indicatrice: indicatrice binaire dont on estime la moyenne. + grappes: identifiant de copie de chaque observation. + + Returns: + Le couple (intervalle de confiance, design effect appliqué). L'intervalle + vaut None si l'indicatrice est vide. + """ + if not len(indicatrice): + return None, float("nan") + deff = design_effect(indicatrice, grappes) + return wilson_interval(int(indicatrice.sum()), len(indicatrice), deff=deff), deff + + +def synthese_globale(df: pd.DataFrame, n_boot: int = 1000) -> pd.DataFrame: + """Accord, kappa, rappel et précision sur l'erreur, avec IC à 95 % par grappes. + + Les 83 items d'une copie ne sont pas indépendants : les IC sont donc corrigés + du **design effect de Kish**, calculé pour **chaque** métrique sur sa propre + indicatrice (celle de l'accord et celle du rappel n'ont pas le même). Le kappa + n'étant la moyenne d'aucune indicatrice, son IC vient d'un bootstrap par + grappes, qui ne suppose rien sur la structure de corrélation. + + Args: + df: prédictions à l'item (colonnes copy_id, y_true, y_pred). + n_boot: nombre de tirages du bootstrap par grappes (kappa). + + Returns: + Un DataFrame indexé par métrique : valeur, borne basse, borne haute, + `unite` (« pct » pour une proportion de [0, 1], « num » pour le kappa), + `deff` (design effect appliqué, NaN pour le kappa qui passe par le + bootstrap), `methode` et `lecture` (sens de la métrique). + """ + grappes = df["copy_id"] + err_exp = df["y_true"] != "1" + err_mod = df["y_pred"] != "1" + + accord, deff_accord = _proportion_groupee(df["y_true"] == df["y_pred"], grappes) + # Rappel et précision se mesurent sur des SOUS-ENSEMBLES d'items : la grappe + # doit être restreinte de la même façon, sinon le design effect est faussé. + rappel, deff_rappel = _proportion_groupee(err_mod[err_exp], grappes[err_exp]) + precision, deff_precision = _proportion_groupee(err_exp[err_mod], grappes[err_mod]) + # Sur-correction : l'expert voit une erreur, le modèle non (biais VLM connu). + surcorr, deff_surcorr = _proportion_groupee(~err_mod[err_exp], grappes[err_exp]) + surdet, deff_surdet = _proportion_groupee(err_mod[~err_exp], grappes[~err_exp]) + kappa = kappa_interval_clustered(df["y_true"], df["y_pred"], grappes, n_boot=n_boot) + + methode_wilson = "Wilson corrigé du design effect" + lignes = [ + ( + "Accord brut", + accord, + "pct", + deff_accord, + methode_wilson, + "part des items codés à l'identique : plus haut = mieux", + ), + ( + "Kappa de Cohen", + kappa, + "num", + float("nan"), + f"bootstrap par grappes ({n_boot} tirages)", + "accord corrigé du hasard : plus haut = mieux", + ), + ( + "Rappel des erreurs (sensibilité)", + rappel, + "pct", + deff_rappel, + methode_wilson, + "part des erreurs de l'élève retrouvées : plus haut = moins de fautes ratées", + ), + ( + "Précision sur les erreurs", + precision, + "pct", + deff_precision, + methode_wilson, + "part des erreurs signalées qui en sont vraiment : plus haut = moins de " + "fausses alertes", + ), + ( + "Taux de sur-correction", + surcorr, + "pct", + deff_surcorr, + methode_wilson, + "erreurs de l'élève que le modèle valide à tort : plus **bas** = mieux", + ), + ( + "Taux de sur-détection", + surdet, + "pct", + deff_surdet, + methode_wilson, + "items corrects que le modèle sanctionne à tort : plus **bas** = mieux", + ), + ] + return pd.DataFrame( + [ + { + "metrique": nom, + "valeur": float("nan") if v is None else v.estimate, + "bas": float("nan") if v is None else v.lower, + "haut": float("nan") if v is None else v.upper, + "unite": unite, + "deff": deff, + "methode": methode, + "lecture": lecture, + } + for nom, v, unite, deff, methode, lecture in lignes + ] + ).set_index("metrique") + + +# ── Prévalence par item ─────────────────────────────────────────────────────── +def prevalence_par_item( + df: pd.DataFrame, mots: dict[str, str], rangs: dict[str, int] +) -> pd.DataFrame: + """Prévalence d'erreur par item, expert vs modèle, avec IC de Wilson à 95 %. + + **Aucune correction de clustering ici, et c'est voulu** : pour un item donné il + y a exactement *une* observation par copie, chacune venant d'un élève différent. + Il n'y a donc pas de corrélation intra-copie à corriger et le design effect vaut + 1. L'appliquer élargirait les intervalles d'un facteur ≈ 3 sans justification. + La correction concerne les métriques agrégées sur les 83 items d'une copie, + dans `synthese_globale`. + + Args: + df: prédictions à l'item. + mots: item_id → mot attendu. + rangs: item_id → rang dans la dictée. + + Returns: + Un DataFrame indexé par item_id, trié par rang : prévalences d'erreur + expert et modèle avec leurs IC, et l'écart modèle − expert en points de + pourcentage. + """ + lignes = [] + for item_id, grp in df.groupby("item_id"): + n = len(grp) + k_exp = int((grp["y_true"] != "1").sum()) + k_mod = int((grp["y_pred"] != "1").sum()) + ci_exp = wilson_interval(k_exp, n) + ci_mod = wilson_interval(k_mod, n) + lignes.append( + { + "item_id": item_id, + "position": rangs.get(item_id, 0), + "mot_attendu": mots.get(item_id, item_id), + "pct_expert": k_exp / n * 100, + "ic_expert_bas": ci_exp.lower * 100, + "ic_expert_haut": ci_exp.upper * 100, + "pct_modele": k_mod / n * 100, + "ic_modele_bas": ci_mod.lower * 100, + "ic_modele_haut": ci_mod.upper * 100, + "ecart": (k_mod - k_exp) / n * 100, + } + ) + return pd.DataFrame(lignes).set_index("item_id").sort_values("position") + + +# ── Métriques selon la difficulté de la copie ───────────────────────────────── +def metriques_par_difficulte( + df: pd.DataFrame, copies: pd.DataFrame, col_tri: str, n_quantiles: int = 8 +) -> pd.DataFrame: + """Accord, kappa, rappel et précision par quantile de difficulté de copie. + + Args: + df: prédictions à l'item. + copies: métriques par copie (index = copy_id), source de la difficulté. + col_tri: colonne de `copies` définissant la difficulté (nb d'erreurs). + n_quantiles: nombre de groupes de difficulté. + + Comme dans `synthese_globale`, les IC sont corrigés du design effect propre à + chaque indicatrice : ces métriques agrègent les 83 items corrélés d'une copie. + + Returns: + Un DataFrame, une ligne par quantile, trié par difficulté croissante : + difficulté moyenne (`x`), effectif, accord et son IC Wilson, kappa, + rappel et son IC, précision. + """ + from sklearn.metrics import cohen_kappa_score + + try: + groupes = pd.qcut(copies[col_tri], q=n_quantiles, duplicates="drop") + except ValueError: # trop peu de valeurs distinctes pour des quantiles + groupes = pd.cut(copies[col_tri], bins=n_quantiles) + + copies = copies.assign(_groupe=groupes) + milieu = copies.groupby("_groupe", observed=True)[col_tri].mean() + + lignes = [] + for etiquette, copies_grp in copies.groupby("_groupe", observed=True): + sub = df[df["copy_id"].isin(copies_grp.index)] + if len(sub) < 10: + continue + grappes = sub["copy_id"] + err_exp = sub["y_true"] != "1" + err_mod = sub["y_pred"] != "1" + n_me = int(err_mod.sum()) + n_vp = int((err_exp & err_mod).sum()) + accord, _ = _proportion_groupee(sub["y_true"] == sub["y_pred"], grappes) + rappel, _ = _proportion_groupee(err_mod[err_exp], grappes[err_exp]) + try: + kappa = float(cohen_kappa_score(sub["y_true"], sub["y_pred"])) + except ValueError: + kappa = float("nan") + lignes.append( + { + "x": milieu[etiquette], + "n_copies": len(copies_grp), + "accord": accord.estimate, + "accord_bas": accord.lower, + "accord_haut": accord.upper, + "kappa": kappa, + "rappel": rappel.estimate if rappel else float("nan"), + "rappel_bas": rappel.lower if rappel else float("nan"), + "rappel_haut": rappel.upper if rappel else float("nan"), + "precision": n_vp / n_me if n_me else float("nan"), + } + ) + return pd.DataFrame(lignes).sort_values("x") + + +# ── Comparaison multi-runs ──────────────────────────────────────────────────── +def joindre_runs(runs: dict[str, Run]) -> pd.DataFrame | None: + """Joint les runs sur (copy_id, item_id) au format attendu par `multi_model`. + + Équivalent de `multi_model.load_multi_runs`, mais à partir des DataFrames + déjà en mémoire : le site ne relit pas les JSONL une seconde fois. + + Args: + runs: runs chargés par `charger_runs`. + + Returns: + Le DataFrame joint (jointure interne : seuls les items présents dans + tous les runs), avec une colonne `y_pred__` par run. None si + moins de deux runs sont disponibles ou si l'intersection est vide. + """ + if len(runs) < 2: + return None + premier = next(iter(runs.values())) + joint = premier.df[["copy_id", "item_id", "y_true"]].copy() + for label, run in runs.items(): + cols = run.df[["copy_id", "item_id", "y_pred", "confidence"]].rename( + columns={"y_pred": f"y_pred__{label}", "confidence": f"conf__{label}"} + ) + joint = joint.merge(cols, on=["copy_id", "item_id"], how="inner") + if joint.empty: + NOTES.append("Aucun item commun aux runs comparés : la section multi-modèles est omise.") + return None + return joint + + +# ── Écarts par copie ────────────────────────────────────────────────────────── +def classement_ecarts(df: pd.DataFrame) -> pd.DataFrame: + """Copies triées par taux de désaccord décroissant, enrichi du détail utile. + + Args: + df: prédictions à l'item d'un run. + + Returns: + Un DataFrame indexé par copy_id, trié des pires copies aux meilleures : + effectif, nombre et part de désaccords, accord, nombre d'erreurs vues par + l'expert et par le modèle, écart signé sur ce nombre, parts d'items codés + « 0 » (mot absent) et hors grille par l'expert, et — si le run porte le + marqueur du pipeline — la colonne booléenne `vierge`. À part égale de + désaccord, les copies sont triées par identifiant, pour que le classement + soit reproductible. + """ + classement = copies_by_disagreement(df) + copies = per_copy_metrics(df) + out = classement.join(copies[["n_erreurs_expert", "n_erreurs_modele", "confiance_moyenne"]]) + out["ecart_n_erreurs"] = out["n_erreurs_modele"] - out["n_erreurs_expert"] + # Colonnes descriptives : elles éclairent la lecture des fiches mais ne + # servent PAS à écarter des copies (seul le marqueur du pipeline le fait). + out["pct_absent_expert"] = (df["y_true"] == "0").groupby(df["copy_id"]).mean() * 100 + out["pct_hors_grille_expert"] = (~df["y_true"].isin(CODES_VALIDES)).groupby( + df["copy_id"] + ).mean() * 100 + if COLONNE_VIERGE in df.columns: + # Le marqueur est identique sur toutes les lignes d'une copie ; `max` + # ramène simplement le booléen au niveau copie. + out["vierge"] = df[COLONNE_VIERGE].fillna(False).groupby(df["copy_id"]).max().astype(bool) + # Tri stable sur un index déjà ordonné : à taux de désaccord égal, l'ordre + # reste celui des identifiants de copie. + return out.sort_index().sort_values("pct_desaccord", ascending=False, kind="mergesort") + + +def couverture_copies(runs: dict[str, Run]) -> pd.DataFrame: + """Couverture de chaque run : copies évaluées et copies écartées du corpus. + + Une copie manquante n'est pas neutre : le pipeline two-stage écarte les copies + qu'il ne parvient pas à transcrire à l'étape 1, alors que l'end-to-end produit + un codage pour toutes. Comparer les runs à effectif égal exige donc de savoir + combien de copies chacun a laissées de côté. + + Args: + runs: runs chargés par `charger_runs`. + + Returns: + Un DataFrame indexé par libellé de run : nombre de copies évaluées, + nombre et part de copies du corpus (union des runs) non couvertes. + """ + corpus = set() + for run in runs.values(): + corpus |= set(run.df["copy_id"].unique()) + lignes = [] + for label, run in runs.items(): + vues = set(run.df["copy_id"].unique()) + lignes.append( + { + "run": label, + "n_copies": len(vues), + "n_ecartees": len(corpus - vues), + "pct_ecartees": len(corpus - vues) / len(corpus) * 100 if corpus else 0.0, + } + ) + return pd.DataFrame(lignes).set_index("run") + + +def seuil_encre() -> float: + """Seuil de copie vierge du projet, lu dans la config du paquet. + + Returns: + La valeur par défaut de `DataConfig.blank_ink_threshold`, pour que la page + ne puisse pas afficher un seuil périmé si celui du code change. Retombe sur + `SEUIL_ENCRE_PIPELINE` si la config n'est pas importable. + """ + try: + from evaluation_dictee.config import DataConfig + + return float(DataConfig.model_fields["blank_ink_threshold"].default) + except Exception: # noqa: BLE001 — la page doit rester rendable + return SEUIL_ENCRE_PIPELINE + + +def charger_densites_encre(runs: dict[str, Run]) -> tuple[pd.DataFrame | None, str]: + """Densité d'encre par copie, et provenance de la mesure. + + Deux sources, par ordre de préférence : + + 1. la colonne `ink_ratio` du JSONL, écrite par le benchmark en même temps + qu'il applique le seuil — c'est la mesure qui a réellement décidé ; + 2. le CSV produit par `scripts/compute_ink_ratios.py`, qui mesure le corpus + sans appel modèle (utile avant d'avoir relancé le benchmark). + + Args: + runs: runs chargés par `charger_runs`. + + Returns: + Le couple (DataFrame indexé par copy_id avec la colonne `ink_ratio`, + libellé de provenance). Le DataFrame vaut None si aucune source n'est + disponible ; l'incident est alors consigné dans `NOTES`. + """ + for label, run in runs.items(): + if COLONNE_ENCRE in run.df.columns: + serie = run.df.groupby("copy_id")[COLONNE_ENCRE].first().dropna() + if len(serie): + return serie.to_frame(COLONNE_ENCRE), f"prédictions du run {label}" + + # Import local : `fsspec` arrive avec s3fs, mais la page doit rester rendable + # même dans un environnement où le paquet du projet n'est pas installé. + chemin = PREFIX.rstrip("/") + "/" + FICHIER_ENCRE + try: + import fsspec + + with fsspec.open(chemin, "rt", encoding="utf-8") as f: + mesures = pd.read_csv(f, sep=";") + except Exception as exc: # noqa: BLE001 — la page doit rester rendable + NOTES.append( + f"Distribution des densités d'encre indisponible ({type(exc).__name__}) : " + f"section omise. La produire avec `uv run scripts/compute_ink_ratios.py " + f"--config configs/scoring/dictee_end2end.yaml --export`." + ) + return None, "" + if COLONNE_ENCRE not in mesures.columns or "copy_id" not in mesures.columns: + NOTES.append(f"`{FICHIER_ENCRE}` n'a pas les colonnes attendues : section omise.") + return None, "" + return mesures.set_index("copy_id")[[COLONNE_ENCRE]], f"`{FICHIER_ENCRE}`" + + +def marqueur_vierge_disponible(runs: dict[str, Run]) -> bool: + """Vrai si tous les runs portent le marqueur de copie vierge du pipeline. + + Args: + runs: runs chargés par `charger_runs`. + + Returns: + True seulement si chaque run a la colonne `blank`. Les runs exportés avant + l'ajout du filtre de densité d'encre ne l'ont pas : aucune copie ne peut + alors être écartée, et la page doit le signaler au lieu de deviner. + """ + return bool(runs) and all(COLONNE_VIERGE in run.df.columns for run in runs.values()) + + +def separer_copies_vierges(classement: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]: + """Sépare les copies vierges du reste du classement, selon le pipeline. + + Le critère est le marqueur `blank` posé par `pipeline/benchmark.py` d'après la + densité d'encre de l'image. En son absence (run antérieur au garde-fou), rien + n'est écarté : mieux vaut un classement bruité et signalé comme tel qu'un + filtrage improvisé qui divergerait du pipeline. + + Args: + classement: sortie de `classement_ecarts`. + + Returns: + Le couple (copies vierges, copies exploitables). L'ordre du classement est + conservé de part et d'autre ; le premier terme est vide si le run ne porte + pas le marqueur. + """ + if "vierge" not in classement.columns: + return classement.iloc[:0], classement + vierge = classement["vierge"] + return classement[vierge], classement[~vierge] + + +def nature_copie(ligne: pd.Series) -> str: + """Qualifie une copie d'après ce que l'annotateur a pu y coder. + + Sert à annoter les fiches : une copie que l'expert n'a pas pu coder sur une + large part de ses items s'interprète autrement qu'une copie ordinaire, même + quand la densité d'encre ne la classe pas comme vierge (écriture illisible). + + Args: + ligne: une ligne de `classement_ecarts`. + + Returns: + Un libellé court, ou une chaîne vide pour une copie ordinaire. + """ + if bool(ligne.get("vierge", False)): + return "copie vierge selon le pipeline — densité d'encre sous le seuil" + absent = float(ligne["pct_absent_expert"]) + hors = float(ligne["pct_hors_grille_expert"]) + total = absent + hors + if total < 25: + return "" + motif = "illisibles" if hors > absent else "absents" + return ( + f"copie largement incodable — l'expert code {total:.0f} % des items comme " + f"{motif}, alors que la densité d'encre ne la classe pas vierge" + ) + + +def transcription_modele(sub: pd.DataFrame) -> tuple[str, str]: + """Transcription du modèle pour une copie, et sa provenance. + + Args: + sub: lignes de prédiction d'une seule copie, dans l'ordre de la dictée. + + Returns: + Le couple (texte, provenance). La transcription brute de l'étape 1 (HTR) + est prioritaire ; à défaut le texte est recollé item par item. + """ + if "raw_transcription" in sub.columns: + brutes = sub["raw_transcription"].dropna() + brutes = brutes[brutes.astype(str).str.strip() != ""] + if len(brutes): + return str(brutes.iloc[0]).strip(), "étape 1 (HTR), lecture brute du modèle" + if "transcription" in sub.columns: + mots = [str(m) for m in sub["transcription"].fillna("").tolist() if str(m).strip()] + if mots: + return " ".join(mots), "recollée item par item" + return "", "non fournie par ce run" + + +def decomposition_copie(sub: pd.DataFrame) -> str: + """Résume les transitions expert → modèle d'une copie (« 1→9 : 12 ; … »).""" + dis = sub[sub["y_true"] != sub["y_pred"]] + if dis.empty: + return "aucun désaccord" + compte = dis.groupby(["y_true", "y_pred"]).size().sort_values(ascending=False) + return " ; ".join(f"{t or '∅'}→{p} : {n}" for (t, p), n in compte.items()) + + +# ── Rendu HTML des codages ──────────────────────────────────────────────────── +def _puce(mot: str, code: str, classe: str, infobulle: str, detail: str = "") -> str: + """Puce HTML d'un item : mot attendu au-dessus, code en dessous.""" + sup = f"{html.escape(detail)}" if detail else "" + return ( + f"" + f"{html.escape(mot)}" + f"{html.escape(code)}{sup}" + ) + + +def codage_expert_html( + items: list[GridItem], codes_expert: dict[str, str], codes_modele: dict[str, str] +) -> str: + """Bandeau des codes de l'annotateur expert, nuancé par code. + + Args: + items: items de la grille, dans l'ordre de la dictée. + codes_expert: item_id → code expert. + codes_modele: item_id → code modèle (sert à marquer les items divergents). + + Returns: + Le fragment HTML du bandeau de puces. + """ + puces = [] + for it in items: + code = codes_expert.get(it.item_id, "?") + divergent = codes_modele.get(it.item_id, "?") != code + classe = f"puce-code-{code if code in CODES_VALIDES else 'autre'}" + if divergent: + classe += " puce-divergente" + libelle = LIBELLE_CODE.get(code, f"code « {code or 'vide'} »") + puces.append(_puce(it.attendu, code or "∅", classe, f"expert : {libelle}")) + return f"
{''.join(puces)}
" + + +def codage_modele_html( + items: list[GridItem], + codes_expert: dict[str, str], + codes_modele: dict[str, str], + lectures: dict[str, str] | None = None, +) -> str: + """Bandeau des codes du modèle : **vert** si accord avec l'expert, **rouge** sinon. + + Args: + items: items de la grille, dans l'ordre de la dictée. + codes_expert: item_id → code expert. + codes_modele: item_id → code modèle. + lectures: item_id → mot lu par le modèle ; affiché sous la puce quand il + diffère du mot attendu (c'est là que se logent les erreurs de lecture). + + Returns: + Le fragment HTML du bandeau de puces. + """ + lectures = lectures or {} + puces = [] + for it in items: + code_m = codes_modele.get(it.item_id, "?") + code_e = codes_expert.get(it.item_id, "?") + accord = code_m == code_e + classe = "puce-accord" if accord else "puce-desaccord" + infobulle = ( + f"modèle : {LIBELLE_CODE.get(code_m, code_m)} — accord avec l'expert" + if accord + else f"modèle : {LIBELLE_CODE.get(code_m, code_m)} / " + f"expert : {LIBELLE_CODE.get(code_e, code_e or 'vide')}" + ) + lu = str(lectures.get(it.item_id, "") or "").strip() + detail = "" + if not accord: + detail = f"exp. {code_e or '∅'}" + if lu and lu != it.attendu: + detail = f"{detail} · lu « {lu} »" if detail else f"lu « {lu} »" + puces.append(_puce(it.attendu, code_m, classe, infobulle, detail)) + return f"
{''.join(puces)}
" + + +def legende_puces() -> str: + """Légende des couleurs des bandeaux de puces.""" + return ( + "
" + "accord" + "désaccord" + "1 correct" + "9 erreur" + "0 absent" + "
" + ) + + +# Fonctions du paquet ré-exportées telles quelles : les pages passent par ce +# module pour y accéder, afin qu'un import raté n'empêche pas le rendu (le bloc +# try/except ci-dessus est alors le seul point de défaillance). +__all__ = [ + "agreement_per_item", + "confidence_score", + "disagreement_decomposition", + "disagreement_type_summary", + "referral_curve_with_ci", + "wilson_interval", +] diff --git a/website/_quarto.yml b/website/_quarto.yml index 188db40..b133958 100644 --- a/website/_quarto.yml +++ b/website/_quarto.yml @@ -1,6 +1,16 @@ project: type: website output-dir: _site + render: + - "*.qmd" + - "cards/*.qmd" + - "slides/*.qmd" + resources: + - "slides/_extensions/" + - "slides/css/" + - "slides/js/" + - "slides/img/" + - "slides/modulesCompl/" website: title: "Évaluation automatique de la dictée" @@ -14,19 +24,26 @@ website: left: - text: "Accueil" file: index.qmd + - text: "Configuration du SSPCloud" + file: initialisation.qmd - text: "Architecture" file: architecture.qmd - text: "Résultats" file: resultats.qmd + - text: "Écarts IA – humain" + file: ecarts.qmd - text: "Évaluation & métriques" file: evaluation.qmd - text: "Fine-tuning" file: finetuning.qmd + - text: "Présentations" + file: presentations.qmd format: html: theme: - cosmo + - style/styles.scss toc: true toc-title: "Sur cette page" toc-depth: 3 diff --git a/website/architecture.qmd b/website/architecture.qmd index 5f1de40..513ea3e 100644 --- a/website/architecture.qmd +++ b/website/architecture.qmd @@ -1,6 +1,6 @@ --- title: "Architecture du projet" -subtitle: "Du scan de la copie à l'évaluation, étape par étape" +subtitle: "De l'imagett de la copie à l'évaluation, étape par étape" --- ## Vue d'ensemble @@ -38,6 +38,8 @@ Grâce à cette abstraction, la boucle d'évaluation (`pipeline/benchmark.py`) n connaît qu'un `Scorer` générique : ajouter une nouvelle approche demain n'impliquerait de toucher ni les métriques, ni le suivi, ni les notebooks. +![Schéma d'appel au LLM](/img/call_llm.png) + ## Le déroulé d'un run Le module `pipeline/benchmark.py` orchestre chaque run : @@ -92,7 +94,7 @@ evaluation_dictee/ Chaque run est tracé, pour qu'on puisse toujours retrouver *quoi* a produit *quel* résultat : -- **Langfuse** (inférence et évaluation) : une **session** par lancement → une +- **Langfuse** (https://langfuse.lab.sspcloud.fr/) : une **session** par lancement → une **trace** par copie (entrée, sortie, score d'accord) → une **génération** par appel au modèle. Les métriques agrégées y sont enregistrées. - **MLflow** (fine-tuning sur GPU) : les courbes d'entraînement (perte, taux diff --git a/website/cards/01-presentation-generale.qmd b/website/cards/01-presentation-generale.qmd new file mode 100644 index 0000000..6014ba3 --- /dev/null +++ b/website/cards/01-presentation-generale.qmd @@ -0,0 +1,12 @@ +--- +title: "Évaluation automatique de la production d'écrits — Point d'étape du 27/07/2026" +image: /img/cards/logo_copies_eleves_pile_corrigee.svg +date: "2026-07-27" +description: | + Description : Vue d'ensemble du projet, premiers tests réalisés par le SSP Lab (pipeline, choix, premiers résultats). +author: Laura Gaimard +slides: + text: Slides + url: /slides/presentation-generale.html +order: 1 +--- diff --git a/website/ecarts.qmd b/website/ecarts.qmd new file mode 100644 index 0000000..2941c48 --- /dev/null +++ b/website/ecarts.qmd @@ -0,0 +1,739 @@ +--- +title: "Analyse des plus grands écarts d'évaluation IA – humain" +subtitle: "Les 20 copies sur lesquelles le modèle et l'annotateur expert divergent le plus" +toc-depth: 2 +--- + +```{python} +#| echo: false +#| warning: false +# Même câblage que la page Résultats : on relit les prédictions exportées et on +# recalcule tout au rendu. Voir `website/_analyse.py`. +import sys + +sys.path.insert(0, ".") + +import matplotlib.pyplot as plt +import numpy as np + +import _analyse as A + +A.init_matplotlib() + +#: Nombre de copies détaillées. +N_PIRES = 20 + +RUNS = A.charger_runs() +ITEMS, MOTS, RANGS = A.charger_grille() +REF = A.RUN_REFERENCE if A.RUN_REFERENCE in RUNS else (next(iter(RUNS)) if RUNS else "") + +# Les copies vierges sont écartées d'emblée : leur désaccord est maximal parce que +# l'élève n'a rien écrit, ce qui ne dit rien de la qualité du codage. Le critère +# est celui du pipeline (densité d'encre), relu depuis le marqueur `blank` du +# JSONL. Elles sont récapitulées dans leur propre section, plus bas. +BRUTS = {label: A.classement_ecarts(r.df) for label, r in RUNS.items()} +VIERGES, CLASSEMENTS = {}, {} +for label, classement in BRUTS.items(): + VIERGES[label], CLASSEMENTS[label] = A.separer_copies_vierges(classement) +MARQUEUR_OK = A.marqueur_vierge_disponible(RUNS) + +SEUIL_ENCRE = A.seuil_encre() +ENCRE, SOURCE_ENCRE = A.charger_densites_encre(RUNS) +``` + +::: {.callout-important} +## Les copies numérisées ne sont pas affichées ici + +Les copies sont des écritures d'élèves **mineurs**. Conformément à la règle du +projet, ces microdonnées ne quittent jamais le SSP Cloud et ne sont jamais +versionnées : **ce site ne contient aucune image de copie**, y compris quand il +est rendu à l'intérieur de l'environnement sécurisé. + +Cette page fournit donc, pour chaque copie, tout le reste : les statistiques +d'écart, la **transcription produite par le modèle**, le **codage de +l'annotateur expert** et le **codage du modèle**, désaccords en rouge et accords +en vert. + +Pour examiner une copie **avec son image**, générer le diagnostic visuel dans +l'environnement sécurisé — il produit un HTML autonome, à ne pas sortir du SSP +Cloud : + +```python +from evaluation_dictee.evaluation.visual_diff import report_single_copy + +report_single_copy( + copy_id="dictee_2015_0042.png", + predictions_df=predictions, # load_predictions(_predictions.jsonl) + grid_items=grille.items, + images_dir="s3://projet-production-ecrits-depp/dictee_2015/", + expert_labels=labels, + output_path="data/processed/diagnostic_copie.html", +) +``` + +Voir aussi `notebooks/04_diagnostic.ipynb`. +::: + +```{python} +#| echo: false +#| output: asis +if not RUNS: + print('::: {.callout-warning appearance="simple"}') + print("Aucun run de prédictions n'a pu être chargé : cette page est vide.") + print("Vérifier `S3_PREDICTIONS_PREFIX` et l'export des prédictions.") + print(":::") + print(A.bloc_notes()) +``` + +## Comment les copies sont classées + +Le critère est le **taux de désaccord** : la part des 83 items de la copie où le +code du modèle diffère de celui de l'annotateur expert. C'est la mesure la plus +directe de l'écart d'évaluation, item par item. + +Les **copies vierges sont écartées du classement**, au sens exact où le pipeline +l'entend : le benchmark mesure la **densité d'encre** de l'image +(`data/loaders.ink_ratio`) et, sous le seuil `data.blank_ink_threshold` du YAML du +run (2,5 % par défaut — seul le pré-imprimé marque), déclare la copie vierge, la +code « 0 » partout sans appeler le modèle, et estampille chaque ligne du JSONL +d'un `blank: true`. Le site **relit ce marqueur** plutôt que d'appliquer son +propre critère : une seule définition, celle du pipeline. Ces copies sont traitées +à part dans la [section suivante](#copies-vierges), car le mode de défaillance +qu'elles révèlent compte en soi. + +::: {.callout-note appearance="simple"} +Un second critère est possible : l'écart sur le **nombre total d'erreurs** +attribuées à la copie (`|erreurs modèle − erreurs expert|`), qui cible les copies +où le modèle se trompe sur le *niveau* de l'élève même quand les désaccords +item par item se compensent. Il est affiché en colonne dans les tableaux +ci-dessous, pour comparaison. +::: + +## Copies vierges, écartées du classement {#copies-vierges} + +```{python} +#| echo: false +#| output: asis +if RUNS and not MARQUEUR_OK: + sans = [label for label, r in RUNS.items() if A.COLONNE_VIERGE not in r.df.columns] + print('::: {.callout-warning appearance="default"}') + print("## Ces runs sont antérieurs au filtre de densité d'encre\n") + print( + "Le marqueur `blank` est absent des prédictions exportées pour : " + + ", ".join(f"**{label}**" for label in sans) + + ". Ces runs ont été produits avant que `pipeline/benchmark.py` ne " + "détecte les copies vierges, **aucune copie n'est donc écartée ici** et le " + "haut du classement est saturé de copies vides — le modèle y a bel et bien " + "été appelé, et il a halluciné le texte de référence.\n" + ) + print( + "Le correctif est déjà dans le code : il suffit de **relancer le " + "benchmark**, puis de réexporter. Les copies effectivement sous le seuil " + "d'encre seront alors codées « 0 » sans appel modèle — donc en accord avec " + "l'annotateur quand celui-ci les a aussi jugées absentes — et quitteront le " + "haut de ce classement. Les copies portant un peu d'encre (un prénom, une " + "amorce de phrase) resteront hors du filtre : c'est le seuil " + "`blank_ink_threshold` qui arbitre, pas le codage de l'expert.\n" + ) + print("```bash") + print("uv run scripts/run_benchmark.py --config configs/scoring/dictee_end2end.yaml") + print("uv run scripts/export_predictions.py --config configs/scoring/dictee_end2end.yaml") + print("```") + print(":::") +``` + +```{python} +#| echo: false +#| output: asis +if RUNS and MARQUEUR_OK: + lignes = [] + for label, vierges in VIERGES.items(): + lignes.append( + [ + f"Approche {label}", + A.milliers(len(vierges)), + A.pts(len(vierges) / len(BRUTS[label]) * 100, 2), + A.pts(vierges["pct_desaccord"].mean()) if not vierges.empty else "—", + A.pts(BRUTS[label]["pct_desaccord"].mean()), + ] + ) + print( + A.table_markdown( + [ + "", "Copies vierges", "Part du corpus", + "Désaccord moyen sur ces copies", "Désaccord moyen d'ensemble", + ], + lignes, + "Copies dont la densité d'encre est sous le seuil du pipeline", + ) + ) + print( + "\nLe pipeline code ces copies « 0 » sur tous les items sans appeler " + "le modèle : leur désaccord avec l'annotateur ne mesure plus le codage, mais " + "la concordance entre le filtre d'encre et le jugement de l'expert." + ) +``` + +### D'où vient le seuil de densité d'encre + +```{python} +#| echo: false +#| output: asis +if ENCRE is None: + print('::: {.callout-note appearance="simple"}') + print( + "La distribution des densités d'encre n'est pas disponible : ni les " + "prédictions ni le corpus mesuré ne la portent. La produire en quelques " + "minutes, sans appel modèle ni GPU :\n" + ) + print("```bash") + print("uv run scripts/compute_ink_ratios.py \\") + print(" --config configs/scoring/dictee_end2end.yaml --export") + print("```") + print(":::") +else: + print( + f"Le seuil du projet vaut **{A.pct(SEUIL_ENCRE)}** de pixels sombres " + f"(`data.blank_ink_threshold`). Il n'a pas été choisi par tâtonnement : la " + "distribution ci-dessous montre qu'il tombe dans un **intervalle vide**, " + "entre les formulaires non remplis et les copies écrites. Toute valeur de " + "cet intervalle donnerait exactement le même partage — le classement ne " + "dépend donc pas du réglage fin du seuil.\n" + ) + print(f"Source des mesures : {SOURCE_ENCRE}.") +``` + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Densité d'encre des copies. À gauche, la distribution sur tout le corpus (échelle verticale logarithmique, sans quoi le petit mode des copies vierges serait invisible). À droite, les 120 copies les moins encrées, classées : la marche verticale marque la frontière entre formulaires non remplis et copies écrites." +if ENCRE is not None: + # Croiser la mesure avec le codage de l'annotateur : si les copies que le seuil + # écarte sont exactement celles où l'expert n'a rien pu coder, la mesure + # d'encre et le jugement humain concordent. + ref = BRUTS[REF][["pct_absent_expert", "pct_hors_grille_expert"]] + encre = ENCRE.join(ref, how="inner") + encre["incodable_expert"] = ( + encre["pct_absent_expert"] + encre["pct_hors_grille_expert"] + ) >= 90 + sous = encre[encre[A.COLONNE_ENCRE] < SEUIL_ENCRE] + sur = encre[encre[A.COLONNE_ENCRE] >= SEUIL_ENCRE] + borne_basse = sous[A.COLONNE_ENCRE].max() if not sous.empty else None + borne_haute = sur[A.COLONNE_ENCRE].min() if not sur.empty else None + + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5.4)) + + # ── Panneau gauche : distribution d'ensemble ───────────────────────────── + ax1.hist( + encre[A.COLONNE_ENCRE], bins=120, color=A.C_EXPERT, + edgecolor="white", linewidth=0.2, + ) + # Ombrer la zone « vierge » sans étendre l'axe jusqu'à 0 : on part de la borne + # gauche que l'histogramme a fixée d'après les données. + ax1.axvspan(ax1.get_xlim()[0], SEUIL_ENCRE, color=A.C_PB, alpha=0.07) + ax1.axvline(SEUIL_ENCRE, color=A.C_PB, lw=2, ls="--") + # Texte cantonné au coin haut gauche, libre de barres : aucune flèche ne + # traverse les données. + ax1.text( + 0.02, 0.96, + f"seuil {A.pct(SEUIL_ENCRE)}\n← {len(sous)} copies vierges", + transform=ax1.transAxes, va="top", ha="left", + fontsize=9.5, color=A.C_PB, fontweight="bold", + bbox=dict(boxstyle="round,pad=0.4", facecolor="white", edgecolor=A.C_PB, alpha=0.9), + ) + ax1.set_xlabel("Densité d'encre (part de pixels sombres)") + ax1.set_ylabel("Nombre de copies (échelle logarithmique)") + ax1.set_yscale("log") + ax1.set_title("Distribution sur tout le corpus", fontweight="bold") + + # ── Panneau droit : les copies les moins encrées, classées ─────────────── + # Une courbe de valeurs triées rend la rupture lisible sans recouvrement, + # là où un nuage dispersé noierait les quelques dizaines de copies vierges. + n_zoom = 120 + tri = encre.sort_values(A.COLONNE_ENCRE).head(n_zoom).reset_index() + rangs = np.arange(1, len(tri) + 1) + if borne_basse is not None and borne_haute is not None: + ax2.axhspan(borne_basse, borne_haute, color="grey", alpha=0.18) + # Étiquette posée juste au-dessus de la bande, pour ne pas chevaucher la + # ligne de seuil qui la traverse. + ax2.annotate( + f"aucune copie entre {borne_basse:.2%} et {borne_haute:.2%}", + xy=(len(tri) * 0.99, borne_haute), + xytext=(0, 4), textcoords="offset points", + ha="right", va="bottom", fontsize=9, color="#333", fontstyle="italic", + ) + ax2.axhline(SEUIL_ENCRE, color=A.C_PB, lw=2, ls="--", label=f"seuil {A.pct(SEUIL_ENCRE)}") + for masque, couleur, etiquette in [ + (~tri["incodable_expert"], A.C_EXPERT, "l'expert a codé la copie"), + (tri["incodable_expert"], A.C_PB, "l'expert n'a rien pu coder (≥ 90 % des items)"), + ]: + ax2.scatter( + rangs[masque.values], tri.loc[masque, A.COLONNE_ENCRE], + s=30, color=couleur, edgecolors="white", linewidths=0.4, + zorder=3, label=etiquette, + ) + ax2.set_xlabel(f"Rang de la copie, des {n_zoom} moins encrées vers les plus encrées") + ax2.set_ylabel("Densité d'encre") + ax2.set_title(f"Les {n_zoom} copies les moins encrées", fontweight="bold") + ax2.legend(fontsize=8.5, loc="lower right") + plt.tight_layout() + plt.show() +``` + +```{python} +#| echo: false +#| output: asis +if ENCRE is not None: + accord_expert = int(sous["incodable_expert"].sum()) + faux_positifs = int((~sous["incodable_expert"]).sum()) + manques = int(sur["incodable_expert"].sum()) + lignes = [ + [ + "Copies sous le seuil (vierges)", + A.milliers(len(sous)), + A.pts(len(sous) / len(encre) * 100, 2), + f"{sous[A.COLONNE_ENCRE].min():.2%} – {sous[A.COLONNE_ENCRE].max():.2%}", + ], + [ + "Copies au-dessus du seuil", + A.milliers(len(sur)), + A.pts(len(sur) / len(encre) * 100, 2), + f"{sur[A.COLONNE_ENCRE].min():.2%} – {sur[A.COLONNE_ENCRE].max():.2%}", + ], + ] + print( + A.table_markdown( + ["", "Copies", "Part du corpus", "Densité d'encre observée"], + lignes, + "Partage du corpus par le seuil de densité d'encre", + ) + ) + print( + f"\n**Concordance avec l'annotateur** — parmi les {len(sous)} copies que le " + f"seuil déclare vierges, **{accord_expert}** sont aussi celles où l'expert " + f"n'a pu coder aucun item (au moins 90 % d'items « absent » ou hors " + f"grille) ; {faux_positifs} portent trop peu d'encre alors que l'expert y a " + f"vu quelque chose. À l'inverse, {manques} copies dépassent le seuil bien " + "que l'expert n'ait rien pu y coder — écriture illisible ou trop pâle, que " + "la densité d'encre ne peut pas distinguer d'une copie écrite. Ces cas " + "restent donc dans le classement, signalés dans leur fiche." + ) +``` + +::: {.callout-important} +## Le mode de défaillance le plus net du modèle + +Sur une copie vide, le modèle **reconstitue le texte de la dictée de mémoire et +code tous les items « correct »**. Il connaît le texte de référence — il fait +partie de son prompt — et le restitue au lieu de constater qu'il n'y a rien à +lire. C'est la forme extrême du biais de **sur-correction** : non seulement le +modèle ne voit pas les erreurs de l'élève, mais il fabrique une copie parfaite là +où il n'y a pas de copie du tout. + +Deux conséquences pratiques : + +1. **Le filtre en amont est la bonne réponse**, et il existe déjà : le seuil de + densité d'encre (`data.blank_ink_threshold`) code ces copies « 0 » sans + solliciter le modèle. Le problème relève du pré-traitement, pas de la finesse + du codage. +2. **L'approche two-stage se comporte mieux sans ce filtre** : ne parvenant pas à + produire de transcription, elle **écarte** ces copies plutôt que d'inventer + une réponse. Un refus explicite vaut mieux qu'une erreur silencieuse. +::: + +```{python} +#| echo: false +#| output: asis +if RUNS and MARQUEUR_OK and any(not v.empty for v in VIERGES.values()): + print("::: {.panel-tabset}") + for label, vierges in VIERGES.items(): + print(f"\n## Approche {label}\n") + if vierges.empty: + print("*Aucune copie vierge détectée dans ce run.*\n") + continue + lignes = [ + [ + f"`{copy_id}`", + A.pts(ligne["pct_absent_expert"]), + A.pts(ligne["pct_hors_grille_expert"]), + A.pts(ligne["pct_desaccord"]), + str(int(ligne["n_erreurs_expert"])), + ] + for copy_id, ligne in vierges.iterrows() + ] + print( + A.table_markdown( + [ + "Copie", "Items codés « absent »", "Items hors grille", "Désaccord", + "Erreurs vues par l'expert", + ], + lignes, + f"Copies vierges — approche {label}", + ) + ) + print("\n:::") + print( + "\nAucune fiche détaillée n'est produite pour ces copies : le codage " + "y est automatique (« 0 » partout) et donc identique d'une copie à l'autre. " + "Le désaccord résiduel mesure les cas où l'annotateur a vu quelque chose " + "que le filtre d'encre a manqué, ou l'inverse. Le classement et les fiches " + "ci-dessous portent sur les copies exploitables." + ) +``` + +## Les copies les plus divergentes + +```{python} +#| echo: false +#| output: asis +if RUNS: + lignes = [] + for label, classement in CLASSEMENTS.items(): + pires = classement.head(N_PIRES) + lignes.append( + [ + f"Approche {label}", + A.milliers(len(classement)), + A.pts(classement["pct_desaccord"].mean()), + A.pts(classement["pct_desaccord"].median()), + A.pts(pires["pct_desaccord"].min()), + A.pts(pires["pct_desaccord"].max()), + A.pts(pires["n_desaccords"].sum() / classement["n_desaccords"].sum() * 100), + ] + ) + print( + A.table_markdown( + [ + "", "Copies", "Désaccord moyen", "Désaccord médian", + f"Seuil d'entrée au top {N_PIRES}", "Pire copie", + f"Part des désaccords portée par le top {N_PIRES}", + ], + lignes, + f"Où se situent les {N_PIRES} pires copies dans le corpus classé", + ) + ) + premier = CLASSEMENTS[REF] + part = premier.head(N_PIRES)["n_desaccords"].sum() / premier["n_desaccords"].sum() * 100 + print( + f"\n**Lecture** : les {N_PIRES} pires copies ne portent que " + f"{A.pts(part)} des désaccords totaux de l'approche {REF}. Les écarts ne " + "sont donc **pas concentrés sur une poignée de copies aberrantes** : ils " + "sont diffus sur tout le corpus. Corriger ces cas extrêmes ne réglerait " + "pas le problème de fond — mais ils restent le meilleur endroit pour " + "*comprendre* les modes de défaillance du modèle." + ) +``` + +```{python} +#| echo: false +#| output: asis +# Une seule réserve doit être dite avant de lire les fiches : si plus de N_PIRES +# copies sont à égalité au sommet, la sélection parmi elles est arbitraire. +if RUNS: + alertes = [] + for label, classement in CLASSEMENTS.items(): + maxi = classement["pct_desaccord"].max() + n_ex_aequo = int((classement["pct_desaccord"] >= maxi - 1e-9).sum()) + if n_ex_aequo > N_PIRES: + alertes.append( + f"**Approche {label}** — {n_ex_aequo} copies sont à égalité au " + f"désaccord maximal ({A.pts(maxi)}) : les {N_PIRES} retenues sont " + "les premières par identifiant, un choix arbitraire parmi elles." + ) + if alertes: + print('::: {.callout-note appearance="simple"}') + print("**Ex æquo au sommet du classement**\n") + for alerte in alertes: + print(f"- {alerte}") + print(":::") +``` + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Distribution du taux de désaccord par copie. La zone ombrée est celle des 20 copies détaillées ci-dessous." +if RUNS: + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13.5, 4.8)) + for label, classement in CLASSEMENTS.items(): + couleur = RUNS[label].couleur + ax1.hist( + classement["pct_desaccord"], bins=40, color=couleur, alpha=0.55, + edgecolor="white", linewidth=0.3, label=f"Approche {label}", + ) + seuil = classement.head(N_PIRES)["pct_desaccord"].min() + ax1.axvspan(seuil, 100, color=couleur, alpha=0.10) + ax1.axvline(seuil, color=couleur, ls="--", lw=1.4) + ax1.set_xlabel("Taux de désaccord de la copie (%)") + ax1.set_ylabel("Nombre de copies") + ax1.set_title(f"Où se situent les {N_PIRES} pires copies", fontweight="bold") + ax1.legend(fontsize=8.5) + + # Courbe de concentration : les désaccords sont-ils portés par peu de copies ? + for label, classement in CLASSEMENTS.items(): + cumul = classement["n_desaccords"].cumsum() / classement["n_desaccords"].sum() * 100 + part_copies = np.arange(1, len(classement) + 1) / len(classement) * 100 + ax2.plot(part_copies, cumul, color=RUNS[label].couleur, lw=2.2, label=f"Approche {label}") + ax2.plot([0, 100], [0, 100], color="grey", ls="--", lw=1, label="répartition uniforme") + ax2.set_xlabel("% de copies, des plus divergentes aux plus fidèles") + ax2.set_ylabel("% cumulé des désaccords") + ax2.set_title("Concentration des désaccords", fontweight="bold") + ax2.legend(fontsize=8.5, loc="lower right") + plt.tight_layout() + plt.show() +``` + +### Classement + +```{python} +#| echo: false +#| output: asis +if RUNS: + print("::: {.panel-tabset}") + for label, classement in CLASSEMENTS.items(): + print(f"\n## Approche {label}\n") + lignes = [] + for rang, (copy_id, ligne) in enumerate(classement.head(N_PIRES).iterrows(), start=1): + lignes.append( + [ + str(rang), + f"`{copy_id}`", + A.pts(ligne["pct_desaccord"]), + f"{int(ligne['n_desaccords'])} / {int(ligne['n_items'])}", + str(int(ligne["n_erreurs_expert"])), + str(int(ligne["n_erreurs_modele"])), + f"{int(ligne['ecart_n_erreurs']):+d}", + A.pts(ligne["pct_absent_expert"] + ligne["pct_hors_grille_expert"]), + ] + ) + print( + A.table_markdown( + [ + "Rang", "Copie", "Désaccord", "Items en désaccord", + "Erreurs — expert", "Erreurs — modèle", "Écart", + "Items incodables (expert)", + ], + lignes, + f"Les {N_PIRES} copies les plus divergentes — approche {label}", + ) + ) + print("\n:::") +``` + +```{python} +#| echo: false +#| output: asis +if len(RUNS) > 1 and REF: + autres = [label for label in CLASSEMENTS if label != REF] + rangs_ref = CLASSEMENTS[REF].head(N_PIRES).index + communs = { + label: len(set(rangs_ref) & set(CLASSEMENTS[label].head(N_PIRES).index)) + for label in autres + } + detail = " ; ".join( + f"**{n} copies sur {N_PIRES}** en commun avec l'approche {label}" + for label, n in communs.items() + ) + print( + f"Les deux approches ne butent pas sur les mêmes copies : {detail}. " + "Les modes de défaillance sont donc largement **spécifiques à " + "l'architecture**, ce qui explique que leur désaccord mutuel soit un bon " + "signal d'incertitude (voir la page [Résultats](resultats.qmd))." + ) +``` + +### Quels items concentrent ces écarts ? + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Items les plus impliqués dans les désaccords des 20 pires copies." +if RUNS and ITEMS: + fig, ax = plt.subplots(figsize=(10, 6.5)) + ref_run = RUNS[REF] + pires_ids = CLASSEMENTS[REF].head(N_PIRES).index + sub = ref_run.df[ref_run.df["copy_id"].isin(pires_ids)] + desaccords = sub[sub["y_true"] != sub["y_pred"]] + compte = desaccords["item_id"].value_counts().head(20).sort_values() + couleurs = [ + A.C_PB if v >= 0.75 * N_PIRES else A.C_MOYEN if v >= 0.5 * N_PIRES else A.C_EXPERT + for v in compte.values + ] + ax.barh( + [A.libelle_item(i, MOTS, RANGS) for i in compte.index], + compte.values, color=couleurs, edgecolor="k", linewidth=0.3, + ) + for i, v in enumerate(compte.values): + ax.annotate( + f"{v} / {N_PIRES}", (v, i), textcoords="offset points", + xytext=(4, 0), va="center", fontsize=8, + ) + ax.set_xlabel(f"Nombre de copies (sur les {N_PIRES} pires) où l'item est en désaccord") + ax.set_title(f"Items les plus problématiques — approche {REF}", fontweight="bold") + ax.set_xlim(0, N_PIRES * 1.12) + plt.tight_layout() + plt.show() +``` + +## Les 20 copies en détail + +```{python} +#| echo: false +#| output: asis +if RUNS and ITEMS: + exclusion = ( + " — copies vierges exclues au seuil de densité d'encre du pipeline" + if MARQUEUR_OK + else " — **copies vierges comprises**, ce run étant antérieur au filtre de " + "densité d'encre (voir l'avertissement plus haut)" + ) + print('::: {.callout-note appearance="simple"}') + print( + f"Les fiches ci-dessous portent sur les {N_PIRES} copies les plus " + f"divergentes de l'approche {REF}, l'approche privilégiée du projet" + f"{exclusion}. Chaque fiche compare le codage de l'expert à celui des " + "**deux** approches, ce qui permet de voir si l'erreur est partagée ou " + "spécifique à une architecture. Le classement de l'autre approche est donné " + "dans l'onglet correspondant plus haut." + ) + print(":::") + + # Couverture : signaler d'emblée combien des fiches ne pourront montrer + # qu'une seule approche, et pourquoi. + couverture = A.couverture_copies(RUNS) + pires_ids = set(CLASSEMENTS[REF].head(N_PIRES).index) + absentes = { + label: len(pires_ids - set(r.df["copy_id"].unique())) + for label, r in RUNS.items() + if label != REF + } + manquantes = {label: n for label, n in absentes.items() if n} + if manquantes: + detail = " ; ".join( + f"**{n} des {N_PIRES}** pour l'approche {label}" for label, n in manquantes.items() + ) + print('::: {.callout-warning appearance="simple"}') + print( + f"**Toutes les fiches ne montrent pas les deux approches** : {detail}. " + "Ces copies ont été **écartées** par le run concerné, qui n'a produit " + "aucun codage pour elles — le two-stage abandonne celles dont l'étape 1 " + "ne rend aucune transcription. C'est en soi un résultat : sur les " + "copies les plus difficiles, cette approche **refuse de répondre** " + "plutôt que de produire un codage arbitraire. Sur l'ensemble du corpus, " + f"{A.milliers(couverture['n_ecartees'].max())} copies sont dans ce cas." + ) + print(":::") + print(A.legende_puces()) +elif RUNS and not ITEMS: + print('::: {.callout-warning appearance="simple"}') + print("La grille de codage n'a pas pu être chargée : les fiches par copie,") + print("qui ont besoin du mot attendu de chaque item, sont omises.") + print(":::") +``` + +```{python} +#| echo: false +#| output: asis +if RUNS and ITEMS and REF: + for rang, copy_id in enumerate(CLASSEMENTS[REF].head(N_PIRES).index, start=1): + stats_ref = CLASSEMENTS[REF].loc[copy_id] + print(f"\n### {rang}. `{copy_id}` — {A.pts(stats_ref['pct_desaccord'])} de désaccord\n") + nature = A.nature_copie(stats_ref) + if nature: + print(f'::: {{.callout-warning appearance="simple"}}\n{nature.capitalize()}.\n:::\n') + + # ── Statistiques de la copie, expert vs chaque approche ────────────── + codes_expert = {} + lignes_stats = [] + blocs = {} + for label, r in RUNS.items(): + sub = r.df[r.df["copy_id"] == copy_id] + if sub.empty: + # Absence explicite : ce run n'a produit aucun codage pour la copie. + lignes_stats.append( + [f"Approche {label}", *["—"] * 4, "*copie écartée par ce run*"] + ) + continue + sub = sub.set_index("item_id") + codes_expert = {i: str(c) for i, c in sub["y_true"].items()} or codes_expert + codes_modele = {i: str(c) for i, c in sub["y_pred"].items()} + lectures = ( + {i: str(t) for i, t in sub["transcription"].fillna("").items()} + if "transcription" in sub.columns + else {} + ) + n_dis = int((sub["y_true"] != sub["y_pred"]).sum()) + texte, provenance = A.transcription_modele(sub) + blocs[label] = (codes_modele, lectures, texte, provenance) + lignes_stats.append( + [ + f"Approche {label}", + A.pct((len(sub) - n_dis) / len(sub)), + f"{n_dis} / {len(sub)}", + str(int((sub["y_true"] != "1").sum())), + str(int((sub["y_pred"] != "1").sum())), + A.decomposition_copie(sub.reset_index()), + ] + ) + print( + A.table_markdown( + [ + "", "Accord", "Items en désaccord", "Erreurs vues par l'expert", + "Erreurs vues par le modèle", "Transitions expert → modèle", + ], + lignes_stats, + ) + ) + + # ── Transcription lue par le modèle ────────────────────────────────── + for label, (_, _, texte, provenance) in blocs.items(): + print( + f"\n**Transcription par le modèle — approche {label}** " + f"({provenance})\n" + ) + if texte: + print("> " + texte.replace("\n", "\n> ")) + else: + print("> *(ce run ne fournit pas de transcription)*") + + # ── Codages comparés ───────────────────────────────────────────────── + codes_ref_modele = blocs.get(REF, ({}, {}, "", ""))[0] + print("\n**Codage de l'annotateur expert**\n") + print(A.codage_expert_html(ITEMS, codes_expert, codes_ref_modele)) + for label, (codes_modele, lectures, _, _) in blocs.items(): + print( + f"\n**Codage par le modèle — approche {label}** " + "(vert = accord avec l'expert, rouge = désaccord)\n" + ) + print(A.codage_modele_html(ITEMS, codes_expert, codes_modele, lectures)) + print("\n---") +``` + +```{python} +#| echo: false +#| output: asis +print(A.bloc_notes()) +``` + +## Comment lire ces fiches + +- **Puce grise / jaune / sombre** dans le bandeau de l'expert : le code retenu + par l'annotateur (`1` correct, `9` erreur, `0` mot absent). Un liseré signale + les items où le modèle de référence diverge. +- **Puce verte** dans un bandeau modèle : le modèle a mis le même code que + l'expert. **Puce rouge** : il a mis un code différent, et la puce indique + alors le code attendu (`exp. 9`, par exemple). +- **`lu « … »`** sous une puce : le mot que le modèle a *lu* diffère du mot + attendu. C'est le signe d'une erreur de **lecture** plutôt que de **jugement** — + la distinction que l'approche en deux étapes cherche à rendre visible. +- Survoler une puce affiche le détail (code du modèle et code de l'expert). + +::: {.callout-tip appearance="simple"} +Trois modes de défaillance récurrents, à vérifier sur ces fiches : + +1. **Décalage propagé** — l'élève oublie un mot ou saute une ligne ; le modèle + décale tous les items suivants. Le désaccord explose en fin de dictée. +2. **Sur-correction** — le modèle « lit » le mot attendu là où l'élève a écrit + autre chose, et valide donc à tort (le biais VLM le plus coûteux ici). +3. **Sur-détection des absences** — le modèle code `0` (mot absent) une écriture + qu'il n'arrive pas à déchiffrer, au lieu de la juger. +::: diff --git a/website/evaluation.qmd b/website/evaluation.qmd index b7aa36b..c2ad448 100644 --- a/website/evaluation.qmd +++ b/website/evaluation.qmd @@ -1,6 +1,6 @@ --- title: "Évaluation de la prédiction" -subtitle: "Comment assurer la fiabilité de la prédiction " +subtitle: "Comment assurer la fiabilité de la prédiction ?" --- Évaluer, ici, ce n'est pas seulement « le modèle a-t-il raison ». C'est répondre à diff --git a/website/img/call_llm.png b/website/img/call_llm.png new file mode 100644 index 0000000..a8a2092 Binary files /dev/null and b/website/img/call_llm.png differ diff --git a/website/img/cards/logo-insee.svg b/website/img/cards/logo-insee.svg new file mode 100644 index 0000000..117b89a --- /dev/null +++ b/website/img/cards/logo-insee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/img/cards/logo_copies_eleves_pile_corrigee.svg b/website/img/cards/logo_copies_eleves_pile_corrigee.svg new file mode 100644 index 0000000..5dbea62 --- /dev/null +++ b/website/img/cards/logo_copies_eleves_pile_corrigee.svg @@ -0,0 +1,14 @@ +Logo : pile de copies d'élèves corrigéesTrois feuilles copies légèrement décalées en pile, la feuille du dessus montrant une marge rouge, des lignes d'écriture et une coche de correction rouge. + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/img/create_secret.png b/website/img/create_secret.png new file mode 100644 index 0000000..29d5993 Binary files /dev/null and b/website/img/create_secret.png differ diff --git a/website/img/key_llmlab1.png b/website/img/key_llmlab1.png new file mode 100644 index 0000000..1f13970 Binary files /dev/null and b/website/img/key_llmlab1.png differ diff --git a/website/img/key_llmlab2.png b/website/img/key_llmlab2.png new file mode 100644 index 0000000..00a09ec Binary files /dev/null and b/website/img/key_llmlab2.png differ diff --git a/website/img/langfuse_key.png b/website/img/langfuse_key.png new file mode 100644 index 0000000..260134c Binary files /dev/null and b/website/img/langfuse_key.png differ diff --git a/website/img/langfuse_secret.png b/website/img/langfuse_secret.png new file mode 100644 index 0000000..614c8d0 Binary files /dev/null and b/website/img/langfuse_secret.png differ diff --git a/website/img/overview_pipeline.png b/website/img/overview_pipeline.png new file mode 100644 index 0000000..ccca322 Binary files /dev/null and b/website/img/overview_pipeline.png differ diff --git a/website/index.qmd b/website/index.qmd index e57beee..948f052 100644 --- a/website/index.qmd +++ b/website/index.qmd @@ -73,7 +73,8 @@ Ces deux approches sont détaillées dans la page **[Résultats](resultats.qmd)* | Page | Ce que vous y trouverez | |------|-------------------------| | **[Architecture](architecture.qmd)** | Comment le projet est organisé : du scan à la métrique, brique par brique. | -| **[Résultats](resultats.qmd)** | Les deux approches en détail, et les tableaux de comparaison (à compléter). | +| **[Résultats](resultats.qmd)** | Les deux approches en détail, les tableaux de comparaison et l'analyse complète du benchmark. | +| **[Écarts IA – humain](ecarts.qmd)** | Les 20 copies où le modèle et l'annotateur expert divergent le plus, décortiquées item par item. | | **[Évaluation & métriques](evaluation.qmd)** | Chaque métrique expliquée : ce qu'elle mesure et pourquoi. | | **[Fine-tuning](finetuning.qmd)** | Spécialiser un modèle à l'écriture d'enfants. | diff --git a/website/initialisation.qmd b/website/initialisation.qmd new file mode 100644 index 0000000..5ac82cc --- /dev/null +++ b/website/initialisation.qmd @@ -0,0 +1,94 @@ +--- +title: "Initialisation du projet" +subtitle: "Comment installer le projet afin de pouvoir l'exécuter ?" +--- + +Cette page permet l'installation et la configuration du projet d'évaluation automatique des écrits d'élèves. + + +# 1 - Récupération des jetons de connexion des outils utilisés + +## A - Clés langfuse + +Il faut : + +- D'abord se connecter sur https://langfuse.lab.sspcloud.fr/, +- puis cliquer sur **"Go to project"** sur le projet **evaluation-ecrits-depp**. + +Ensuite, il faut (voir la capture d'écran ci-dessous) : + +- cliquer sur 1 - **"Settings"** (en bas à gauche de l'interface), +- puis 2 - **"API keys"**, +- puis sur 3 - **"Create new API keys"** +- et enfin sur **"Create API keys"** + +![Copie d'écran de l'interface langfuse](/img/langfuse_key.png) + +Une fois les trois varaibles d'environnement récupérés, il faut les ajouter dans un fichier de secret sur le SSPCloud (voir partie 3) + +## B - Clés llm.lab + +Il faut : + +- D'abord se connecter sur https://llm.lab.sspcloud.fr/, +- puis cliquer sur l'icone en haut à droite, puis sur **Réglages** + +![Copie d'écran de l'interface llm.lab](/img/key_llmlab1.png) + +Sur l'interface des réglages, il faut : + +- Cliquer sur l'onglet "Compte" +- Dans la partie **Clés d'API**, cliquer sur **afficher** +- Récupérer la clé d'API liée à la session llm.lab + +![Copie d'écran de l'interface llm.lab](/img/key_llmlab2.png) + +::: {.callout-note appearance="simple"} +L'endpoint d'accès à l'API de llm.lab est déjà connu. Il s'agit de https://llm.lab.sspcloud.fr/api/v1 +::: + +# 2 - Initialisation du projet sur le SSP Cloud + +## A - Configuration du fichier des secrets + +Un fichier de secrets est une sorte de coffre fort comportant les variables d'environnement confidentielles, qui ne peuvent pas être diffusées sur Github (ou n'importe quelle autre forge logicielle). Par conséquent, elles ne peuvent pas être ajoutées en *dur* dans les scripts du projet. + +Pour créer le fichier de secrets : + +- Il faut se connecter sur le SSP Cloud (https://datalab.sspcloud.fr/) +- Aller dans l'onglet **Mes secrets** +- Cliquer sur le bouton **"Nouveau secret"** et renommer le fichier avec le nom souhaité +- Cliquer sur ce nouveau fichier + +![Copie d'écran du SSP Cloud](/img/create_secret.png) + +Une fois le fichier des secret ouvert, toutes les variables d'environnement confidentielles à utiliser dans le projet peuvent être ajoutées. Ici, on retrouve les trois variables d'environnement Langfuse et les deux variables llm.lab: + +![Copie d'écran du fichier des secrets](/img/langfuse_secret.png) + +## B - Configuration du service VSCode + +Pour exécuter les scripts liés au projet, il faut ouvrir un service VSCode, avec python préinstallé. Ici, pas besoin de GPU (cartes graphiques), donc un simple service **Vscode-python** suffit. Pour cela, sur le SSPCloud : + +- Aller dans l'onglet **Catalogue de services** +- Lancer un service **VSCode-python** + +::: {.callout-warning appearance="simple"} +Pour l'entrainement HTR (Handwritten Text Recognition), il faut ouvrir un service comportant une GPU. Pour cela, il faut sélectionner le service **VSCode-python-gpu** dans le catalogue des services. +::: + +Une fenête de configuration s'affiche, qui est déjà pré-remplie avec plusieurs informations de connections (configuration GIT, configuration au stockage S3...) + +On considère ici que le paramétrage GIT a déjà été effectué. Si ce n'est pas le cas, il faut suivre ce [tuto](https://www.sspcloud.fr/document?path=SSPCloud%E2%90%A3Documentation%E2%80%BAUsing%E2%90%A3the%E2%90%A3Datalab%E2%80%BAVersion%E2%90%A3control) + +Il faut : + +- Aller dans l'onglet **Vault** +- Ajouter le nom du fichier comme variable de **Secret** + +Maintenant, la configuration est terminée, vous pouvez **lancer** le service. + +::: {.callout-note appearance="simple"} +Vous pouvez renommer et enregistrer cette configuration pour la réutiliser plus tard. +::: + diff --git a/website/presentations.qmd b/website/presentations.qmd new file mode 100644 index 0000000..8de4dcf --- /dev/null +++ b/website/presentations.qmd @@ -0,0 +1,16 @@ +--- +title: "Présentations" +subtitle: "Les supports de présentation du projet" +listing: + id: presentations + contents: cards + sort: "order desc" + categories: false + template: style/listing-grid.ejs +page-layout: full +toc: false +--- + +::: {#presentations} +::: + diff --git a/website/resultats.qmd b/website/resultats.qmd index 2134921..c75b153 100644 --- a/website/resultats.qmd +++ b/website/resultats.qmd @@ -1,69 +1,58 @@ --- title: "Résultats des deux approches" -subtitle: "Deux façons de coder une copie, une seule grille de métriques" +subtitle: "Deux méthodes d'évaluation de copies, une seule grille de métriques" --- ```{python} #| echo: false #| warning: false -# Câblage des tableaux : on lit les prédictions exportées (S3 ou local) et on -# calcule les métriques à la volée. Les runs affichés sont surchargeables par -# variables d'environnement ; un run absent ou inaccessible retombe sur « — », -# ce qui garde la page rendable partout (y compris hors SSP Cloud). -import math -import os - -from evaluation_dictee.evaluation.report import load_predictions -from evaluation_dictee.evaluation.results_summary import htr_summary, scoring_summary - -PREFIX = os.environ.get( - "S3_PREDICTIONS_PREFIX", "s3://projet-production-ecrits-depp/predictions" -) -RUNS = { - "end_to_end": os.environ.get("RESULTATS_RUN_END_TO_END", "dictee_REFERENCE"), - "two_stage": os.environ.get("RESULTATS_RUN_TWO_STAGE", ""), - "htr_base": os.environ.get("RESULTATS_RUN_HTR_BASE", ""), - "htr_finetune": os.environ.get("RESULTATS_RUN_HTR_FINETUNE", ""), -} -NOTES: list[str] = [] - - -def _path(run: str, htr: bool = False) -> str: - suffix = "_htr_predictions.jsonl" if htr else "_predictions.jsonl" - return PREFIX.rstrip("/") + "/" + run + suffix - - -def _load(run: str, htr: bool): - """Charge et synthétise un run ; None si absent/inaccessible (avec note).""" - if not run: - return None - try: - df = load_predictions(_path(run, htr=htr)) - return htr_summary(df, run=run) if htr else scoring_summary(df, run=run) - except Exception as exc: # noqa: BLE001 — la page doit rester rendable - NOTES.append(f"`{run}` indisponible ({type(exc).__name__}).") - return None - +# Câblage de la page : on relit les prédictions déjà exportées (S3 ou dossier +# local) et on recalcule chiffres et figures à la volée — aucun pipeline n'est +# réexécuté au rendu. Les runs affichés sont surchargeables par variables +# d'environnement ; un run absent retombe sur « — », ce qui garde la page +# rendable partout (y compris hors SSP Cloud). Voir `website/_analyse.py`. +import sys -def pct(x) -> str: - return "—" if x is None or (isinstance(x, float) and math.isnan(x)) else f"{x:.1%}" +sys.path.insert(0, ".") +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy import stats as scistats -def num(x, d: int = 3) -> str: - return "—" if x is None or (isinstance(x, float) and math.isnan(x)) else f"{x:.{d}f}" +import _analyse as A +A.init_matplotlib() -def _col(summary, attr, formatter): - return "—" if summary is None else formatter(getattr(summary, attr)) +RUNS = A.charger_runs() +ITEMS, MOTS, RANGS = A.charger_grille() +REF = A.RUN_REFERENCE if A.RUN_REFERENCE in RUNS else (next(iter(RUNS)) if RUNS else "") ``` -::: {.callout-note appearance="simple"} -**Tableaux calculés automatiquement** à partir des prédictions exportées sur S3 -(voir la commande d'export en bas de page). Chaque cellule affiche « — » tant que -le run correspondant n'a pas été exporté. Le seul run disponible à ce jour est un -échantillon de démonstration (~20 copies, **non représentatif**) : les chiffres -ci-dessous illustrent le format, pas la performance finale. -::: +```{python} +#| echo: false +#| output: asis +if RUNS: + effectifs = " ; ".join( + f"**{label}** (`{r.nom}`) : {A.milliers(r.n_copies)} copies × " + f"{r.df['item_id'].nunique()} items" + for label, r in RUNS.items() + ) + print('::: {.callout-note appearance="simple"}') + print("**Tous les chiffres et figures de cette page sont recalculés au rendu**") + print("à partir des prédictions exportées (voir la commande d'export en bas de") + print("page). Les deux approches sont affichées côte à côte partout où c'est") + print(f"possible.\n\nRuns comparés — {effectifs}.") + note = A.note_codes_non_standard(RUNS) + if note: + print(f"\n{note}") + print(":::") +else: + print('::: {.callout-warning appearance="simple"}') + print("Aucun run de prédictions n'a pu être chargé : les chiffres de cette page") + print("affichent « — ». Voir le détail en bas de page.") + print(":::") +``` ## Les deux approches en détail @@ -75,15 +64,6 @@ la comparaison porte bien sur l'**architecture**, à modèle égal. On sépare explicitement la **lecture** et le **jugement** : -```{mermaid} -flowchart LR - A["Image de la copie"] --> B["Étape 1 — HTR
transcription du texte brut
(fautes comprises)"] - B --> C["Texte transcrit"] - C --> D["Étape 2 — codage
texte + référence → codes"] - R["Texte de référence"] --> D - D --> E["Code par item"] -``` - - **Étape 1 (HTR)** : un modèle multimodal lit l'image et restitue le texte **exactement tel que l'élève l'a écrit**, fautes comprises. Il ne connaît ni le texte de référence ni le découpage en items : on mesure la **lecture pour @@ -94,21 +74,15 @@ flowchart LR | Avantages | Limites | |-----------|---------| -| La transcription intermédiaire est **inspectable** (on voit ce que le modèle a lu). | Deux étapes = **deux sources d'erreur** qui se cumulent. | +| La transcription intermédiaire peut être analysée (on voit ce que le modèle a lu). | Deux étapes = **deux sources d'erreur** qui se cumulent : l'erreur de transcription et d'évaluation | | Les erreurs de **lecture** et de **jugement** sont découplées. | Deux appels au modèle = coût et latence plus élevés. | -| L'étape 2 peut utiliser un modèle texte moins cher. | | +| L'étape 2 peut utiliser un modèle texte moins cher au token. | | ### Approche 2 — End-to-end (une seule passe) Un unique modèle multimodal (*VLM*) reçoit l'image, le texte de référence et la grille, et produit **directement** le code de chaque item. -```{mermaid} -flowchart LR - A["Image de la copie"] --> C["VLM
lit ET code
en une passe"] - R["Texte de référence + grille"] --> C - C --> E["Code + confiance par item"] -``` | Avantages | Limites | |-----------|---------| @@ -120,6 +94,175 @@ C'est l'approche **par défaut** du projet (la méthode « C » : image + réfé grille → codes). ::: +## Synthèse — codage de la dictée {#synthese-codage-de-la-dictee} + +Comparaison des deux approches sur les mêmes copies, avec **intervalles de +confiance à 95 %** corrigés de la structure en grappes des données. Le paragraphe +suivant explique pourquoi cette correction est indispensable ici, et ce qu'elle +change. + +::: {.callout-note collapse="true" title="Pourquoi corriger les intervalles du « design effect de Kish » ?"} +### Le problème + +Un intervalle de confiance se resserre à mesure qu'on accumule des observations +**indépendantes**. Or les 83 items d'une même copie ne le sont pas : si un élève +se trompe sur un mot, il a de bonnes chances de se tromper sur le suivant. Son +niveau est commun à ses 83 items. + +Conséquence : les ≈ 290 000 items du corpus ne portent pas l'information de +290 000 observations indépendantes. Traiter chaque item comme indépendant +produirait des intervalles trop étroits — on annoncerait une précision qu'on n'a +pas. + +### La correction + +Le **design effect de Kish** chiffre cette perte d'information : + +$$\text{deff} = 1 + (m - 1)\times \text{ICC}$$ + +où *m* est le nombre d'items par copie (83) et l'**ICC** (*intra-class +correlation*) la part de la variance qui vient des différences **entre** copies +plutôt que de la variabilité **à l'intérieur** d'une copie. Un ICC nul signifie +qu'une copie n'apprend rien sur elle-même et que les items sont de fait +indépendants ; un ICC de 1 signifie qu'une copie entière ne vaut qu'une seule +observation. + +On divise alors l'effectif par ce facteur pour obtenir l'**effectif effectif** — +le nombre d'observations indépendantes qui porterait autant d'information — et +c'est sur lui qu'on calcule l'intervalle. Celui-ci s'élargit d'un facteur √deff. + +### Un design effect par métrique, pas un pour tout le corpus + +Point important, et facile à rater : le design effect appartient à une +**indicatrice** précise, pas à un jeu de données. Sur ce corpus, l'indicatrice +d'erreur donne ≈ 10, celle d'accord ≈ 7,5, celle du rappel ≈ 4. Chaque ligne du +tableau ci-dessous est donc corrigée par **son propre** design effect, affiché en +colonne. + +### Le cas du kappa + +Le kappa n'est la moyenne d'aucune indicatrice : lui appliquer le design effect +d'une proportion voisine le sur-corrige d'environ 50 %. Son intervalle vient donc +d'un **bootstrap par grappes** — on retire au hasard des copies entières, avec +remise, et on observe la dispersion du kappa. Cette méthode ne suppose rien sur la +structure de corrélation. Elle est ici exacte et rapide : la matrice de confusion +de chaque copie est calculée une fois, un tirage se réduisant à sommer celles des +copies tirées. + +### Où la correction ne s'applique pas + +À la [prévalence par item](#prevalence-derreur-par-item) : chaque item n'y est +observé qu'une fois par copie, donc une fois par élève. Ces observations sont +indépendantes et leur design effect vaut 1. +::: + +```{python} +#| echo: false +#| output: asis +if RUNS: + syntheses = {label: A.synthese_globale(r.df) for label, r in RUNS.items()} + premiere = next(iter(syntheses.values())) + lignes = [] + for metrique, ligne in premiere.iterrows(): + fmt = A.formateur_unite(ligne["unite"]) + cellules = [ + A.ic( + syntheses[label].loc[metrique, "valeur"], + syntheses[label].loc[metrique, "bas"], + syntheses[label].loc[metrique, "haut"], + fmt, + ) + for label in RUNS + ] + # Design effect appliqué : identique aux deux approches à un cheveu près, + # on affiche celui du run de référence (« bootstrap » pour le kappa). + deff = syntheses[REF].loc[metrique, "deff"] if REF else float("nan") + correction = "bootstrap" if pd.isna(deff) else f"÷ {deff:.1f}" + lignes.append([metrique, *cellules, correction, ligne["lecture"]]) + + # ECE : calibration du score de confiance, non dérivable de la synthèse ci-dessus. + from evaluation_dictee.evaluation.results_summary import scoring_summary + + eces = [A.num(scoring_summary(r.df, run=r.nom).ece) for r in RUNS.values()] + lignes.append( + ["ECE (calibration)", *eces, "—", "écart confiance/justesse : plus **bas** = mieux"] + ) + + # Couverture : une approche qui écarte des copies n'est pas comparable à + # effectif égal. Le two-stage abandonne celles qu'il ne sait pas transcrire. + COUVERTURE = A.couverture_copies(RUNS) + lignes.append( + [ + "Copies écartées du corpus", + *[ + f"{A.milliers(COUVERTURE.loc[label, 'n_ecartees'])} " + f"({A.pts(COUVERTURE.loc[label, 'pct_ecartees'], 2)})" + for label in RUNS + ], + "—", + "copies sans codage produit : plus **bas** = mieux", + ] + ) + print( + A.table_markdown( + [ + "Métrique", + *(f"Approche {label}" for label in RUNS), + "Effectif divisé par", + "Lecture", + ], + lignes, + "Codage de la dictée : comparaison des deux approches, IC 95 % entre crochets", + ) + ) + print( + "\nLa colonne « effectif divisé par » donne le design effect " + "appliqué à chaque métrique : un facteur de 7,5 signifie que les ≈ 290 000 " + "items pèsent autant que 39 000 observations indépendantes, et que " + "l'intervalle est √7,5 ≈ 2,7 fois plus large qu'un calcul naïf. " + "Voir l'encadré ci-dessus." + ) +``` + +```{python} +#| echo: false +#| output: asis +if RUNS and (COUVERTURE["n_ecartees"] > 0).any(): + detail = " ; ".join( + f"**{label}** en écarte {A.milliers(COUVERTURE.loc[label, 'n_ecartees'])}" + for label in RUNS + if COUVERTURE.loc[label, "n_ecartees"] > 0 + ) + print('::: {.callout-warning appearance="simple"}') + print( + f"**Les deux approches ne couvrent pas le même corpus** : {detail}. " + "L'approche two-stage abandonne les copies dont l'étape 1 ne produit " + "aucune transcription ; l'end-to-end code toujours quelque chose, y " + "compris sur des copies blanches (voir la page " + "[Écarts IA – humain](ecarts.qmd)). Les métriques ci-dessus sont donc " + "calculées sur des ensembles légèrement différents — l'écart est faible " + "en volume, mais il porte précisément sur les copies les plus difficiles." + ) + print(":::") +``` + +::: {.callout-important appearance="simple"} +**Ce que dit ce tableau.** Les deux approches se trompent différemment. +L'**end-to-end** est plus prudent : il valide beaucoup d'items et rate donc une +grande part des erreurs de l'élève (sur-correction élevée). Le **two-stage** +détecte davantage d'erreurs mais en invente aussi beaucoup (précision plus +faible, sur-détection élevée). Le kappa, qui corrige l'accord du hasard, reste +loin de la variabilité inter-codeurs humains : à ce +stade, **aucune des deux approches n'est utilisable sans relecture humaine**. +::: + +::: {.callout-note appearance="simple" collapse="true" title="Pourquoi l'ECE est peu informatif ici"} +Les modèles renvoient un score de confiance quasi constant à 1,0 : l'ECE se +réduit alors au taux d'erreur et ne mesure plus rien d'utile. C'est précisément +pourquoi le projet construit un score de confiance **par désaccord +inter-modèles** (voir plus bas), bien plus prédictif. +::: + ## Zoom sur la transcription (HTR) **HTR** = *Handwritten Text Recognition*, la reconnaissance d'écriture manuscrite. @@ -138,119 +281,784 @@ des transcriptions de référence **humaines** (fautes préservées), du CP au C On mesure la qualité de lecture par le **CER** et le **WER** (définis dans la page [Évaluation](evaluation.qmd)). -## Tableau de comparaison — codage de la dictée - -Comparaison des deux approches, **à modèle égal** (pour isoler l'effet de -l'architecture). Chiffres calculés sur les prédictions exportées. - ```{python} #| echo: false #| output: asis -e2e = _load(RUNS["end_to_end"], htr=False) -two = _load(RUNS["two_stage"], htr=False) - -# (libellé, attribut de synthèse, formateur, lecture). Les deux dernières lignes -# ne sont pas dérivables du JSONL de prédictions (copies non transcrites exclues à -# l'écriture ; coût/latence non tracés ici) → « — » assumé. -ROWS = [ - ("Accord brut", "raw_agreement", pct, "plus haut = mieux"), - ("Kappa de Cohen", "cohen_kappa", num, "plus haut = mieux"), - ("Rappel des fautes", "recall_errors", pct, "plus haut = moins de fautes ratées"), - ("Précision sur les fautes", "precision_errors", pct, "plus haut = moins de fausses alertes"), - ("Taux de sur-correction", "overcorrection_rate", pct, "plus **bas** = mieux"), - ("ECE (calibration)", "ece", num, "plus **bas** = confiance plus fiable"), -] +# Tableau HTR : run distinct (`_htr_predictions.jsonl`), donc chargement à part. +import os -lines = [ - "| Métrique | Approche end-to-end | Approche two-stage | Lecture |", - "|----------|:-------------------:|:------------------:|---------|", -] -for label, attr, fmt, lecture in ROWS: - lines.append(f"| {label} | {_col(e2e, attr, fmt)} | {_col(two, attr, fmt)} | {lecture} |") -# Lignes non dérivables des prédictions (pour mémoire, cf. note sous le tableau). -lines.append("| Copies non transcrites | — | — | plus **bas** = mieux |") -lines.append("| Coût / latence par copie | — | — | plus **bas** = mieux |") -lines.append("") -lines.append(": Codage de la dictée — end-to-end vs two-stage {.striped .hover}") - -n_info = " ; ".join( - f"{name} : {s.n_copies} copies, {s.n_items} items" - for name, s in (("end-to-end", e2e), ("two-stage", two)) - if s is not None +RUNS_HTR = { + "modèle de base": os.environ.get("RESULTATS_RUN_HTR_BASE", ""), + "modèle fine-tuné": os.environ.get("RESULTATS_RUN_HTR_FINETUNE", ""), +} + + +def _synthese_htr(nom): + """Synthèse d'un run HTR ; None si le run est absent ou inaccessible.""" + if not nom or not A.PAQUET_OK: + return None + from evaluation_dictee.evaluation.report import load_predictions + from evaluation_dictee.evaluation.results_summary import htr_summary + + chemin = A.PREFIX.rstrip("/") + "/" + nom + "_htr_predictions.jsonl" + try: + return htr_summary(load_predictions(chemin), run=nom) + except Exception as exc: # noqa: BLE001 — la page doit rester rendable + A.NOTES.append(f"Run HTR `{nom}` indisponible ({type(exc).__name__}).") + return None + + +lignes = [] +for label, nom in RUNS_HTR.items(): + s = _synthese_htr(nom) + valeurs = [ + A.pct(getattr(s, attr)) if s else "—" + for attr in ("cer", "wer", "cer_normalise", "wer_normalise") + ] + lignes.append([f"_{label}_", *valeurs, "—"]) +print( + A.table_markdown( + ["Modèle", "CER", "WER", "CER normalisé", "WER normalisé", "Sur-correction"], + lignes, + "Transcription (HTR) sur Scoledit — plus bas = mieux", + ) ) -print("\n".join(lines)) -if n_info: - print(f"\nEffectifs — {n_info}.") ``` ::: {.callout-note appearance="simple"} -Les lignes **Copies non transcrites** et **Coût / latence** ne sont pas dérivables -du JSONL de prédictions (les copies non transcrites sont exclues à l'écriture, le -coût/latence n'y est pas tracé) : elles restent à renseigner depuis les logs de run -ou Langfuse. +Le tableau HTR reste vide tant que le benchmark de transcription seule n'a pas +été lancé et exporté (`RESULTATS_RUN_HTR_BASE`, `RESULTATS_RUN_HTR_FINETUNE`). +La colonne **sur-correction** n'est pas présente dans le JSONL HTR : elle reste +à calculer depuis les transcriptions. ::: -## Tableau de comparaison — transcription seule (HTR) +# Analyse détaillée du benchmark -Qualité de lecture par modèle, sur Scoledit. Chiffres calculés sur les prédictions -HTR exportées (`_htr_predictions.jsonl`). +Les principales questions sur l'évaluation du modèles sont : + - est-ce que le modèle est capable de produire des indicateurs agrégés non biaisés, dont le nombre d'erreurs par élèves ? + - est-ce que le modèle estime bien les difficultés des élèves, notamment en regroupant les élèves par des varaibles auxiliaires (IPS, sexe, REP...) + +Les sections qui suivent reprennent l'analyse du notebook +`notebooks/03_analyse_resultats.ipynb`. + +## Prévalence d'erreur par item + +**Objectif DEPP** : + - 1. calculer le nombre d'erreur par copie. + - 2. Calculer le nombre d'erreurs par item. + +Chaque point est un item de la dictée ; un point sous la diagonale +signifie que le modèle **sous-estime** la difficulté de cet item. + +Les intervalles de confiance sont des intervalles de Wilson **sans correction de +clustering**, et c'est volontaire : pour un item donné, il y a exactement *une* +observation par copie, chacune venant d'un élève différent. Ces observations sont +donc indépendantes et il n'y a rien à corriger. La correction s'applique aux +métriques agrégées sur les 83 items d'une copie — voir +[la synthèse](#synthese-codage-de-la-dictee) ci-dessus. + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Prévalence d'erreur par item — expert (axe horizontal) vs modèle (axe vertical). Les traits fins sont les intervalles de Wilson à 95 %." +PREVALENCES = {} +if RUNS: + fig, axes = plt.subplots(1, len(RUNS), figsize=(7.5 * len(RUNS), 7), squeeze=False) + for ax, (label, r) in zip(axes[0], RUNS.items()): + prev = A.prevalence_par_item(r.df, MOTS, RANGS) + PREVALENCES[label] = prev + x, y = prev["pct_expert"].values, prev["pct_modele"].values + maxv = max(x.max(), y.max()) * 1.08 + + ax.plot([0, maxv], [0, maxv], color="grey", ls="--", lw=1, label="y = x") + # Barres d'erreur discrètes : IC Wilson 95 % sur chaque prévalence, sans + # correction de clustering (une observation par copie, cf. texte). + ax.errorbar( + x, + y, + xerr=[x - prev["ic_expert_bas"], prev["ic_expert_haut"] - x], + yerr=[y - prev["ic_modele_bas"], prev["ic_modele_haut"] - y], + fmt="none", + ecolor=r.couleur, + elinewidth=0.7, + alpha=0.35, + capsize=0, + zorder=2, + ) + ax.scatter(x, y, s=40, alpha=0.85, color=r.couleur, edgecolors="k", linewidths=0.4, zorder=3) + pente, ordonnee, r_pearson, *_ = scistats.linregress(x, y) + xs = np.array([0, maxv]) + ax.plot( + xs, + ordonnee + pente * xs, + color=A.C_EXPERT, + lw=1.5, + label=f"y = {pente:.2f}x {ordonnee:+.2f} | r = {r_pearson:.3f}", + ) + for _, ligne in prev.iterrows(): + ax.annotate( + f"{ligne['position']:02.0f}·{ligne['mot_attendu']}", + (ligne["pct_expert"], ligne["pct_modele"]), + fontsize=5.5, + xytext=(4, 3), + textcoords="offset points", + alpha=0.8, + ) + ax.set_xlabel("Prévalence d'erreur selon l'expert (%)") + ax.set_ylabel("Prévalence d'erreur selon le modèle (%)") + ax.set_title(f"Approche {label}", fontweight="bold") + ax.legend(loc="lower right", fontsize=8) + plt.tight_layout() + plt.show() +``` + +```{python} +#| echo: false +#| output: asis +if PREVALENCES: + lignes = [] + for label, prev in PREVALENCES.items(): + x, y = prev["pct_expert"].values, prev["pct_modele"].values + pente, _, r_pearson, *_ = scistats.linregress(x, y) + lignes.append( + [ + f"Approche {label}", + f"{np.corrcoef(x, y)[0, 1]:+.3f}", + f"{scistats.spearmanr(x, y).correlation:+.3f}", + f"{pente:.3f}", + f"{r_pearson ** 2:.3f}", + f"{(y - x).mean():+.2f} pts", + ] + ) + print( + A.table_markdown( + ["", "Pearson r", "Spearman ρ", "Pente", "R²", "Biais moyen"], + lignes, + "Fidélité de la hiérarchie de difficulté des items", + ) + ) + print( + "\nLecture : un **Spearman ρ** élevé signifie que le modèle " + "retrouve le bon *classement* des items difficiles, même si le *niveau* " + "de prévalence est biaisé (colonne « biais moyen »)." + ) +``` + +Les items sur lesquels le modèle s'écarte le plus de l'expert : ```{python} #| echo: false #| output: asis -htr_rows = [ - ("modèle de base", _load(RUNS["htr_base"], htr=True)), - ("modèle fine-tuné", _load(RUNS["htr_finetune"], htr=True)), +if PREVALENCES: + print("::: {.panel-tabset}") + for label, prev in PREVALENCES.items(): + print(f"\n## Approche {label}\n") + pires = prev.reindex(prev["ecart"].abs().sort_values(ascending=False).index).head(15) + lignes = [ + [ + f"{int(ligne['position']):02d}", + f"`{ligne['mot_attendu']}`", + A.pts(ligne["pct_expert"]), + A.pts(ligne["pct_modele"]), + f"{ligne['ecart']:+.1f} pts", + ] + for _, ligne in pires.iterrows() + ] + print( + A.table_markdown( + ["Rang", "Mot attendu", "Erreur — expert", "Erreur — modèle", "Écart"], + lignes, + f"15 items les plus divergents — approche {label}", + ) + ) + print("\n:::") +``` + +## Distribution du nombre d'erreurs par copie + +Trois vues par approche : erreurs totales, fautes d'orthographe seules (code 9), +et mots absents (code 0). Un décalage des moyennes révèle un **biais +systématique** ; un décalage des formes révèle un biais qui dépend du niveau de +l'élève. + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Distribution du nombre d'erreurs par copie — expert vs modèle, pour chaque approche." +COLONNES = [ + ("n_erreurs_expert", "n_erreurs_modele", "Total d'erreurs (fautes + absents)"), + ("n_fautes_expert", "n_fautes_modele", "Fautes d'orthographe (code 9)"), + ("n_manquants_expert", "n_manquants_modele", "Mots absents (code 0)"), ] +if RUNS: + fig, axes = plt.subplots( + len(RUNS), 3, figsize=(16, 4.3 * len(RUNS)), squeeze=False, sharey="row" + ) + for ligne_axes, (label, r) in zip(axes, RUNS.items()): + for ax, (col_e, col_m, titre) in zip(ligne_axes, COLONNES): + max_v = max(r.copies[col_e].max(), r.copies[col_m].max()) + bins = np.arange(0, max_v + 2) - 0.5 + ax.hist( + r.copies[col_e], bins=bins, alpha=0.6, color=A.C_EXPERT, + label="Expert", edgecolor="white", linewidth=0.3, + ) + ax.hist( + r.copies[col_m], bins=bins, alpha=0.6, color=r.couleur, + label="Modèle", edgecolor="white", linewidth=0.3, + ) + ax.axvline( + r.copies[col_e].mean(), color=A.C_EXPERT, ls="--", lw=1.5, + label=f"moy. expert {r.copies[col_e].mean():.1f}", + ) + ax.axvline( + r.copies[col_m].mean(), color=r.couleur, ls="--", lw=1.5, + label=f"moy. modèle {r.copies[col_m].mean():.1f}", + ) + ax.set_xlabel("Nombre par copie") + ax.set_ylabel("Nombre de copies") + ax.set_title(f"{titre}\napproche {label}", fontsize=9, fontweight="bold") + ax.legend(fontsize=7.5) + plt.tight_layout() + plt.show() +``` -lines = [ - "| Modèle | CER | WER | CER normalisé | WER normalisé | Sur-correction |", - "|--------|:---:|:---:|:-------------:|:-------------:|:--------------:|", +```{python} +#| echo: false +#| output: asis +if RUNS: + def _stats(colonne): + return [ + f"{colonne.mean():.2f}", f"{colonne.median():.1f}", f"{colonne.std():.2f}", + f"{colonne.quantile(0.25):.1f}", f"{colonne.quantile(0.75):.1f}", + f"{colonne.max():.0f}", + ] + + premier = next(iter(RUNS.values())) + lignes = [["**Expert**", *_stats(premier.copies["n_erreurs_expert"])]] + for label, r in RUNS.items(): + lignes.append([f"Modèle — {label}", *_stats(r.copies["n_erreurs_modele"])]) + ks = scistats.ks_2samp(r.copies["n_erreurs_expert"], r.copies["n_erreurs_modele"]) + lignes.append( + [ + f"↳ test de Kolmogorov–Smirnov ({label})", + f"D = {ks.statistic:.3f}", + f"p = {ks.pvalue:.2g}", + "", "", "", "", + ] + ) + print( + A.table_markdown( + ["Nombre d'erreurs par copie", "Moyenne", "Médiane", "Écart-type", "Q1", "Q3", "Max"], + lignes, + "Distribution du nombre total d'erreurs par copie", + ) + ) + print( + "\nLe test de Kolmogorov–Smirnov compare la distribution du " + "modèle à celle de l'expert : une p-valeur très faible signifie que les " + "deux distributions diffèrent, au-delà de leurs seules moyennes." + ) +``` + +La **courbe de survie** chiffre directement ce qui sert au pilotage : la part de +copies au-dessus de chaque seuil d'erreurs. + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Part de copies dépassant chaque seuil d'erreurs — expert vs modèles." +if RUNS: + fig, ax = plt.subplots(figsize=(11, 5.5)) + premier = next(iter(RUNS.values())) + max_err = int( + max( + premier.copies["n_erreurs_expert"].max(), + *[r.copies["n_erreurs_modele"].max() for r in RUNS.values()], + ) + ) + seuils = np.arange(0, max_err + 1) + survie_exp = [(premier.copies["n_erreurs_expert"] > s).mean() * 100 for s in seuils] + ax.plot(seuils, survie_exp, color=A.C_EXPERT, lw=2.8, label="Expert (référence)") + for label, r in RUNS.items(): + survie_mod = [(r.copies["n_erreurs_modele"] > s).mean() * 100 for s in seuils] + ax.plot(seuils, survie_mod, color=r.couleur, lw=2, ls="--", label=f"Modèle — {label}") + ax.fill_between(seuils, survie_exp, survie_mod, alpha=0.08, color=r.couleur) + for seuil in (5, 10, 20, 40): + if seuil > max_err: + continue + ax.axvline(seuil, color="grey", ls=":", lw=1, alpha=0.6) + pct_e = (premier.copies["n_erreurs_expert"] > seuil).mean() * 100 + ax.annotate( + f"s={seuil}\nexpert {pct_e:.0f} %", + xy=(seuil, pct_e), xytext=(seuil + 0.8, pct_e + 4), + fontsize=7.5, color="grey", + ) + ax.set_xlabel("Seuil s : nombre d'erreurs par copie") + ax.set_ylabel("% de copies avec plus de s erreurs") + ax.set_title("Courbe de survie empirique", fontweight="bold") + ax.set_ylim(0, 105) + ax.legend(fontsize=9) + plt.show() +``` + +## Nombre d'erreurs modèle vs expert, copie par copie + +**Objectif DEPP** : le modèle reproduit-il le **classement des élèves** ? Une +corrélation forte et une pente proche de 1 signent un classement fidèle, même si +le niveau absolu est biaisé. + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Nombre d'erreurs par copie : modèle vs expert. Chaque point est une copie." +CORRELATIONS = {} +if RUNS: + fig, axes = plt.subplots(1, len(RUNS), figsize=(6.8 * len(RUNS), 6.6), squeeze=False) + for ax, (label, r) in zip(axes[0], RUNS.items()): + x = r.copies["n_erreurs_expert"].values + y = r.copies["n_erreurs_modele"].values + maxv = max(x.max(), y.max()) * 1.05 + ax.plot([0, maxv], [0, maxv], color="grey", ls="--", lw=1, label="y = x") + ax.scatter(x, y, s=26, alpha=0.45, color=r.couleur, edgecolors="none") + pente, ordonnee, r_pearson, *_ = scistats.linregress(x, y) + xs = np.array([0, maxv]) + ax.plot( + xs, ordonnee + pente * xs, color=A.C_EXPERT, lw=1.7, + label=f"y = {pente:.2f}x {ordonnee:+.2f}\nr = {r_pearson:.3f}, R² = {r_pearson**2:.3f}", + ) + CORRELATIONS[label] = { + "pearson": float(np.corrcoef(x, y)[0, 1]), + "spearman": float(scistats.spearmanr(x, y).correlation), + "kendall": float(scistats.kendalltau(x, y).correlation), + "pente": pente, + "r2": r_pearson**2, + } + ax.set_xlabel("Nombre d'erreurs selon l'expert (proxy du niveau de l'élève)") + ax.set_ylabel("Nombre d'erreurs selon le modèle") + ax.set_title(f"Approche {label}", fontweight="bold") + ax.legend(loc="upper left", fontsize=8) + plt.tight_layout() + plt.show() +``` + +```{python} +#| echo: false +#| output: asis +if CORRELATIONS: + lignes = [ + [ + f"Approche {label}", + f"{c['pearson']:+.3f}", + f"{c['spearman']:+.3f}", + f"{c['kendall']:+.3f}", + f"{c['pente']:.3f}", + f"{c['r2']:.3f}", + ] + for label, c in CORRELATIONS.items() + ] + print( + A.table_markdown( + ["", "Pearson r", "Spearman ρ", "Kendall τ", "Pente", "R²"], + lignes, + "Fidélité du classement des copies (proxy du niveau des élèves)", + ) + ) +``` + +## Métriques d'accord selon la difficulté de la copie + +L'accord se dégrade-t-il sur les copies difficiles ? On découpe les copies en +huit groupes de difficulté croissante, une fois selon le nombre d'erreurs vues +par l'**expert** (difficulté réelle), une fois selon celui vu par le **modèle** +(difficulté perçue). + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Accord, kappa, rappel et précision selon la difficulté de la copie. Zone ombrée = IC Wilson 95 %." +SOURCES = [ + ("n_erreurs_expert", "Difficulté réelle (erreurs vues par l'expert)"), + ("n_erreurs_modele", "Difficulté perçue (erreurs vues par le modèle)"), +] +METRIQUES = [ + ("accord", "Accord brut", "accord_bas", "accord_haut"), + ("kappa", "Kappa de Cohen", None, None), + ("rappel", "Rappel (sensibilité)", "rappel_bas", "rappel_haut"), + ("precision", "Précision sur les erreurs", None, None), ] -for label, s in htr_rows: - cer = _col(s, "cer", pct) - wer = _col(s, "wer", pct) - cer_n = _col(s, "cer_normalise", pct) - wer_n = _col(s, "wer_normalise", pct) - # Sur-correction HTR : non présente dans le JSONL de prédictions → « — ». - lines.append(f"| _{label}_ | {cer} | {wer} | {cer_n} | {wer_n} | — |") -lines.append("") -lines.append(": Transcription (HTR) sur Scoledit — plus bas = mieux {.striped .hover}") -print("\n".join(lines)) +if RUNS: + for col_tri, titre_source in SOURCES: + courbes = { + label: A.metriques_par_difficulte(r.df, r.copies, col_tri) + for label, r in RUNS.items() + } + fig, axes = plt.subplots(1, 4, figsize=(18, 4.4), squeeze=False) + for ax, (col, titre, col_bas, col_haut) in zip(axes[0], METRIQUES): + for label, r in RUNS.items(): + courbe = courbes[label] + ax.plot( + courbe["x"], courbe[col], color=r.couleur, lw=2, + marker="o", ms=4.5, label=label, + ) + if col_bas: + ax.fill_between( + courbe["x"], courbe[col_bas], courbe[col_haut], + alpha=0.15, color=r.couleur, + ) + ax.set_xlabel("Nombre d'erreurs par copie") + ax.set_title(titre, fontsize=9.5, fontweight="bold") + ax.legend(fontsize=8) + fig.suptitle(titre_source, fontsize=11, fontweight="bold", color=A.C_EXPERT) + plt.tight_layout() + plt.show() +``` + +## Consensus inter-modèles + +**Motivation** : quand deux approches indépendantes convergent, la prédiction est +fiable ; quand elles divergent, c'est un signal d'incertitude qui appelle un +humain. Le désaccord inter-modèles est un **bien meilleur prédicteur de +confiance** que le score annoncé par un modèle unique. + +```{python} +#| echo: false +#| warning: false +DF_ACCORD = None +CONF_COPIES = None +if len(RUNS) >= 2: + joint = A.joindre_runs(RUNS) + if joint is not None: + DF_ACCORD = A.agreement_per_item(joint) + CONF_COPIES = A.confidence_score(DF_ACCORD) +``` + +```{python} +#| echo: false +#| output: asis +if DF_ACCORD is None: + print('::: {.callout-note appearance="simple"}') + print("Cette section demande **au moins deux runs** portant sur les mêmes copies.") + print("Lancer un second benchmark (autre approche, autre modèle ou autre prompt)") + print("puis l'exporter pour l'activer.") + print(":::") +else: + print( + f"Sur les {A.milliers(len(DF_ACCORD))} items communs aux deux approches, " + f"**{A.pct(DF_ACCORD['unanimite'].mean())} font l'objet d'un accord** " + "entre les modèles. Le score de confiance moyen par copie (part d'items " + f"sur lesquels les modèles s'accordent) vaut " + f"{A.pts(CONF_COPIES['score_confiance'].mean())}." + ) +``` + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Le consensus inter-modèles prédit l'accord avec l'expert." +if DF_ACCORD is not None: + resume = A.disagreement_type_summary(DF_ACCORD) + fig, ax = plt.subplots(figsize=(8, 4.8)) + couleurs = [ + A.C_PB if v < 70 else A.C_MOYEN if v < 90 else A.C_OK + for v in resume["accord_avec_expert"] + ] + ax.bar( + resume.index.astype(str), resume["accord_avec_expert"], + color=couleurs, edgecolor="k", linewidth=0.4, width=0.55, + ) + for i, (idx, val) in enumerate(resume["accord_avec_expert"].items()): + ax.annotate( + f"{val:.1f} %\n({resume.loc[idx, 'pct_items']:.0f} % des items)", + (i, val), textcoords="offset points", xytext=(0, 5), + ha="center", fontsize=8.5, + ) + n_modeles = int(DF_ACCORD["n_modeles"].iloc[0]) + ax.set_xlabel(f"Nombre de modèles d'accord entre eux (sur {n_modeles})") + ax.set_ylabel("Accord du code majoritaire avec l'expert (%)") + ax.set_ylim(0, 105) + ax.set_title("Le désaccord inter-modèles est prédictif d'erreur", fontweight="bold") + plt.show() +``` + +```{python} +#| echo: false +#| output: asis +if DF_ACCORD is not None: + lignes = [ + [ + str(idx), + A.milliers(ligne["n_items"]), + A.pts(ligne["pct_items"]), + A.pts(ligne["accord_avec_expert"]), + ] + for idx, ligne in resume.iterrows() + ] + print( + A.table_markdown( + ["Modèles d'accord", "Items", "Part des items", "Accord avec l'expert"], + lignes, + "Accord avec l'expert selon le niveau de consensus inter-modèles", + ) + ) + print( + "\n**Lecture** : quand les deux approches s'accordent (dernière ligne), " + "l'accord avec l'expert est nettement plus élevé. Le désaccord " + "inter-modèles est donc un signal exploitable pour orienter la relecture " + "humaine — c'est ce qu'exploite la section suivante." + ) +``` + +## Stratégie de renvoi vers un correcteur humain + +**Question opérationnelle** : combien de copies faut-il renvoyer en correction +humaine pour atteindre un accord cible sur les copies conservées ? On trie les +copies par score de confiance (part d'items où les modèles s'accordent) et on +renvoie celles sous le seuil. + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Arbitrage charge humaine / qualité automatisée." +COURBES_RENVOI = {} +if DF_ACCORD is not None: + fig, ax = plt.subplots(figsize=(10.5, 6)) + for label, r in RUNS.items(): + courbe = A.referral_curve_with_ci(DF_ACCORD, CONF_COPIES, reference_run=label) + COURBES_RENVOI[label] = courbe + xs = courbe["pct_copies_renvoyees"].values + ys = courbe["pct_accord_retenues"].values + ax.plot(xs, ys, color=r.couleur, lw=2.5, marker="o", ms=4.5, label=f"Approche {label}") + ax.fill_between(xs, courbe["accord_lo"], courbe["accord_hi"], alpha=0.15, color=r.couleur) + ax.axhline(ys[0], color=r.couleur, ls=":", lw=1, alpha=0.7) + for seuil in (20, 50): + idx = (courbe["pct_copies_renvoyees"] - seuil).abs().idxmin() + acc = courbe.loc[idx, "pct_accord_retenues"] + renv = courbe.loc[idx, "pct_copies_renvoyees"] + if not np.isnan(acc): + ax.plot(renv, acc, "o", color="#111", ms=7, zorder=5) + ax.annotate( + f"{renv:.0f} % renvoyé → accord {acc:.1f} %", + (renv, acc), textcoords="offset points", xytext=(10, -18), + fontsize=8, + arrowprops=dict(arrowstyle="->", color="grey", lw=0.6), + ) + ax.set_xlabel("% de copies renvoyées en correction manuelle") + ax.set_ylabel("Accord modèle-expert sur les copies conservées (%)") + ax.set_title("Arbitrage : charge humaine vs qualité automatisée", fontweight="bold") + ax.set_xlim(-2, 102) + ax.legend(loc="lower right", fontsize=9) + plt.show() +``` + +```{python} +#| echo: false +#| output: asis +if COURBES_RENVOI: + print("::: {.panel-tabset}") + for label, courbe in COURBES_RENVOI.items(): + print(f"\n## Approche {label}\n") + cible = courbe[courbe["seuil_confiance"].isin([50, 60, 70, 80, 90, 95, 100])] + lignes = [ + [ + f"{int(ligne['seuil_confiance'])} %", + A.pts(ligne["pct_copies_renvoyees"]), + A.ic( + ligne["pct_accord_retenues"] / 100, + ligne["accord_lo"] / 100, + ligne["accord_hi"] / 100, + ), + A.milliers(ligne["n_copies_retenues"]), + ] + for _, ligne in cible.iterrows() + ] + print( + A.table_markdown( + ["Seuil de confiance τ", "Copies renvoyées", "Accord sur les copies conservées", + "Copies conservées"], + lignes, + f"Seuils de renvoi — approche {label}", + ) + ) + print("\n:::") +``` + +## Décomposition des désaccords + +Chaque désaccord n'a pas les mêmes conséquences. **Sur-correction** +(expert : erreur → modèle : correct) veut dire qu'une faute de l'élève est +passée inaperçue. **Sur-détection** (expert : correct → modèle : erreur) veut +dire qu'on sanctionne un élève à tort. + +```{python} +#| echo: false +#| output: asis +if RUNS: + decompositions = { + label: A.disagreement_decomposition(r.df).set_index("transition") + for label, r in RUNS.items() + } + transitions = [] + for deco in decompositions.values(): + transitions += [t for t in deco.index if t not in transitions] + lignes = [] + for transition in transitions: + cellules = [] + for deco in decompositions.values(): + if transition in deco.index: + cellules.append( + f"{A.milliers(deco.loc[transition, 'n'])} " + f"({deco.loc[transition, 'pct_desaccords']:.1f} %)" + ) + else: + cellules.append("—") + lignes.append([f"`{transition}`", *cellules]) + print( + A.table_markdown( + ["Transition expert → modèle", *(f"Approche {label}" for label in RUNS)], + lignes, + "Décomposition des désaccords : effectif et part parmi les désaccords", + ) + ) +``` + +## Accord par item + +Accord entre modèle et expert pour chacun des 83 items, avec intervalle de +Wilson à 95 %. Les items sont triés par accord croissant selon l'approche de +référence : ceux du haut du graphique sont les plus problématiques. + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Accord modèle-expert par item, trié par accord croissant. Barres = IC Wilson 95 %." +if RUNS: + ref_items = RUNS[REF].items.sort_values("accord") + ordre = ref_items.index.tolist() + fig, ax = plt.subplots(figsize=(9.5, 0.28 * len(ordre) + 1.5)) + y = np.arange(len(ordre)) + decalages = np.linspace(-0.2, 0.2, len(RUNS)) if len(RUNS) > 1 else [0] + for decalage, (label, r) in zip(decalages, RUNS.items()): + items = r.items.reindex(ordre) + valeurs = items["accord"] * 100 + err = [ + (items["accord"] - items["accord_lo"]) * 100, + (items["accord_hi"] - items["accord"]) * 100, + ] + ax.errorbar( + valeurs, y + decalage, xerr=err, fmt="o", ms=4, lw=0, + elinewidth=0.9, capsize=1.8, color=r.couleur, alpha=0.9, label=f"Approche {label}", + ) + ax.set_yticks(y) + ax.set_yticklabels([A.libelle_item(i, MOTS, RANGS) for i in ordre], fontsize=7) + ax.invert_yaxis() + ax.axvline(85, color=A.C_PB, ls=":", lw=1) + ax.axvline(95, color=A.C_MOYEN, ls=":", lw=1) + ax.set_xlabel("Accord avec l'expert (%)") + ax.set_title(f"Accord par item — items triés selon l'approche {REF}", fontweight="bold") + ax.legend(loc="lower right", fontsize=8.5) + plt.show() +``` + +## Accord par copie + +```{python} +#| echo: false +#| warning: false +#| fig-cap: "Distribution de l'accord par copie, et lien entre difficulté de la copie et accord." +if RUNS: + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13.5, 4.8)) + for label, r in RUNS.items(): + ax1.hist( + r.copies["accord"] * 100, bins=30, color=r.couleur, alpha=0.55, + edgecolor="white", linewidth=0.3, label=f"{label} (moy. {r.copies['accord'].mean():.1%})", + ) + ax1.axvline(r.copies["accord"].mean() * 100, color=r.couleur, ls="--", lw=1.5) + ax1.set_xlabel("Accord sur la copie (%)") + ax1.set_ylabel("Nombre de copies") + ax1.set_title("Distribution de l'accord par copie", fontweight="bold") + ax1.legend(fontsize=8.5) + + for label, r in RUNS.items(): + ax2.scatter( + r.copies["pct_erreur_expert"], r.copies["accord"] * 100, + s=14, alpha=0.25, color=r.couleur, edgecolors="none", + ) + n_q = min(10, r.copies["pct_erreur_expert"].nunique()) + quantiles = pd.qcut(r.copies["pct_erreur_expert"], q=n_q, duplicates="drop") + tendance = r.copies.groupby(quantiles, observed=True).agg( + x=("pct_erreur_expert", "mean"), y=("accord", "mean") + ) + ax2.plot( + tendance["x"], tendance["y"] * 100, color=r.couleur, lw=2.2, + marker="o", ms=5, label=f"Approche {label}", + ) + ax2.set_xlabel("% d'items en erreur sur la copie, selon l'expert") + ax2.set_ylabel("Accord modèle-expert (%)") + ax2.set_title("L'accord baisse-t-il sur les copies difficiles ?", fontweight="bold") + ax2.legend(fontsize=8.5) + plt.tight_layout() + plt.show() +``` + +Les copies sur lesquelles le modèle s'écarte le plus de l'expert sont détaillées +une par une — statistiques, transcription et codages comparés — dans la page +**[Écarts IA – humain](ecarts.qmd)**. + +```{python} +#| echo: false +#| output: asis +# Pourquoi certaines cases valent « — » : paquet absent, run non exporté, S3 +# inaccessible… Le détail est consigné au fil du chargement. +print(A.bloc_notes()) ``` ## Comment produire ces chiffres **1. Lancer les runs**, puis **2. exporter les prédictions sur S3** — c'est cette -exportation que la présente page relit pour remplir les tableaux (aucun pipeline -n'est réexécuté au rendu du site) : +exportation que la présente page relit (aucun pipeline n'est réexécuté au rendu) : ```bash -# Codage de la dictée (approche définie dans le YAML : end_to_end ou two_stage) -uv run scripts/run_benchmark.py --config configs/scoring/dictee_REFERENCE.yaml -uv run scripts/export_predictions.py --config configs/scoring/dictee_REFERENCE.yaml +# Codage de la dictée, une commande par approche +uv run scripts/run_benchmark.py --config configs/scoring/dictee_end2end.yaml +uv run scripts/export_predictions.py --config configs/scoring/dictee_end2end.yaml +uv run scripts/run_benchmark.py --config configs/scoring/dictee_two_stage.yaml +uv run scripts/export_predictions.py --config configs/scoring/dictee_two_stage.yaml # Transcription seule (CER/WER) sur Scoledit uv run scripts/run_htr_benchmark.py --config configs/htr/htr_REFERENCE.yaml uv run scripts/export_predictions.py --config configs/htr/htr_REFERENCE.yaml --htr + +# Densité d'encre du corpus : documente le seuil de détection des copies vierges +# (quelques minutes, aucun appel modèle). Alimente la page « Écarts IA – humain ». +uv run scripts/compute_ink_ratios.py --config configs/scoring/dictee_end2end.yaml --export +``` + +**3. Rendre le site.** Le rendu exige `matplotlib` et le noyau Jupyter, réunis +dans l'extra `website` : + +```bash +uv sync --extra website +uv run quarto render website # ou : uv run quarto preview website ``` -**3. Choisir les runs affichés** via des variables d'environnement (sinon -`end_to_end` = `dictee_REFERENCE`, les autres vides → « — ») avant de rendre le site : +**4. Choisir les runs affichés** via des variables d'environnement (par défaut +`dictee_end2end` et `dictee_two_stage`) : ```bash -export RESULTATS_RUN_END_TO_END=dictee_gemma4_zeroshot -export RESULTATS_RUN_TWO_STAGE=dictee_gemma4_two_stage +export RESULTATS_RUN_END_TO_END=dictee_end2end +export RESULTATS_RUN_TWO_STAGE=dictee_two_stage +export RESULTATS_RUN_REFERENCE="end-to-end" # run de référence des classements export RESULTATS_RUN_HTR_BASE=htr_gemma4_base export RESULTATS_RUN_HTR_FINETUNE=htr_gemma4_finetune -uv run quarto render website/resultats.qmd # ou : quarto preview website +# Rendu hors ligne, depuis un dossier local de prédictions : +export S3_PREDICTIONS_PREFIX=/chemin/vers/predictions ``` -L'analyse détaillée (distributions, intervalles de confiance, matrices de -confusion, graphiques) se fait ensuite dans les **notebooks** : +L'exploration interactive (au-delà de ce que le site affiche) se fait dans les +**notebooks** : -- `notebooks/03_analyse_resultats.ipynb` — métriques globales et par item ; -- `notebooks/04_diagnostic.ipynb` — inspection copie par copie ; +- `notebooks/03_analyse_resultats.ipynb` — la source de cette page ; +- `notebooks/04_diagnostic.ipynb` — inspection copie par copie, avec image ; - `notebooks/05_analyse_transcription_htr.ipynb` — analyse HTR. ## Ce qu'on cherche à conclure @@ -263,4 +1071,4 @@ Au-delà des chiffres bruts, le projet vise trois décisions : améliore-t-il assez la lecture pour justifier son coût) ? 3. **Où placer le seuil** de renvoi vers un correcteur humain, pour garantir un taux d'erreur résiduel acceptable tout en automatisant le maximum de copies - (voir la **courbe de renvoi** dans la page [Évaluation](evaluation.qmd)). + (voir la **courbe de renvoi** ci-dessus et la page [Évaluation](evaluation.qmd)). diff --git a/website/slides/_extensions/confetti/_extension.yml b/website/slides/_extensions/confetti/_extension.yml new file mode 100644 index 0000000..ef9c74a --- /dev/null +++ b/website/slides/_extensions/confetti/_extension.yml @@ -0,0 +1,24 @@ +title: Confetti +author: Bréant Arthur +version: 1.0.0 +quarto-required: ">=1.2.0" +contributes: + revealjs-plugins: + - name: RevealConfetti + script: + - confetti.js + config: + confetti: + particleCount: 50 + angle: 90 + spread: 45 + startVelocity: 45 + decay: 0.9 + gravity: 1 + drift: 0 + ticks: 200 + colors: null + shapes: null + scalar: 1 + zIndex: 100 + disableForReducedMotion: false diff --git a/website/slides/_extensions/confetti/confetti.js b/website/slides/_extensions/confetti/confetti.js new file mode 100644 index 0000000..4db0344 --- /dev/null +++ b/website/slides/_extensions/confetti/confetti.js @@ -0,0 +1,48 @@ +var script = document.createElement("script"); +script.setAttribute("type", "text/javascript"); +script.setAttribute("src", "https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"); +document.getElementsByTagName("head")[0].appendChild(script); + +function getPosition(event) { + posX = event.pageX; + posY = event.pageY; +} + +document.addEventListener("mousemove", getPosition, false); + +window.RevealConfetti = function () { + return { + id: "RevealConfetti", + init: function (deck) { + + deck.addKeyBinding({ keyCode: 67, key: "C", description: "Throw confettis" }, () => { + const config = deck.getConfig(); + const options = config.confetti || {}; + + confetti({ + particleCount: options.particleCount, + angle: options.angle, + spread: options.spread, + startVelocity: options.startVelocity, + decay: options.decay, + gravity: options.gravity, + drift: options.drift, + ticks: options.ticks, + colors: options.colors, + shapes: options.shapes, + scalar: options.scalar, + zIndex: options.zIndex, + disableForReducedMotion: options.disableForReducedMotion, + origin: { + x: posX / window.innerWidth, + y: posY / window.innerHeight + } + }); + + console.log(`posX: ${posX} | posy: ${posY}`); + console.log(options); + console.log('🎊🎉'); + }); + }, + }; +}; \ No newline at end of file diff --git a/website/slides/_extensions/fontawesome/_extension.yml b/website/slides/_extensions/fontawesome/_extension.yml new file mode 100644 index 0000000..cdd0a27 --- /dev/null +++ b/website/slides/_extensions/fontawesome/_extension.yml @@ -0,0 +1,6 @@ +title: Font Awesome support +author: Carlos Scheidegger +version: 0.0.1 +contributes: + shortcodes: + - fontawesome.lua diff --git a/website/slides/_extensions/fontawesome/assets/css/all.css b/website/slides/_extensions/fontawesome/assets/css/all.css new file mode 100644 index 0000000..9c2adee --- /dev/null +++ b/website/slides/_extensions/fontawesome/assets/css/all.css @@ -0,0 +1,7831 @@ +/*! + * Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2022 Fonticons, Inc. + */ +.fa { + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); } + +.fa, +.fas, +.fa-solid, +.far, +.fa-regular, +.fal, +.fa-light, +.fat, +.fa-thin, +.fad, +.fa-duotone, +.fab, +.fa-brands { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: var(--fa-display, inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; } + +.fa-1x { + font-size: 1em; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-6x { + font-size: 6em; } + +.fa-7x { + font-size: 7em; } + +.fa-8x { + font-size: 8em; } + +.fa-9x { + font-size: 9em; } + +.fa-10x { + font-size: 10em; } + +.fa-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; } + +.fa-xs { + font-size: 0.75em; + line-height: 0.08333em; + vertical-align: 0.125em; } + +.fa-sm { + font-size: 0.875em; + line-height: 0.07143em; + vertical-align: 0.05357em; } + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; } + +.fa-xl { + font-size: 1.5em; + line-height: 0.04167em; + vertical-align: -0.125em; } + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; } + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); } + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); } + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); } + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); } + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-delay: var(--fa-animation-delay, 0); + animation-delay: var(--fa-animation-delay, 0); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin-reverse { + --fa-animation-direction: reverse; } + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); } + +@media (prefers-reduced-motion: reduce) { + .fa-beat, + .fa-bounce, + .fa-fade, + .fa-beat-fade, + .fa-flip, + .fa-pulse, + .fa-shake, + .fa-spin, + .fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + transition-delay: 0s; + transition-duration: 0s; } } + +@-webkit-keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@-webkit-keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.fa-rotate-90 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, none)); } + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2.5em; } + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; + z-index: var(--fa-stack-z-index, auto); } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: var(--fa-inverse, #fff); } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ +.fa-0::before { + content: "\30"; } + +.fa-1::before { + content: "\31"; } + +.fa-2::before { + content: "\32"; } + +.fa-3::before { + content: "\33"; } + +.fa-4::before { + content: "\34"; } + +.fa-5::before { + content: "\35"; } + +.fa-6::before { + content: "\36"; } + +.fa-7::before { + content: "\37"; } + +.fa-8::before { + content: "\38"; } + +.fa-9::before { + content: "\39"; } + +.fa-a::before { + content: "\41"; } + +.fa-address-book::before { + content: "\f2b9"; } + +.fa-contact-book::before { + content: "\f2b9"; } + +.fa-address-card::before { + content: "\f2bb"; } + +.fa-contact-card::before { + content: "\f2bb"; } + +.fa-vcard::before { + content: "\f2bb"; } + +.fa-align-center::before { + content: "\f037"; } + +.fa-align-justify::before { + content: "\f039"; } + +.fa-align-left::before { + content: "\f036"; } + +.fa-align-right::before { + content: "\f038"; } + +.fa-anchor::before { + content: "\f13d"; } + +.fa-anchor-circle-check::before { + content: "\e4aa"; } + +.fa-anchor-circle-exclamation::before { + content: "\e4ab"; } + +.fa-anchor-circle-xmark::before { + content: "\e4ac"; } + +.fa-anchor-lock::before { + content: "\e4ad"; } + +.fa-angle-down::before { + content: "\f107"; } + +.fa-angle-left::before { + content: "\f104"; } + +.fa-angle-right::before { + content: "\f105"; } + +.fa-angle-up::before { + content: "\f106"; } + +.fa-angles-down::before { + content: "\f103"; } + +.fa-angle-double-down::before { + content: "\f103"; } + +.fa-angles-left::before { + content: "\f100"; } + +.fa-angle-double-left::before { + content: "\f100"; } + +.fa-angles-right::before { + content: "\f101"; } + +.fa-angle-double-right::before { + content: "\f101"; } + +.fa-angles-up::before { + content: "\f102"; } + +.fa-angle-double-up::before { + content: "\f102"; } + +.fa-ankh::before { + content: "\f644"; } + +.fa-apple-whole::before { + content: "\f5d1"; } + +.fa-apple-alt::before { + content: "\f5d1"; } + +.fa-archway::before { + content: "\f557"; } + +.fa-arrow-down::before { + content: "\f063"; } + +.fa-arrow-down-1-9::before { + content: "\f162"; } + +.fa-sort-numeric-asc::before { + content: "\f162"; } + +.fa-sort-numeric-down::before { + content: "\f162"; } + +.fa-arrow-down-9-1::before { + content: "\f886"; } + +.fa-sort-numeric-desc::before { + content: "\f886"; } + +.fa-sort-numeric-down-alt::before { + content: "\f886"; } + +.fa-arrow-down-a-z::before { + content: "\f15d"; } + +.fa-sort-alpha-asc::before { + content: "\f15d"; } + +.fa-sort-alpha-down::before { + content: "\f15d"; } + +.fa-arrow-down-long::before { + content: "\f175"; } + +.fa-long-arrow-down::before { + content: "\f175"; } + +.fa-arrow-down-short-wide::before { + content: "\f884"; } + +.fa-sort-amount-desc::before { + content: "\f884"; } + +.fa-sort-amount-down-alt::before { + content: "\f884"; } + +.fa-arrow-down-up-across-line::before { + content: "\e4af"; } + +.fa-arrow-down-up-lock::before { + content: "\e4b0"; } + +.fa-arrow-down-wide-short::before { + content: "\f160"; } + +.fa-sort-amount-asc::before { + content: "\f160"; } + +.fa-sort-amount-down::before { + content: "\f160"; } + +.fa-arrow-down-z-a::before { + content: "\f881"; } + +.fa-sort-alpha-desc::before { + content: "\f881"; } + +.fa-sort-alpha-down-alt::before { + content: "\f881"; } + +.fa-arrow-left::before { + content: "\f060"; } + +.fa-arrow-left-long::before { + content: "\f177"; } + +.fa-long-arrow-left::before { + content: "\f177"; } + +.fa-arrow-pointer::before { + content: "\f245"; } + +.fa-mouse-pointer::before { + content: "\f245"; } + +.fa-arrow-right::before { + content: "\f061"; } + +.fa-arrow-right-arrow-left::before { + content: "\f0ec"; } + +.fa-exchange::before { + content: "\f0ec"; } + +.fa-arrow-right-from-bracket::before { + content: "\f08b"; } + +.fa-sign-out::before { + content: "\f08b"; } + +.fa-arrow-right-long::before { + content: "\f178"; } + +.fa-long-arrow-right::before { + content: "\f178"; } + +.fa-arrow-right-to-bracket::before { + content: "\f090"; } + +.fa-sign-in::before { + content: "\f090"; } + +.fa-arrow-right-to-city::before { + content: "\e4b3"; } + +.fa-arrow-rotate-left::before { + content: "\f0e2"; } + +.fa-arrow-left-rotate::before { + content: "\f0e2"; } + +.fa-arrow-rotate-back::before { + content: "\f0e2"; } + +.fa-arrow-rotate-backward::before { + content: "\f0e2"; } + +.fa-undo::before { + content: "\f0e2"; } + +.fa-arrow-rotate-right::before { + content: "\f01e"; } + +.fa-arrow-right-rotate::before { + content: "\f01e"; } + +.fa-arrow-rotate-forward::before { + content: "\f01e"; } + +.fa-redo::before { + content: "\f01e"; } + +.fa-arrow-trend-down::before { + content: "\e097"; } + +.fa-arrow-trend-up::before { + content: "\e098"; } + +.fa-arrow-turn-down::before { + content: "\f149"; } + +.fa-level-down::before { + content: "\f149"; } + +.fa-arrow-turn-up::before { + content: "\f148"; } + +.fa-level-up::before { + content: "\f148"; } + +.fa-arrow-up::before { + content: "\f062"; } + +.fa-arrow-up-1-9::before { + content: "\f163"; } + +.fa-sort-numeric-up::before { + content: "\f163"; } + +.fa-arrow-up-9-1::before { + content: "\f887"; } + +.fa-sort-numeric-up-alt::before { + content: "\f887"; } + +.fa-arrow-up-a-z::before { + content: "\f15e"; } + +.fa-sort-alpha-up::before { + content: "\f15e"; } + +.fa-arrow-up-from-bracket::before { + content: "\e09a"; } + +.fa-arrow-up-from-ground-water::before { + content: "\e4b5"; } + +.fa-arrow-up-from-water-pump::before { + content: "\e4b6"; } + +.fa-arrow-up-long::before { + content: "\f176"; } + +.fa-long-arrow-up::before { + content: "\f176"; } + +.fa-arrow-up-right-dots::before { + content: "\e4b7"; } + +.fa-arrow-up-right-from-square::before { + content: "\f08e"; } + +.fa-external-link::before { + content: "\f08e"; } + +.fa-arrow-up-short-wide::before { + content: "\f885"; } + +.fa-sort-amount-up-alt::before { + content: "\f885"; } + +.fa-arrow-up-wide-short::before { + content: "\f161"; } + +.fa-sort-amount-up::before { + content: "\f161"; } + +.fa-arrow-up-z-a::before { + content: "\f882"; } + +.fa-sort-alpha-up-alt::before { + content: "\f882"; } + +.fa-arrows-down-to-line::before { + content: "\e4b8"; } + +.fa-arrows-down-to-people::before { + content: "\e4b9"; } + +.fa-arrows-left-right::before { + content: "\f07e"; } + +.fa-arrows-h::before { + content: "\f07e"; } + +.fa-arrows-left-right-to-line::before { + content: "\e4ba"; } + +.fa-arrows-rotate::before { + content: "\f021"; } + +.fa-refresh::before { + content: "\f021"; } + +.fa-sync::before { + content: "\f021"; } + +.fa-arrows-spin::before { + content: "\e4bb"; } + +.fa-arrows-split-up-and-left::before { + content: "\e4bc"; } + +.fa-arrows-to-circle::before { + content: "\e4bd"; } + +.fa-arrows-to-dot::before { + content: "\e4be"; } + +.fa-arrows-to-eye::before { + content: "\e4bf"; } + +.fa-arrows-turn-right::before { + content: "\e4c0"; } + +.fa-arrows-turn-to-dots::before { + content: "\e4c1"; } + +.fa-arrows-up-down::before { + content: "\f07d"; } + +.fa-arrows-v::before { + content: "\f07d"; } + +.fa-arrows-up-down-left-right::before { + content: "\f047"; } + +.fa-arrows::before { + content: "\f047"; } + +.fa-arrows-up-to-line::before { + content: "\e4c2"; } + +.fa-asterisk::before { + content: "\2a"; } + +.fa-at::before { + content: "\40"; } + +.fa-atom::before { + content: "\f5d2"; } + +.fa-audio-description::before { + content: "\f29e"; } + +.fa-austral-sign::before { + content: "\e0a9"; } + +.fa-award::before { + content: "\f559"; } + +.fa-b::before { + content: "\42"; } + +.fa-baby::before { + content: "\f77c"; } + +.fa-baby-carriage::before { + content: "\f77d"; } + +.fa-carriage-baby::before { + content: "\f77d"; } + +.fa-backward::before { + content: "\f04a"; } + +.fa-backward-fast::before { + content: "\f049"; } + +.fa-fast-backward::before { + content: "\f049"; } + +.fa-backward-step::before { + content: "\f048"; } + +.fa-step-backward::before { + content: "\f048"; } + +.fa-bacon::before { + content: "\f7e5"; } + +.fa-bacteria::before { + content: "\e059"; } + +.fa-bacterium::before { + content: "\e05a"; } + +.fa-bag-shopping::before { + content: "\f290"; } + +.fa-shopping-bag::before { + content: "\f290"; } + +.fa-bahai::before { + content: "\f666"; } + +.fa-baht-sign::before { + content: "\e0ac"; } + +.fa-ban::before { + content: "\f05e"; } + +.fa-cancel::before { + content: "\f05e"; } + +.fa-ban-smoking::before { + content: "\f54d"; } + +.fa-smoking-ban::before { + content: "\f54d"; } + +.fa-bandage::before { + content: "\f462"; } + +.fa-band-aid::before { + content: "\f462"; } + +.fa-barcode::before { + content: "\f02a"; } + +.fa-bars::before { + content: "\f0c9"; } + +.fa-navicon::before { + content: "\f0c9"; } + +.fa-bars-progress::before { + content: "\f828"; } + +.fa-tasks-alt::before { + content: "\f828"; } + +.fa-bars-staggered::before { + content: "\f550"; } + +.fa-reorder::before { + content: "\f550"; } + +.fa-stream::before { + content: "\f550"; } + +.fa-baseball::before { + content: "\f433"; } + +.fa-baseball-ball::before { + content: "\f433"; } + +.fa-baseball-bat-ball::before { + content: "\f432"; } + +.fa-basket-shopping::before { + content: "\f291"; } + +.fa-shopping-basket::before { + content: "\f291"; } + +.fa-basketball::before { + content: "\f434"; } + +.fa-basketball-ball::before { + content: "\f434"; } + +.fa-bath::before { + content: "\f2cd"; } + +.fa-bathtub::before { + content: "\f2cd"; } + +.fa-battery-empty::before { + content: "\f244"; } + +.fa-battery-0::before { + content: "\f244"; } + +.fa-battery-full::before { + content: "\f240"; } + +.fa-battery::before { + content: "\f240"; } + +.fa-battery-5::before { + content: "\f240"; } + +.fa-battery-half::before { + content: "\f242"; } + +.fa-battery-3::before { + content: "\f242"; } + +.fa-battery-quarter::before { + content: "\f243"; } + +.fa-battery-2::before { + content: "\f243"; } + +.fa-battery-three-quarters::before { + content: "\f241"; } + +.fa-battery-4::before { + content: "\f241"; } + +.fa-bed::before { + content: "\f236"; } + +.fa-bed-pulse::before { + content: "\f487"; } + +.fa-procedures::before { + content: "\f487"; } + +.fa-beer-mug-empty::before { + content: "\f0fc"; } + +.fa-beer::before { + content: "\f0fc"; } + +.fa-bell::before { + content: "\f0f3"; } + +.fa-bell-concierge::before { + content: "\f562"; } + +.fa-concierge-bell::before { + content: "\f562"; } + +.fa-bell-slash::before { + content: "\f1f6"; } + +.fa-bezier-curve::before { + content: "\f55b"; } + +.fa-bicycle::before { + content: "\f206"; } + +.fa-binoculars::before { + content: "\f1e5"; } + +.fa-biohazard::before { + content: "\f780"; } + +.fa-bitcoin-sign::before { + content: "\e0b4"; } + +.fa-blender::before { + content: "\f517"; } + +.fa-blender-phone::before { + content: "\f6b6"; } + +.fa-blog::before { + content: "\f781"; } + +.fa-bold::before { + content: "\f032"; } + +.fa-bolt::before { + content: "\f0e7"; } + +.fa-zap::before { + content: "\f0e7"; } + +.fa-bolt-lightning::before { + content: "\e0b7"; } + +.fa-bomb::before { + content: "\f1e2"; } + +.fa-bone::before { + content: "\f5d7"; } + +.fa-bong::before { + content: "\f55c"; } + +.fa-book::before { + content: "\f02d"; } + +.fa-book-atlas::before { + content: "\f558"; } + +.fa-atlas::before { + content: "\f558"; } + +.fa-book-bible::before { + content: "\f647"; } + +.fa-bible::before { + content: "\f647"; } + +.fa-book-bookmark::before { + content: "\e0bb"; } + +.fa-book-journal-whills::before { + content: "\f66a"; } + +.fa-journal-whills::before { + content: "\f66a"; } + +.fa-book-medical::before { + content: "\f7e6"; } + +.fa-book-open::before { + content: "\f518"; } + +.fa-book-open-reader::before { + content: "\f5da"; } + +.fa-book-reader::before { + content: "\f5da"; } + +.fa-book-quran::before { + content: "\f687"; } + +.fa-quran::before { + content: "\f687"; } + +.fa-book-skull::before { + content: "\f6b7"; } + +.fa-book-dead::before { + content: "\f6b7"; } + +.fa-bookmark::before { + content: "\f02e"; } + +.fa-border-all::before { + content: "\f84c"; } + +.fa-border-none::before { + content: "\f850"; } + +.fa-border-top-left::before { + content: "\f853"; } + +.fa-border-style::before { + content: "\f853"; } + +.fa-bore-hole::before { + content: "\e4c3"; } + +.fa-bottle-droplet::before { + content: "\e4c4"; } + +.fa-bottle-water::before { + content: "\e4c5"; } + +.fa-bowl-food::before { + content: "\e4c6"; } + +.fa-bowl-rice::before { + content: "\e2eb"; } + +.fa-bowling-ball::before { + content: "\f436"; } + +.fa-box::before { + content: "\f466"; } + +.fa-box-archive::before { + content: "\f187"; } + +.fa-archive::before { + content: "\f187"; } + +.fa-box-open::before { + content: "\f49e"; } + +.fa-box-tissue::before { + content: "\e05b"; } + +.fa-boxes-packing::before { + content: "\e4c7"; } + +.fa-boxes-stacked::before { + content: "\f468"; } + +.fa-boxes::before { + content: "\f468"; } + +.fa-boxes-alt::before { + content: "\f468"; } + +.fa-braille::before { + content: "\f2a1"; } + +.fa-brain::before { + content: "\f5dc"; } + +.fa-brazilian-real-sign::before { + content: "\e46c"; } + +.fa-bread-slice::before { + content: "\f7ec"; } + +.fa-bridge::before { + content: "\e4c8"; } + +.fa-bridge-circle-check::before { + content: "\e4c9"; } + +.fa-bridge-circle-exclamation::before { + content: "\e4ca"; } + +.fa-bridge-circle-xmark::before { + content: "\e4cb"; } + +.fa-bridge-lock::before { + content: "\e4cc"; } + +.fa-bridge-water::before { + content: "\e4ce"; } + +.fa-briefcase::before { + content: "\f0b1"; } + +.fa-briefcase-medical::before { + content: "\f469"; } + +.fa-broom::before { + content: "\f51a"; } + +.fa-broom-ball::before { + content: "\f458"; } + +.fa-quidditch::before { + content: "\f458"; } + +.fa-quidditch-broom-ball::before { + content: "\f458"; } + +.fa-brush::before { + content: "\f55d"; } + +.fa-bucket::before { + content: "\e4cf"; } + +.fa-bug::before { + content: "\f188"; } + +.fa-bug-slash::before { + content: "\e490"; } + +.fa-bugs::before { + content: "\e4d0"; } + +.fa-building::before { + content: "\f1ad"; } + +.fa-building-circle-arrow-right::before { + content: "\e4d1"; } + +.fa-building-circle-check::before { + content: "\e4d2"; } + +.fa-building-circle-exclamation::before { + content: "\e4d3"; } + +.fa-building-circle-xmark::before { + content: "\e4d4"; } + +.fa-building-columns::before { + content: "\f19c"; } + +.fa-bank::before { + content: "\f19c"; } + +.fa-institution::before { + content: "\f19c"; } + +.fa-museum::before { + content: "\f19c"; } + +.fa-university::before { + content: "\f19c"; } + +.fa-building-flag::before { + content: "\e4d5"; } + +.fa-building-lock::before { + content: "\e4d6"; } + +.fa-building-ngo::before { + content: "\e4d7"; } + +.fa-building-shield::before { + content: "\e4d8"; } + +.fa-building-un::before { + content: "\e4d9"; } + +.fa-building-user::before { + content: "\e4da"; } + +.fa-building-wheat::before { + content: "\e4db"; } + +.fa-bullhorn::before { + content: "\f0a1"; } + +.fa-bullseye::before { + content: "\f140"; } + +.fa-burger::before { + content: "\f805"; } + +.fa-hamburger::before { + content: "\f805"; } + +.fa-burst::before { + content: "\e4dc"; } + +.fa-bus::before { + content: "\f207"; } + +.fa-bus-simple::before { + content: "\f55e"; } + +.fa-bus-alt::before { + content: "\f55e"; } + +.fa-business-time::before { + content: "\f64a"; } + +.fa-briefcase-clock::before { + content: "\f64a"; } + +.fa-c::before { + content: "\43"; } + +.fa-cake-candles::before { + content: "\f1fd"; } + +.fa-birthday-cake::before { + content: "\f1fd"; } + +.fa-cake::before { + content: "\f1fd"; } + +.fa-calculator::before { + content: "\f1ec"; } + +.fa-calendar::before { + content: "\f133"; } + +.fa-calendar-check::before { + content: "\f274"; } + +.fa-calendar-day::before { + content: "\f783"; } + +.fa-calendar-days::before { + content: "\f073"; } + +.fa-calendar-alt::before { + content: "\f073"; } + +.fa-calendar-minus::before { + content: "\f272"; } + +.fa-calendar-plus::before { + content: "\f271"; } + +.fa-calendar-week::before { + content: "\f784"; } + +.fa-calendar-xmark::before { + content: "\f273"; } + +.fa-calendar-times::before { + content: "\f273"; } + +.fa-camera::before { + content: "\f030"; } + +.fa-camera-alt::before { + content: "\f030"; } + +.fa-camera-retro::before { + content: "\f083"; } + +.fa-camera-rotate::before { + content: "\e0d8"; } + +.fa-campground::before { + content: "\f6bb"; } + +.fa-candy-cane::before { + content: "\f786"; } + +.fa-cannabis::before { + content: "\f55f"; } + +.fa-capsules::before { + content: "\f46b"; } + +.fa-car::before { + content: "\f1b9"; } + +.fa-automobile::before { + content: "\f1b9"; } + +.fa-car-battery::before { + content: "\f5df"; } + +.fa-battery-car::before { + content: "\f5df"; } + +.fa-car-burst::before { + content: "\f5e1"; } + +.fa-car-crash::before { + content: "\f5e1"; } + +.fa-car-on::before { + content: "\e4dd"; } + +.fa-car-rear::before { + content: "\f5de"; } + +.fa-car-alt::before { + content: "\f5de"; } + +.fa-car-side::before { + content: "\f5e4"; } + +.fa-car-tunnel::before { + content: "\e4de"; } + +.fa-caravan::before { + content: "\f8ff"; } + +.fa-caret-down::before { + content: "\f0d7"; } + +.fa-caret-left::before { + content: "\f0d9"; } + +.fa-caret-right::before { + content: "\f0da"; } + +.fa-caret-up::before { + content: "\f0d8"; } + +.fa-carrot::before { + content: "\f787"; } + +.fa-cart-arrow-down::before { + content: "\f218"; } + +.fa-cart-flatbed::before { + content: "\f474"; } + +.fa-dolly-flatbed::before { + content: "\f474"; } + +.fa-cart-flatbed-suitcase::before { + content: "\f59d"; } + +.fa-luggage-cart::before { + content: "\f59d"; } + +.fa-cart-plus::before { + content: "\f217"; } + +.fa-cart-shopping::before { + content: "\f07a"; } + +.fa-shopping-cart::before { + content: "\f07a"; } + +.fa-cash-register::before { + content: "\f788"; } + +.fa-cat::before { + content: "\f6be"; } + +.fa-cedi-sign::before { + content: "\e0df"; } + +.fa-cent-sign::before { + content: "\e3f5"; } + +.fa-certificate::before { + content: "\f0a3"; } + +.fa-chair::before { + content: "\f6c0"; } + +.fa-chalkboard::before { + content: "\f51b"; } + +.fa-blackboard::before { + content: "\f51b"; } + +.fa-chalkboard-user::before { + content: "\f51c"; } + +.fa-chalkboard-teacher::before { + content: "\f51c"; } + +.fa-champagne-glasses::before { + content: "\f79f"; } + +.fa-glass-cheers::before { + content: "\f79f"; } + +.fa-charging-station::before { + content: "\f5e7"; } + +.fa-chart-area::before { + content: "\f1fe"; } + +.fa-area-chart::before { + content: "\f1fe"; } + +.fa-chart-bar::before { + content: "\f080"; } + +.fa-bar-chart::before { + content: "\f080"; } + +.fa-chart-column::before { + content: "\e0e3"; } + +.fa-chart-gantt::before { + content: "\e0e4"; } + +.fa-chart-line::before { + content: "\f201"; } + +.fa-line-chart::before { + content: "\f201"; } + +.fa-chart-pie::before { + content: "\f200"; } + +.fa-pie-chart::before { + content: "\f200"; } + +.fa-chart-simple::before { + content: "\e473"; } + +.fa-check::before { + content: "\f00c"; } + +.fa-check-double::before { + content: "\f560"; } + +.fa-check-to-slot::before { + content: "\f772"; } + +.fa-vote-yea::before { + content: "\f772"; } + +.fa-cheese::before { + content: "\f7ef"; } + +.fa-chess::before { + content: "\f439"; } + +.fa-chess-bishop::before { + content: "\f43a"; } + +.fa-chess-board::before { + content: "\f43c"; } + +.fa-chess-king::before { + content: "\f43f"; } + +.fa-chess-knight::before { + content: "\f441"; } + +.fa-chess-pawn::before { + content: "\f443"; } + +.fa-chess-queen::before { + content: "\f445"; } + +.fa-chess-rook::before { + content: "\f447"; } + +.fa-chevron-down::before { + content: "\f078"; } + +.fa-chevron-left::before { + content: "\f053"; } + +.fa-chevron-right::before { + content: "\f054"; } + +.fa-chevron-up::before { + content: "\f077"; } + +.fa-child::before { + content: "\f1ae"; } + +.fa-child-dress::before { + content: "\e59c"; } + +.fa-child-reaching::before { + content: "\e59d"; } + +.fa-child-rifle::before { + content: "\e4e0"; } + +.fa-children::before { + content: "\e4e1"; } + +.fa-church::before { + content: "\f51d"; } + +.fa-circle::before { + content: "\f111"; } + +.fa-circle-arrow-down::before { + content: "\f0ab"; } + +.fa-arrow-circle-down::before { + content: "\f0ab"; } + +.fa-circle-arrow-left::before { + content: "\f0a8"; } + +.fa-arrow-circle-left::before { + content: "\f0a8"; } + +.fa-circle-arrow-right::before { + content: "\f0a9"; } + +.fa-arrow-circle-right::before { + content: "\f0a9"; } + +.fa-circle-arrow-up::before { + content: "\f0aa"; } + +.fa-arrow-circle-up::before { + content: "\f0aa"; } + +.fa-circle-check::before { + content: "\f058"; } + +.fa-check-circle::before { + content: "\f058"; } + +.fa-circle-chevron-down::before { + content: "\f13a"; } + +.fa-chevron-circle-down::before { + content: "\f13a"; } + +.fa-circle-chevron-left::before { + content: "\f137"; } + +.fa-chevron-circle-left::before { + content: "\f137"; } + +.fa-circle-chevron-right::before { + content: "\f138"; } + +.fa-chevron-circle-right::before { + content: "\f138"; } + +.fa-circle-chevron-up::before { + content: "\f139"; } + +.fa-chevron-circle-up::before { + content: "\f139"; } + +.fa-circle-dollar-to-slot::before { + content: "\f4b9"; } + +.fa-donate::before { + content: "\f4b9"; } + +.fa-circle-dot::before { + content: "\f192"; } + +.fa-dot-circle::before { + content: "\f192"; } + +.fa-circle-down::before { + content: "\f358"; } + +.fa-arrow-alt-circle-down::before { + content: "\f358"; } + +.fa-circle-exclamation::before { + content: "\f06a"; } + +.fa-exclamation-circle::before { + content: "\f06a"; } + +.fa-circle-h::before { + content: "\f47e"; } + +.fa-hospital-symbol::before { + content: "\f47e"; } + +.fa-circle-half-stroke::before { + content: "\f042"; } + +.fa-adjust::before { + content: "\f042"; } + +.fa-circle-info::before { + content: "\f05a"; } + +.fa-info-circle::before { + content: "\f05a"; } + +.fa-circle-left::before { + content: "\f359"; } + +.fa-arrow-alt-circle-left::before { + content: "\f359"; } + +.fa-circle-minus::before { + content: "\f056"; } + +.fa-minus-circle::before { + content: "\f056"; } + +.fa-circle-nodes::before { + content: "\e4e2"; } + +.fa-circle-notch::before { + content: "\f1ce"; } + +.fa-circle-pause::before { + content: "\f28b"; } + +.fa-pause-circle::before { + content: "\f28b"; } + +.fa-circle-play::before { + content: "\f144"; } + +.fa-play-circle::before { + content: "\f144"; } + +.fa-circle-plus::before { + content: "\f055"; } + +.fa-plus-circle::before { + content: "\f055"; } + +.fa-circle-question::before { + content: "\f059"; } + +.fa-question-circle::before { + content: "\f059"; } + +.fa-circle-radiation::before { + content: "\f7ba"; } + +.fa-radiation-alt::before { + content: "\f7ba"; } + +.fa-circle-right::before { + content: "\f35a"; } + +.fa-arrow-alt-circle-right::before { + content: "\f35a"; } + +.fa-circle-stop::before { + content: "\f28d"; } + +.fa-stop-circle::before { + content: "\f28d"; } + +.fa-circle-up::before { + content: "\f35b"; } + +.fa-arrow-alt-circle-up::before { + content: "\f35b"; } + +.fa-circle-user::before { + content: "\f2bd"; } + +.fa-user-circle::before { + content: "\f2bd"; } + +.fa-circle-xmark::before { + content: "\f057"; } + +.fa-times-circle::before { + content: "\f057"; } + +.fa-xmark-circle::before { + content: "\f057"; } + +.fa-city::before { + content: "\f64f"; } + +.fa-clapperboard::before { + content: "\e131"; } + +.fa-clipboard::before { + content: "\f328"; } + +.fa-clipboard-check::before { + content: "\f46c"; } + +.fa-clipboard-list::before { + content: "\f46d"; } + +.fa-clipboard-question::before { + content: "\e4e3"; } + +.fa-clipboard-user::before { + content: "\f7f3"; } + +.fa-clock::before { + content: "\f017"; } + +.fa-clock-four::before { + content: "\f017"; } + +.fa-clock-rotate-left::before { + content: "\f1da"; } + +.fa-history::before { + content: "\f1da"; } + +.fa-clone::before { + content: "\f24d"; } + +.fa-closed-captioning::before { + content: "\f20a"; } + +.fa-cloud::before { + content: "\f0c2"; } + +.fa-cloud-arrow-down::before { + content: "\f0ed"; } + +.fa-cloud-download::before { + content: "\f0ed"; } + +.fa-cloud-download-alt::before { + content: "\f0ed"; } + +.fa-cloud-arrow-up::before { + content: "\f0ee"; } + +.fa-cloud-upload::before { + content: "\f0ee"; } + +.fa-cloud-upload-alt::before { + content: "\f0ee"; } + +.fa-cloud-bolt::before { + content: "\f76c"; } + +.fa-thunderstorm::before { + content: "\f76c"; } + +.fa-cloud-meatball::before { + content: "\f73b"; } + +.fa-cloud-moon::before { + content: "\f6c3"; } + +.fa-cloud-moon-rain::before { + content: "\f73c"; } + +.fa-cloud-rain::before { + content: "\f73d"; } + +.fa-cloud-showers-heavy::before { + content: "\f740"; } + +.fa-cloud-showers-water::before { + content: "\e4e4"; } + +.fa-cloud-sun::before { + content: "\f6c4"; } + +.fa-cloud-sun-rain::before { + content: "\f743"; } + +.fa-clover::before { + content: "\e139"; } + +.fa-code::before { + content: "\f121"; } + +.fa-code-branch::before { + content: "\f126"; } + +.fa-code-commit::before { + content: "\f386"; } + +.fa-code-compare::before { + content: "\e13a"; } + +.fa-code-fork::before { + content: "\e13b"; } + +.fa-code-merge::before { + content: "\f387"; } + +.fa-code-pull-request::before { + content: "\e13c"; } + +.fa-coins::before { + content: "\f51e"; } + +.fa-colon-sign::before { + content: "\e140"; } + +.fa-comment::before { + content: "\f075"; } + +.fa-comment-dollar::before { + content: "\f651"; } + +.fa-comment-dots::before { + content: "\f4ad"; } + +.fa-commenting::before { + content: "\f4ad"; } + +.fa-comment-medical::before { + content: "\f7f5"; } + +.fa-comment-slash::before { + content: "\f4b3"; } + +.fa-comment-sms::before { + content: "\f7cd"; } + +.fa-sms::before { + content: "\f7cd"; } + +.fa-comments::before { + content: "\f086"; } + +.fa-comments-dollar::before { + content: "\f653"; } + +.fa-compact-disc::before { + content: "\f51f"; } + +.fa-compass::before { + content: "\f14e"; } + +.fa-compass-drafting::before { + content: "\f568"; } + +.fa-drafting-compass::before { + content: "\f568"; } + +.fa-compress::before { + content: "\f066"; } + +.fa-computer::before { + content: "\e4e5"; } + +.fa-computer-mouse::before { + content: "\f8cc"; } + +.fa-mouse::before { + content: "\f8cc"; } + +.fa-cookie::before { + content: "\f563"; } + +.fa-cookie-bite::before { + content: "\f564"; } + +.fa-copy::before { + content: "\f0c5"; } + +.fa-copyright::before { + content: "\f1f9"; } + +.fa-couch::before { + content: "\f4b8"; } + +.fa-cow::before { + content: "\f6c8"; } + +.fa-credit-card::before { + content: "\f09d"; } + +.fa-credit-card-alt::before { + content: "\f09d"; } + +.fa-crop::before { + content: "\f125"; } + +.fa-crop-simple::before { + content: "\f565"; } + +.fa-crop-alt::before { + content: "\f565"; } + +.fa-cross::before { + content: "\f654"; } + +.fa-crosshairs::before { + content: "\f05b"; } + +.fa-crow::before { + content: "\f520"; } + +.fa-crown::before { + content: "\f521"; } + +.fa-crutch::before { + content: "\f7f7"; } + +.fa-cruzeiro-sign::before { + content: "\e152"; } + +.fa-cube::before { + content: "\f1b2"; } + +.fa-cubes::before { + content: "\f1b3"; } + +.fa-cubes-stacked::before { + content: "\e4e6"; } + +.fa-d::before { + content: "\44"; } + +.fa-database::before { + content: "\f1c0"; } + +.fa-delete-left::before { + content: "\f55a"; } + +.fa-backspace::before { + content: "\f55a"; } + +.fa-democrat::before { + content: "\f747"; } + +.fa-desktop::before { + content: "\f390"; } + +.fa-desktop-alt::before { + content: "\f390"; } + +.fa-dharmachakra::before { + content: "\f655"; } + +.fa-diagram-next::before { + content: "\e476"; } + +.fa-diagram-predecessor::before { + content: "\e477"; } + +.fa-diagram-project::before { + content: "\f542"; } + +.fa-project-diagram::before { + content: "\f542"; } + +.fa-diagram-successor::before { + content: "\e47a"; } + +.fa-diamond::before { + content: "\f219"; } + +.fa-diamond-turn-right::before { + content: "\f5eb"; } + +.fa-directions::before { + content: "\f5eb"; } + +.fa-dice::before { + content: "\f522"; } + +.fa-dice-d20::before { + content: "\f6cf"; } + +.fa-dice-d6::before { + content: "\f6d1"; } + +.fa-dice-five::before { + content: "\f523"; } + +.fa-dice-four::before { + content: "\f524"; } + +.fa-dice-one::before { + content: "\f525"; } + +.fa-dice-six::before { + content: "\f526"; } + +.fa-dice-three::before { + content: "\f527"; } + +.fa-dice-two::before { + content: "\f528"; } + +.fa-disease::before { + content: "\f7fa"; } + +.fa-display::before { + content: "\e163"; } + +.fa-divide::before { + content: "\f529"; } + +.fa-dna::before { + content: "\f471"; } + +.fa-dog::before { + content: "\f6d3"; } + +.fa-dollar-sign::before { + content: "\24"; } + +.fa-dollar::before { + content: "\24"; } + +.fa-usd::before { + content: "\24"; } + +.fa-dolly::before { + content: "\f472"; } + +.fa-dolly-box::before { + content: "\f472"; } + +.fa-dong-sign::before { + content: "\e169"; } + +.fa-door-closed::before { + content: "\f52a"; } + +.fa-door-open::before { + content: "\f52b"; } + +.fa-dove::before { + content: "\f4ba"; } + +.fa-down-left-and-up-right-to-center::before { + content: "\f422"; } + +.fa-compress-alt::before { + content: "\f422"; } + +.fa-down-long::before { + content: "\f309"; } + +.fa-long-arrow-alt-down::before { + content: "\f309"; } + +.fa-download::before { + content: "\f019"; } + +.fa-dragon::before { + content: "\f6d5"; } + +.fa-draw-polygon::before { + content: "\f5ee"; } + +.fa-droplet::before { + content: "\f043"; } + +.fa-tint::before { + content: "\f043"; } + +.fa-droplet-slash::before { + content: "\f5c7"; } + +.fa-tint-slash::before { + content: "\f5c7"; } + +.fa-drum::before { + content: "\f569"; } + +.fa-drum-steelpan::before { + content: "\f56a"; } + +.fa-drumstick-bite::before { + content: "\f6d7"; } + +.fa-dumbbell::before { + content: "\f44b"; } + +.fa-dumpster::before { + content: "\f793"; } + +.fa-dumpster-fire::before { + content: "\f794"; } + +.fa-dungeon::before { + content: "\f6d9"; } + +.fa-e::before { + content: "\45"; } + +.fa-ear-deaf::before { + content: "\f2a4"; } + +.fa-deaf::before { + content: "\f2a4"; } + +.fa-deafness::before { + content: "\f2a4"; } + +.fa-hard-of-hearing::before { + content: "\f2a4"; } + +.fa-ear-listen::before { + content: "\f2a2"; } + +.fa-assistive-listening-systems::before { + content: "\f2a2"; } + +.fa-earth-africa::before { + content: "\f57c"; } + +.fa-globe-africa::before { + content: "\f57c"; } + +.fa-earth-americas::before { + content: "\f57d"; } + +.fa-earth::before { + content: "\f57d"; } + +.fa-earth-america::before { + content: "\f57d"; } + +.fa-globe-americas::before { + content: "\f57d"; } + +.fa-earth-asia::before { + content: "\f57e"; } + +.fa-globe-asia::before { + content: "\f57e"; } + +.fa-earth-europe::before { + content: "\f7a2"; } + +.fa-globe-europe::before { + content: "\f7a2"; } + +.fa-earth-oceania::before { + content: "\e47b"; } + +.fa-globe-oceania::before { + content: "\e47b"; } + +.fa-egg::before { + content: "\f7fb"; } + +.fa-eject::before { + content: "\f052"; } + +.fa-elevator::before { + content: "\e16d"; } + +.fa-ellipsis::before { + content: "\f141"; } + +.fa-ellipsis-h::before { + content: "\f141"; } + +.fa-ellipsis-vertical::before { + content: "\f142"; } + +.fa-ellipsis-v::before { + content: "\f142"; } + +.fa-envelope::before { + content: "\f0e0"; } + +.fa-envelope-circle-check::before { + content: "\e4e8"; } + +.fa-envelope-open::before { + content: "\f2b6"; } + +.fa-envelope-open-text::before { + content: "\f658"; } + +.fa-envelopes-bulk::before { + content: "\f674"; } + +.fa-mail-bulk::before { + content: "\f674"; } + +.fa-equals::before { + content: "\3d"; } + +.fa-eraser::before { + content: "\f12d"; } + +.fa-ethernet::before { + content: "\f796"; } + +.fa-euro-sign::before { + content: "\f153"; } + +.fa-eur::before { + content: "\f153"; } + +.fa-euro::before { + content: "\f153"; } + +.fa-exclamation::before { + content: "\21"; } + +.fa-expand::before { + content: "\f065"; } + +.fa-explosion::before { + content: "\e4e9"; } + +.fa-eye::before { + content: "\f06e"; } + +.fa-eye-dropper::before { + content: "\f1fb"; } + +.fa-eye-dropper-empty::before { + content: "\f1fb"; } + +.fa-eyedropper::before { + content: "\f1fb"; } + +.fa-eye-low-vision::before { + content: "\f2a8"; } + +.fa-low-vision::before { + content: "\f2a8"; } + +.fa-eye-slash::before { + content: "\f070"; } + +.fa-f::before { + content: "\46"; } + +.fa-face-angry::before { + content: "\f556"; } + +.fa-angry::before { + content: "\f556"; } + +.fa-face-dizzy::before { + content: "\f567"; } + +.fa-dizzy::before { + content: "\f567"; } + +.fa-face-flushed::before { + content: "\f579"; } + +.fa-flushed::before { + content: "\f579"; } + +.fa-face-frown::before { + content: "\f119"; } + +.fa-frown::before { + content: "\f119"; } + +.fa-face-frown-open::before { + content: "\f57a"; } + +.fa-frown-open::before { + content: "\f57a"; } + +.fa-face-grimace::before { + content: "\f57f"; } + +.fa-grimace::before { + content: "\f57f"; } + +.fa-face-grin::before { + content: "\f580"; } + +.fa-grin::before { + content: "\f580"; } + +.fa-face-grin-beam::before { + content: "\f582"; } + +.fa-grin-beam::before { + content: "\f582"; } + +.fa-face-grin-beam-sweat::before { + content: "\f583"; } + +.fa-grin-beam-sweat::before { + content: "\f583"; } + +.fa-face-grin-hearts::before { + content: "\f584"; } + +.fa-grin-hearts::before { + content: "\f584"; } + +.fa-face-grin-squint::before { + content: "\f585"; } + +.fa-grin-squint::before { + content: "\f585"; } + +.fa-face-grin-squint-tears::before { + content: "\f586"; } + +.fa-grin-squint-tears::before { + content: "\f586"; } + +.fa-face-grin-stars::before { + content: "\f587"; } + +.fa-grin-stars::before { + content: "\f587"; } + +.fa-face-grin-tears::before { + content: "\f588"; } + +.fa-grin-tears::before { + content: "\f588"; } + +.fa-face-grin-tongue::before { + content: "\f589"; } + +.fa-grin-tongue::before { + content: "\f589"; } + +.fa-face-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-face-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-face-grin-wide::before { + content: "\f581"; } + +.fa-grin-alt::before { + content: "\f581"; } + +.fa-face-grin-wink::before { + content: "\f58c"; } + +.fa-grin-wink::before { + content: "\f58c"; } + +.fa-face-kiss::before { + content: "\f596"; } + +.fa-kiss::before { + content: "\f596"; } + +.fa-face-kiss-beam::before { + content: "\f597"; } + +.fa-kiss-beam::before { + content: "\f597"; } + +.fa-face-kiss-wink-heart::before { + content: "\f598"; } + +.fa-kiss-wink-heart::before { + content: "\f598"; } + +.fa-face-laugh::before { + content: "\f599"; } + +.fa-laugh::before { + content: "\f599"; } + +.fa-face-laugh-beam::before { + content: "\f59a"; } + +.fa-laugh-beam::before { + content: "\f59a"; } + +.fa-face-laugh-squint::before { + content: "\f59b"; } + +.fa-laugh-squint::before { + content: "\f59b"; } + +.fa-face-laugh-wink::before { + content: "\f59c"; } + +.fa-laugh-wink::before { + content: "\f59c"; } + +.fa-face-meh::before { + content: "\f11a"; } + +.fa-meh::before { + content: "\f11a"; } + +.fa-face-meh-blank::before { + content: "\f5a4"; } + +.fa-meh-blank::before { + content: "\f5a4"; } + +.fa-face-rolling-eyes::before { + content: "\f5a5"; } + +.fa-meh-rolling-eyes::before { + content: "\f5a5"; } + +.fa-face-sad-cry::before { + content: "\f5b3"; } + +.fa-sad-cry::before { + content: "\f5b3"; } + +.fa-face-sad-tear::before { + content: "\f5b4"; } + +.fa-sad-tear::before { + content: "\f5b4"; } + +.fa-face-smile::before { + content: "\f118"; } + +.fa-smile::before { + content: "\f118"; } + +.fa-face-smile-beam::before { + content: "\f5b8"; } + +.fa-smile-beam::before { + content: "\f5b8"; } + +.fa-face-smile-wink::before { + content: "\f4da"; } + +.fa-smile-wink::before { + content: "\f4da"; } + +.fa-face-surprise::before { + content: "\f5c2"; } + +.fa-surprise::before { + content: "\f5c2"; } + +.fa-face-tired::before { + content: "\f5c8"; } + +.fa-tired::before { + content: "\f5c8"; } + +.fa-fan::before { + content: "\f863"; } + +.fa-faucet::before { + content: "\e005"; } + +.fa-faucet-drip::before { + content: "\e006"; } + +.fa-fax::before { + content: "\f1ac"; } + +.fa-feather::before { + content: "\f52d"; } + +.fa-feather-pointed::before { + content: "\f56b"; } + +.fa-feather-alt::before { + content: "\f56b"; } + +.fa-ferry::before { + content: "\e4ea"; } + +.fa-file::before { + content: "\f15b"; } + +.fa-file-arrow-down::before { + content: "\f56d"; } + +.fa-file-download::before { + content: "\f56d"; } + +.fa-file-arrow-up::before { + content: "\f574"; } + +.fa-file-upload::before { + content: "\f574"; } + +.fa-file-audio::before { + content: "\f1c7"; } + +.fa-file-circle-check::before { + content: "\e493"; } + +.fa-file-circle-exclamation::before { + content: "\e4eb"; } + +.fa-file-circle-minus::before { + content: "\e4ed"; } + +.fa-file-circle-plus::before { + content: "\e4ee"; } + +.fa-file-circle-question::before { + content: "\e4ef"; } + +.fa-file-circle-xmark::before { + content: "\e494"; } + +.fa-file-code::before { + content: "\f1c9"; } + +.fa-file-contract::before { + content: "\f56c"; } + +.fa-file-csv::before { + content: "\f6dd"; } + +.fa-file-excel::before { + content: "\f1c3"; } + +.fa-file-export::before { + content: "\f56e"; } + +.fa-arrow-right-from-file::before { + content: "\f56e"; } + +.fa-file-image::before { + content: "\f1c5"; } + +.fa-file-import::before { + content: "\f56f"; } + +.fa-arrow-right-to-file::before { + content: "\f56f"; } + +.fa-file-invoice::before { + content: "\f570"; } + +.fa-file-invoice-dollar::before { + content: "\f571"; } + +.fa-file-lines::before { + content: "\f15c"; } + +.fa-file-alt::before { + content: "\f15c"; } + +.fa-file-text::before { + content: "\f15c"; } + +.fa-file-medical::before { + content: "\f477"; } + +.fa-file-pdf::before { + content: "\f1c1"; } + +.fa-file-pen::before { + content: "\f31c"; } + +.fa-file-edit::before { + content: "\f31c"; } + +.fa-file-powerpoint::before { + content: "\f1c4"; } + +.fa-file-prescription::before { + content: "\f572"; } + +.fa-file-shield::before { + content: "\e4f0"; } + +.fa-file-signature::before { + content: "\f573"; } + +.fa-file-video::before { + content: "\f1c8"; } + +.fa-file-waveform::before { + content: "\f478"; } + +.fa-file-medical-alt::before { + content: "\f478"; } + +.fa-file-word::before { + content: "\f1c2"; } + +.fa-file-zipper::before { + content: "\f1c6"; } + +.fa-file-archive::before { + content: "\f1c6"; } + +.fa-fill::before { + content: "\f575"; } + +.fa-fill-drip::before { + content: "\f576"; } + +.fa-film::before { + content: "\f008"; } + +.fa-filter::before { + content: "\f0b0"; } + +.fa-filter-circle-dollar::before { + content: "\f662"; } + +.fa-funnel-dollar::before { + content: "\f662"; } + +.fa-filter-circle-xmark::before { + content: "\e17b"; } + +.fa-fingerprint::before { + content: "\f577"; } + +.fa-fire::before { + content: "\f06d"; } + +.fa-fire-burner::before { + content: "\e4f1"; } + +.fa-fire-extinguisher::before { + content: "\f134"; } + +.fa-fire-flame-curved::before { + content: "\f7e4"; } + +.fa-fire-alt::before { + content: "\f7e4"; } + +.fa-fire-flame-simple::before { + content: "\f46a"; } + +.fa-burn::before { + content: "\f46a"; } + +.fa-fish::before { + content: "\f578"; } + +.fa-fish-fins::before { + content: "\e4f2"; } + +.fa-flag::before { + content: "\f024"; } + +.fa-flag-checkered::before { + content: "\f11e"; } + +.fa-flag-usa::before { + content: "\f74d"; } + +.fa-flask::before { + content: "\f0c3"; } + +.fa-flask-vial::before { + content: "\e4f3"; } + +.fa-floppy-disk::before { + content: "\f0c7"; } + +.fa-save::before { + content: "\f0c7"; } + +.fa-florin-sign::before { + content: "\e184"; } + +.fa-folder::before { + content: "\f07b"; } + +.fa-folder-blank::before { + content: "\f07b"; } + +.fa-folder-closed::before { + content: "\e185"; } + +.fa-folder-minus::before { + content: "\f65d"; } + +.fa-folder-open::before { + content: "\f07c"; } + +.fa-folder-plus::before { + content: "\f65e"; } + +.fa-folder-tree::before { + content: "\f802"; } + +.fa-font::before { + content: "\f031"; } + +.fa-football::before { + content: "\f44e"; } + +.fa-football-ball::before { + content: "\f44e"; } + +.fa-forward::before { + content: "\f04e"; } + +.fa-forward-fast::before { + content: "\f050"; } + +.fa-fast-forward::before { + content: "\f050"; } + +.fa-forward-step::before { + content: "\f051"; } + +.fa-step-forward::before { + content: "\f051"; } + +.fa-franc-sign::before { + content: "\e18f"; } + +.fa-frog::before { + content: "\f52e"; } + +.fa-futbol::before { + content: "\f1e3"; } + +.fa-futbol-ball::before { + content: "\f1e3"; } + +.fa-soccer-ball::before { + content: "\f1e3"; } + +.fa-g::before { + content: "\47"; } + +.fa-gamepad::before { + content: "\f11b"; } + +.fa-gas-pump::before { + content: "\f52f"; } + +.fa-gauge::before { + content: "\f624"; } + +.fa-dashboard::before { + content: "\f624"; } + +.fa-gauge-med::before { + content: "\f624"; } + +.fa-tachometer-alt-average::before { + content: "\f624"; } + +.fa-gauge-high::before { + content: "\f625"; } + +.fa-tachometer-alt::before { + content: "\f625"; } + +.fa-tachometer-alt-fast::before { + content: "\f625"; } + +.fa-gauge-simple::before { + content: "\f629"; } + +.fa-gauge-simple-med::before { + content: "\f629"; } + +.fa-tachometer-average::before { + content: "\f629"; } + +.fa-gauge-simple-high::before { + content: "\f62a"; } + +.fa-tachometer::before { + content: "\f62a"; } + +.fa-tachometer-fast::before { + content: "\f62a"; } + +.fa-gavel::before { + content: "\f0e3"; } + +.fa-legal::before { + content: "\f0e3"; } + +.fa-gear::before { + content: "\f013"; } + +.fa-cog::before { + content: "\f013"; } + +.fa-gears::before { + content: "\f085"; } + +.fa-cogs::before { + content: "\f085"; } + +.fa-gem::before { + content: "\f3a5"; } + +.fa-genderless::before { + content: "\f22d"; } + +.fa-ghost::before { + content: "\f6e2"; } + +.fa-gift::before { + content: "\f06b"; } + +.fa-gifts::before { + content: "\f79c"; } + +.fa-glass-water::before { + content: "\e4f4"; } + +.fa-glass-water-droplet::before { + content: "\e4f5"; } + +.fa-glasses::before { + content: "\f530"; } + +.fa-globe::before { + content: "\f0ac"; } + +.fa-golf-ball-tee::before { + content: "\f450"; } + +.fa-golf-ball::before { + content: "\f450"; } + +.fa-gopuram::before { + content: "\f664"; } + +.fa-graduation-cap::before { + content: "\f19d"; } + +.fa-mortar-board::before { + content: "\f19d"; } + +.fa-greater-than::before { + content: "\3e"; } + +.fa-greater-than-equal::before { + content: "\f532"; } + +.fa-grip::before { + content: "\f58d"; } + +.fa-grip-horizontal::before { + content: "\f58d"; } + +.fa-grip-lines::before { + content: "\f7a4"; } + +.fa-grip-lines-vertical::before { + content: "\f7a5"; } + +.fa-grip-vertical::before { + content: "\f58e"; } + +.fa-group-arrows-rotate::before { + content: "\e4f6"; } + +.fa-guarani-sign::before { + content: "\e19a"; } + +.fa-guitar::before { + content: "\f7a6"; } + +.fa-gun::before { + content: "\e19b"; } + +.fa-h::before { + content: "\48"; } + +.fa-hammer::before { + content: "\f6e3"; } + +.fa-hamsa::before { + content: "\f665"; } + +.fa-hand::before { + content: "\f256"; } + +.fa-hand-paper::before { + content: "\f256"; } + +.fa-hand-back-fist::before { + content: "\f255"; } + +.fa-hand-rock::before { + content: "\f255"; } + +.fa-hand-dots::before { + content: "\f461"; } + +.fa-allergies::before { + content: "\f461"; } + +.fa-hand-fist::before { + content: "\f6de"; } + +.fa-fist-raised::before { + content: "\f6de"; } + +.fa-hand-holding::before { + content: "\f4bd"; } + +.fa-hand-holding-dollar::before { + content: "\f4c0"; } + +.fa-hand-holding-usd::before { + content: "\f4c0"; } + +.fa-hand-holding-droplet::before { + content: "\f4c1"; } + +.fa-hand-holding-water::before { + content: "\f4c1"; } + +.fa-hand-holding-hand::before { + content: "\e4f7"; } + +.fa-hand-holding-heart::before { + content: "\f4be"; } + +.fa-hand-holding-medical::before { + content: "\e05c"; } + +.fa-hand-lizard::before { + content: "\f258"; } + +.fa-hand-middle-finger::before { + content: "\f806"; } + +.fa-hand-peace::before { + content: "\f25b"; } + +.fa-hand-point-down::before { + content: "\f0a7"; } + +.fa-hand-point-left::before { + content: "\f0a5"; } + +.fa-hand-point-right::before { + content: "\f0a4"; } + +.fa-hand-point-up::before { + content: "\f0a6"; } + +.fa-hand-pointer::before { + content: "\f25a"; } + +.fa-hand-scissors::before { + content: "\f257"; } + +.fa-hand-sparkles::before { + content: "\e05d"; } + +.fa-hand-spock::before { + content: "\f259"; } + +.fa-handcuffs::before { + content: "\e4f8"; } + +.fa-hands::before { + content: "\f2a7"; } + +.fa-sign-language::before { + content: "\f2a7"; } + +.fa-signing::before { + content: "\f2a7"; } + +.fa-hands-asl-interpreting::before { + content: "\f2a3"; } + +.fa-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-asl-interpreting::before { + content: "\f2a3"; } + +.fa-hands-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-hands-bound::before { + content: "\e4f9"; } + +.fa-hands-bubbles::before { + content: "\e05e"; } + +.fa-hands-wash::before { + content: "\e05e"; } + +.fa-hands-clapping::before { + content: "\e1a8"; } + +.fa-hands-holding::before { + content: "\f4c2"; } + +.fa-hands-holding-child::before { + content: "\e4fa"; } + +.fa-hands-holding-circle::before { + content: "\e4fb"; } + +.fa-hands-praying::before { + content: "\f684"; } + +.fa-praying-hands::before { + content: "\f684"; } + +.fa-handshake::before { + content: "\f2b5"; } + +.fa-handshake-angle::before { + content: "\f4c4"; } + +.fa-hands-helping::before { + content: "\f4c4"; } + +.fa-handshake-simple::before { + content: "\f4c6"; } + +.fa-handshake-alt::before { + content: "\f4c6"; } + +.fa-handshake-simple-slash::before { + content: "\e05f"; } + +.fa-handshake-alt-slash::before { + content: "\e05f"; } + +.fa-handshake-slash::before { + content: "\e060"; } + +.fa-hanukiah::before { + content: "\f6e6"; } + +.fa-hard-drive::before { + content: "\f0a0"; } + +.fa-hdd::before { + content: "\f0a0"; } + +.fa-hashtag::before { + content: "\23"; } + +.fa-hat-cowboy::before { + content: "\f8c0"; } + +.fa-hat-cowboy-side::before { + content: "\f8c1"; } + +.fa-hat-wizard::before { + content: "\f6e8"; } + +.fa-head-side-cough::before { + content: "\e061"; } + +.fa-head-side-cough-slash::before { + content: "\e062"; } + +.fa-head-side-mask::before { + content: "\e063"; } + +.fa-head-side-virus::before { + content: "\e064"; } + +.fa-heading::before { + content: "\f1dc"; } + +.fa-header::before { + content: "\f1dc"; } + +.fa-headphones::before { + content: "\f025"; } + +.fa-headphones-simple::before { + content: "\f58f"; } + +.fa-headphones-alt::before { + content: "\f58f"; } + +.fa-headset::before { + content: "\f590"; } + +.fa-heart::before { + content: "\f004"; } + +.fa-heart-circle-bolt::before { + content: "\e4fc"; } + +.fa-heart-circle-check::before { + content: "\e4fd"; } + +.fa-heart-circle-exclamation::before { + content: "\e4fe"; } + +.fa-heart-circle-minus::before { + content: "\e4ff"; } + +.fa-heart-circle-plus::before { + content: "\e500"; } + +.fa-heart-circle-xmark::before { + content: "\e501"; } + +.fa-heart-crack::before { + content: "\f7a9"; } + +.fa-heart-broken::before { + content: "\f7a9"; } + +.fa-heart-pulse::before { + content: "\f21e"; } + +.fa-heartbeat::before { + content: "\f21e"; } + +.fa-helicopter::before { + content: "\f533"; } + +.fa-helicopter-symbol::before { + content: "\e502"; } + +.fa-helmet-safety::before { + content: "\f807"; } + +.fa-hard-hat::before { + content: "\f807"; } + +.fa-hat-hard::before { + content: "\f807"; } + +.fa-helmet-un::before { + content: "\e503"; } + +.fa-highlighter::before { + content: "\f591"; } + +.fa-hill-avalanche::before { + content: "\e507"; } + +.fa-hill-rockslide::before { + content: "\e508"; } + +.fa-hippo::before { + content: "\f6ed"; } + +.fa-hockey-puck::before { + content: "\f453"; } + +.fa-holly-berry::before { + content: "\f7aa"; } + +.fa-horse::before { + content: "\f6f0"; } + +.fa-horse-head::before { + content: "\f7ab"; } + +.fa-hospital::before { + content: "\f0f8"; } + +.fa-hospital-alt::before { + content: "\f0f8"; } + +.fa-hospital-wide::before { + content: "\f0f8"; } + +.fa-hospital-user::before { + content: "\f80d"; } + +.fa-hot-tub-person::before { + content: "\f593"; } + +.fa-hot-tub::before { + content: "\f593"; } + +.fa-hotdog::before { + content: "\f80f"; } + +.fa-hotel::before { + content: "\f594"; } + +.fa-hourglass::before { + content: "\f254"; } + +.fa-hourglass-2::before { + content: "\f254"; } + +.fa-hourglass-half::before { + content: "\f254"; } + +.fa-hourglass-empty::before { + content: "\f252"; } + +.fa-hourglass-end::before { + content: "\f253"; } + +.fa-hourglass-3::before { + content: "\f253"; } + +.fa-hourglass-start::before { + content: "\f251"; } + +.fa-hourglass-1::before { + content: "\f251"; } + +.fa-house::before { + content: "\f015"; } + +.fa-home::before { + content: "\f015"; } + +.fa-home-alt::before { + content: "\f015"; } + +.fa-home-lg-alt::before { + content: "\f015"; } + +.fa-house-chimney::before { + content: "\e3af"; } + +.fa-home-lg::before { + content: "\e3af"; } + +.fa-house-chimney-crack::before { + content: "\f6f1"; } + +.fa-house-damage::before { + content: "\f6f1"; } + +.fa-house-chimney-medical::before { + content: "\f7f2"; } + +.fa-clinic-medical::before { + content: "\f7f2"; } + +.fa-house-chimney-user::before { + content: "\e065"; } + +.fa-house-chimney-window::before { + content: "\e00d"; } + +.fa-house-circle-check::before { + content: "\e509"; } + +.fa-house-circle-exclamation::before { + content: "\e50a"; } + +.fa-house-circle-xmark::before { + content: "\e50b"; } + +.fa-house-crack::before { + content: "\e3b1"; } + +.fa-house-fire::before { + content: "\e50c"; } + +.fa-house-flag::before { + content: "\e50d"; } + +.fa-house-flood-water::before { + content: "\e50e"; } + +.fa-house-flood-water-circle-arrow-right::before { + content: "\e50f"; } + +.fa-house-laptop::before { + content: "\e066"; } + +.fa-laptop-house::before { + content: "\e066"; } + +.fa-house-lock::before { + content: "\e510"; } + +.fa-house-medical::before { + content: "\e3b2"; } + +.fa-house-medical-circle-check::before { + content: "\e511"; } + +.fa-house-medical-circle-exclamation::before { + content: "\e512"; } + +.fa-house-medical-circle-xmark::before { + content: "\e513"; } + +.fa-house-medical-flag::before { + content: "\e514"; } + +.fa-house-signal::before { + content: "\e012"; } + +.fa-house-tsunami::before { + content: "\e515"; } + +.fa-house-user::before { + content: "\e1b0"; } + +.fa-home-user::before { + content: "\e1b0"; } + +.fa-hryvnia-sign::before { + content: "\f6f2"; } + +.fa-hryvnia::before { + content: "\f6f2"; } + +.fa-hurricane::before { + content: "\f751"; } + +.fa-i::before { + content: "\49"; } + +.fa-i-cursor::before { + content: "\f246"; } + +.fa-ice-cream::before { + content: "\f810"; } + +.fa-icicles::before { + content: "\f7ad"; } + +.fa-icons::before { + content: "\f86d"; } + +.fa-heart-music-camera-bolt::before { + content: "\f86d"; } + +.fa-id-badge::before { + content: "\f2c1"; } + +.fa-id-card::before { + content: "\f2c2"; } + +.fa-drivers-license::before { + content: "\f2c2"; } + +.fa-id-card-clip::before { + content: "\f47f"; } + +.fa-id-card-alt::before { + content: "\f47f"; } + +.fa-igloo::before { + content: "\f7ae"; } + +.fa-image::before { + content: "\f03e"; } + +.fa-image-portrait::before { + content: "\f3e0"; } + +.fa-portrait::before { + content: "\f3e0"; } + +.fa-images::before { + content: "\f302"; } + +.fa-inbox::before { + content: "\f01c"; } + +.fa-indent::before { + content: "\f03c"; } + +.fa-indian-rupee-sign::before { + content: "\e1bc"; } + +.fa-indian-rupee::before { + content: "\e1bc"; } + +.fa-inr::before { + content: "\e1bc"; } + +.fa-industry::before { + content: "\f275"; } + +.fa-infinity::before { + content: "\f534"; } + +.fa-info::before { + content: "\f129"; } + +.fa-italic::before { + content: "\f033"; } + +.fa-j::before { + content: "\4a"; } + +.fa-jar::before { + content: "\e516"; } + +.fa-jar-wheat::before { + content: "\e517"; } + +.fa-jedi::before { + content: "\f669"; } + +.fa-jet-fighter::before { + content: "\f0fb"; } + +.fa-fighter-jet::before { + content: "\f0fb"; } + +.fa-jet-fighter-up::before { + content: "\e518"; } + +.fa-joint::before { + content: "\f595"; } + +.fa-jug-detergent::before { + content: "\e519"; } + +.fa-k::before { + content: "\4b"; } + +.fa-kaaba::before { + content: "\f66b"; } + +.fa-key::before { + content: "\f084"; } + +.fa-keyboard::before { + content: "\f11c"; } + +.fa-khanda::before { + content: "\f66d"; } + +.fa-kip-sign::before { + content: "\e1c4"; } + +.fa-kit-medical::before { + content: "\f479"; } + +.fa-first-aid::before { + content: "\f479"; } + +.fa-kitchen-set::before { + content: "\e51a"; } + +.fa-kiwi-bird::before { + content: "\f535"; } + +.fa-l::before { + content: "\4c"; } + +.fa-land-mine-on::before { + content: "\e51b"; } + +.fa-landmark::before { + content: "\f66f"; } + +.fa-landmark-dome::before { + content: "\f752"; } + +.fa-landmark-alt::before { + content: "\f752"; } + +.fa-landmark-flag::before { + content: "\e51c"; } + +.fa-language::before { + content: "\f1ab"; } + +.fa-laptop::before { + content: "\f109"; } + +.fa-laptop-code::before { + content: "\f5fc"; } + +.fa-laptop-file::before { + content: "\e51d"; } + +.fa-laptop-medical::before { + content: "\f812"; } + +.fa-lari-sign::before { + content: "\e1c8"; } + +.fa-layer-group::before { + content: "\f5fd"; } + +.fa-leaf::before { + content: "\f06c"; } + +.fa-left-long::before { + content: "\f30a"; } + +.fa-long-arrow-alt-left::before { + content: "\f30a"; } + +.fa-left-right::before { + content: "\f337"; } + +.fa-arrows-alt-h::before { + content: "\f337"; } + +.fa-lemon::before { + content: "\f094"; } + +.fa-less-than::before { + content: "\3c"; } + +.fa-less-than-equal::before { + content: "\f537"; } + +.fa-life-ring::before { + content: "\f1cd"; } + +.fa-lightbulb::before { + content: "\f0eb"; } + +.fa-lines-leaning::before { + content: "\e51e"; } + +.fa-link::before { + content: "\f0c1"; } + +.fa-chain::before { + content: "\f0c1"; } + +.fa-link-slash::before { + content: "\f127"; } + +.fa-chain-broken::before { + content: "\f127"; } + +.fa-chain-slash::before { + content: "\f127"; } + +.fa-unlink::before { + content: "\f127"; } + +.fa-lira-sign::before { + content: "\f195"; } + +.fa-list::before { + content: "\f03a"; } + +.fa-list-squares::before { + content: "\f03a"; } + +.fa-list-check::before { + content: "\f0ae"; } + +.fa-tasks::before { + content: "\f0ae"; } + +.fa-list-ol::before { + content: "\f0cb"; } + +.fa-list-1-2::before { + content: "\f0cb"; } + +.fa-list-numeric::before { + content: "\f0cb"; } + +.fa-list-ul::before { + content: "\f0ca"; } + +.fa-list-dots::before { + content: "\f0ca"; } + +.fa-litecoin-sign::before { + content: "\e1d3"; } + +.fa-location-arrow::before { + content: "\f124"; } + +.fa-location-crosshairs::before { + content: "\f601"; } + +.fa-location::before { + content: "\f601"; } + +.fa-location-dot::before { + content: "\f3c5"; } + +.fa-map-marker-alt::before { + content: "\f3c5"; } + +.fa-location-pin::before { + content: "\f041"; } + +.fa-map-marker::before { + content: "\f041"; } + +.fa-location-pin-lock::before { + content: "\e51f"; } + +.fa-lock::before { + content: "\f023"; } + +.fa-lock-open::before { + content: "\f3c1"; } + +.fa-locust::before { + content: "\e520"; } + +.fa-lungs::before { + content: "\f604"; } + +.fa-lungs-virus::before { + content: "\e067"; } + +.fa-m::before { + content: "\4d"; } + +.fa-magnet::before { + content: "\f076"; } + +.fa-magnifying-glass::before { + content: "\f002"; } + +.fa-search::before { + content: "\f002"; } + +.fa-magnifying-glass-arrow-right::before { + content: "\e521"; } + +.fa-magnifying-glass-chart::before { + content: "\e522"; } + +.fa-magnifying-glass-dollar::before { + content: "\f688"; } + +.fa-search-dollar::before { + content: "\f688"; } + +.fa-magnifying-glass-location::before { + content: "\f689"; } + +.fa-search-location::before { + content: "\f689"; } + +.fa-magnifying-glass-minus::before { + content: "\f010"; } + +.fa-search-minus::before { + content: "\f010"; } + +.fa-magnifying-glass-plus::before { + content: "\f00e"; } + +.fa-search-plus::before { + content: "\f00e"; } + +.fa-manat-sign::before { + content: "\e1d5"; } + +.fa-map::before { + content: "\f279"; } + +.fa-map-location::before { + content: "\f59f"; } + +.fa-map-marked::before { + content: "\f59f"; } + +.fa-map-location-dot::before { + content: "\f5a0"; } + +.fa-map-marked-alt::before { + content: "\f5a0"; } + +.fa-map-pin::before { + content: "\f276"; } + +.fa-marker::before { + content: "\f5a1"; } + +.fa-mars::before { + content: "\f222"; } + +.fa-mars-and-venus::before { + content: "\f224"; } + +.fa-mars-and-venus-burst::before { + content: "\e523"; } + +.fa-mars-double::before { + content: "\f227"; } + +.fa-mars-stroke::before { + content: "\f229"; } + +.fa-mars-stroke-right::before { + content: "\f22b"; } + +.fa-mars-stroke-h::before { + content: "\f22b"; } + +.fa-mars-stroke-up::before { + content: "\f22a"; } + +.fa-mars-stroke-v::before { + content: "\f22a"; } + +.fa-martini-glass::before { + content: "\f57b"; } + +.fa-glass-martini-alt::before { + content: "\f57b"; } + +.fa-martini-glass-citrus::before { + content: "\f561"; } + +.fa-cocktail::before { + content: "\f561"; } + +.fa-martini-glass-empty::before { + content: "\f000"; } + +.fa-glass-martini::before { + content: "\f000"; } + +.fa-mask::before { + content: "\f6fa"; } + +.fa-mask-face::before { + content: "\e1d7"; } + +.fa-mask-ventilator::before { + content: "\e524"; } + +.fa-masks-theater::before { + content: "\f630"; } + +.fa-theater-masks::before { + content: "\f630"; } + +.fa-mattress-pillow::before { + content: "\e525"; } + +.fa-maximize::before { + content: "\f31e"; } + +.fa-expand-arrows-alt::before { + content: "\f31e"; } + +.fa-medal::before { + content: "\f5a2"; } + +.fa-memory::before { + content: "\f538"; } + +.fa-menorah::before { + content: "\f676"; } + +.fa-mercury::before { + content: "\f223"; } + +.fa-message::before { + content: "\f27a"; } + +.fa-comment-alt::before { + content: "\f27a"; } + +.fa-meteor::before { + content: "\f753"; } + +.fa-microchip::before { + content: "\f2db"; } + +.fa-microphone::before { + content: "\f130"; } + +.fa-microphone-lines::before { + content: "\f3c9"; } + +.fa-microphone-alt::before { + content: "\f3c9"; } + +.fa-microphone-lines-slash::before { + content: "\f539"; } + +.fa-microphone-alt-slash::before { + content: "\f539"; } + +.fa-microphone-slash::before { + content: "\f131"; } + +.fa-microscope::before { + content: "\f610"; } + +.fa-mill-sign::before { + content: "\e1ed"; } + +.fa-minimize::before { + content: "\f78c"; } + +.fa-compress-arrows-alt::before { + content: "\f78c"; } + +.fa-minus::before { + content: "\f068"; } + +.fa-subtract::before { + content: "\f068"; } + +.fa-mitten::before { + content: "\f7b5"; } + +.fa-mobile::before { + content: "\f3ce"; } + +.fa-mobile-android::before { + content: "\f3ce"; } + +.fa-mobile-phone::before { + content: "\f3ce"; } + +.fa-mobile-button::before { + content: "\f10b"; } + +.fa-mobile-retro::before { + content: "\e527"; } + +.fa-mobile-screen::before { + content: "\f3cf"; } + +.fa-mobile-android-alt::before { + content: "\f3cf"; } + +.fa-mobile-screen-button::before { + content: "\f3cd"; } + +.fa-mobile-alt::before { + content: "\f3cd"; } + +.fa-money-bill::before { + content: "\f0d6"; } + +.fa-money-bill-1::before { + content: "\f3d1"; } + +.fa-money-bill-alt::before { + content: "\f3d1"; } + +.fa-money-bill-1-wave::before { + content: "\f53b"; } + +.fa-money-bill-wave-alt::before { + content: "\f53b"; } + +.fa-money-bill-transfer::before { + content: "\e528"; } + +.fa-money-bill-trend-up::before { + content: "\e529"; } + +.fa-money-bill-wave::before { + content: "\f53a"; } + +.fa-money-bill-wheat::before { + content: "\e52a"; } + +.fa-money-bills::before { + content: "\e1f3"; } + +.fa-money-check::before { + content: "\f53c"; } + +.fa-money-check-dollar::before { + content: "\f53d"; } + +.fa-money-check-alt::before { + content: "\f53d"; } + +.fa-monument::before { + content: "\f5a6"; } + +.fa-moon::before { + content: "\f186"; } + +.fa-mortar-pestle::before { + content: "\f5a7"; } + +.fa-mosque::before { + content: "\f678"; } + +.fa-mosquito::before { + content: "\e52b"; } + +.fa-mosquito-net::before { + content: "\e52c"; } + +.fa-motorcycle::before { + content: "\f21c"; } + +.fa-mound::before { + content: "\e52d"; } + +.fa-mountain::before { + content: "\f6fc"; } + +.fa-mountain-city::before { + content: "\e52e"; } + +.fa-mountain-sun::before { + content: "\e52f"; } + +.fa-mug-hot::before { + content: "\f7b6"; } + +.fa-mug-saucer::before { + content: "\f0f4"; } + +.fa-coffee::before { + content: "\f0f4"; } + +.fa-music::before { + content: "\f001"; } + +.fa-n::before { + content: "\4e"; } + +.fa-naira-sign::before { + content: "\e1f6"; } + +.fa-network-wired::before { + content: "\f6ff"; } + +.fa-neuter::before { + content: "\f22c"; } + +.fa-newspaper::before { + content: "\f1ea"; } + +.fa-not-equal::before { + content: "\f53e"; } + +.fa-note-sticky::before { + content: "\f249"; } + +.fa-sticky-note::before { + content: "\f249"; } + +.fa-notes-medical::before { + content: "\f481"; } + +.fa-o::before { + content: "\4f"; } + +.fa-object-group::before { + content: "\f247"; } + +.fa-object-ungroup::before { + content: "\f248"; } + +.fa-oil-can::before { + content: "\f613"; } + +.fa-oil-well::before { + content: "\e532"; } + +.fa-om::before { + content: "\f679"; } + +.fa-otter::before { + content: "\f700"; } + +.fa-outdent::before { + content: "\f03b"; } + +.fa-dedent::before { + content: "\f03b"; } + +.fa-p::before { + content: "\50"; } + +.fa-pager::before { + content: "\f815"; } + +.fa-paint-roller::before { + content: "\f5aa"; } + +.fa-paintbrush::before { + content: "\f1fc"; } + +.fa-paint-brush::before { + content: "\f1fc"; } + +.fa-palette::before { + content: "\f53f"; } + +.fa-pallet::before { + content: "\f482"; } + +.fa-panorama::before { + content: "\e209"; } + +.fa-paper-plane::before { + content: "\f1d8"; } + +.fa-paperclip::before { + content: "\f0c6"; } + +.fa-parachute-box::before { + content: "\f4cd"; } + +.fa-paragraph::before { + content: "\f1dd"; } + +.fa-passport::before { + content: "\f5ab"; } + +.fa-paste::before { + content: "\f0ea"; } + +.fa-file-clipboard::before { + content: "\f0ea"; } + +.fa-pause::before { + content: "\f04c"; } + +.fa-paw::before { + content: "\f1b0"; } + +.fa-peace::before { + content: "\f67c"; } + +.fa-pen::before { + content: "\f304"; } + +.fa-pen-clip::before { + content: "\f305"; } + +.fa-pen-alt::before { + content: "\f305"; } + +.fa-pen-fancy::before { + content: "\f5ac"; } + +.fa-pen-nib::before { + content: "\f5ad"; } + +.fa-pen-ruler::before { + content: "\f5ae"; } + +.fa-pencil-ruler::before { + content: "\f5ae"; } + +.fa-pen-to-square::before { + content: "\f044"; } + +.fa-edit::before { + content: "\f044"; } + +.fa-pencil::before { + content: "\f303"; } + +.fa-pencil-alt::before { + content: "\f303"; } + +.fa-people-arrows-left-right::before { + content: "\e068"; } + +.fa-people-arrows::before { + content: "\e068"; } + +.fa-people-carry-box::before { + content: "\f4ce"; } + +.fa-people-carry::before { + content: "\f4ce"; } + +.fa-people-group::before { + content: "\e533"; } + +.fa-people-line::before { + content: "\e534"; } + +.fa-people-pulling::before { + content: "\e535"; } + +.fa-people-robbery::before { + content: "\e536"; } + +.fa-people-roof::before { + content: "\e537"; } + +.fa-pepper-hot::before { + content: "\f816"; } + +.fa-percent::before { + content: "\25"; } + +.fa-percentage::before { + content: "\25"; } + +.fa-person::before { + content: "\f183"; } + +.fa-male::before { + content: "\f183"; } + +.fa-person-arrow-down-to-line::before { + content: "\e538"; } + +.fa-person-arrow-up-from-line::before { + content: "\e539"; } + +.fa-person-biking::before { + content: "\f84a"; } + +.fa-biking::before { + content: "\f84a"; } + +.fa-person-booth::before { + content: "\f756"; } + +.fa-person-breastfeeding::before { + content: "\e53a"; } + +.fa-person-burst::before { + content: "\e53b"; } + +.fa-person-cane::before { + content: "\e53c"; } + +.fa-person-chalkboard::before { + content: "\e53d"; } + +.fa-person-circle-check::before { + content: "\e53e"; } + +.fa-person-circle-exclamation::before { + content: "\e53f"; } + +.fa-person-circle-minus::before { + content: "\e540"; } + +.fa-person-circle-plus::before { + content: "\e541"; } + +.fa-person-circle-question::before { + content: "\e542"; } + +.fa-person-circle-xmark::before { + content: "\e543"; } + +.fa-person-digging::before { + content: "\f85e"; } + +.fa-digging::before { + content: "\f85e"; } + +.fa-person-dots-from-line::before { + content: "\f470"; } + +.fa-diagnoses::before { + content: "\f470"; } + +.fa-person-dress::before { + content: "\f182"; } + +.fa-female::before { + content: "\f182"; } + +.fa-person-dress-burst::before { + content: "\e544"; } + +.fa-person-drowning::before { + content: "\e545"; } + +.fa-person-falling::before { + content: "\e546"; } + +.fa-person-falling-burst::before { + content: "\e547"; } + +.fa-person-half-dress::before { + content: "\e548"; } + +.fa-person-harassing::before { + content: "\e549"; } + +.fa-person-hiking::before { + content: "\f6ec"; } + +.fa-hiking::before { + content: "\f6ec"; } + +.fa-person-military-pointing::before { + content: "\e54a"; } + +.fa-person-military-rifle::before { + content: "\e54b"; } + +.fa-person-military-to-person::before { + content: "\e54c"; } + +.fa-person-praying::before { + content: "\f683"; } + +.fa-pray::before { + content: "\f683"; } + +.fa-person-pregnant::before { + content: "\e31e"; } + +.fa-person-rays::before { + content: "\e54d"; } + +.fa-person-rifle::before { + content: "\e54e"; } + +.fa-person-running::before { + content: "\f70c"; } + +.fa-running::before { + content: "\f70c"; } + +.fa-person-shelter::before { + content: "\e54f"; } + +.fa-person-skating::before { + content: "\f7c5"; } + +.fa-skating::before { + content: "\f7c5"; } + +.fa-person-skiing::before { + content: "\f7c9"; } + +.fa-skiing::before { + content: "\f7c9"; } + +.fa-person-skiing-nordic::before { + content: "\f7ca"; } + +.fa-skiing-nordic::before { + content: "\f7ca"; } + +.fa-person-snowboarding::before { + content: "\f7ce"; } + +.fa-snowboarding::before { + content: "\f7ce"; } + +.fa-person-swimming::before { + content: "\f5c4"; } + +.fa-swimmer::before { + content: "\f5c4"; } + +.fa-person-through-window::before { + content: "\e433"; } + +.fa-person-walking::before { + content: "\f554"; } + +.fa-walking::before { + content: "\f554"; } + +.fa-person-walking-arrow-loop-left::before { + content: "\e551"; } + +.fa-person-walking-arrow-right::before { + content: "\e552"; } + +.fa-person-walking-dashed-line-arrow-right::before { + content: "\e553"; } + +.fa-person-walking-luggage::before { + content: "\e554"; } + +.fa-person-walking-with-cane::before { + content: "\f29d"; } + +.fa-blind::before { + content: "\f29d"; } + +.fa-peseta-sign::before { + content: "\e221"; } + +.fa-peso-sign::before { + content: "\e222"; } + +.fa-phone::before { + content: "\f095"; } + +.fa-phone-flip::before { + content: "\f879"; } + +.fa-phone-alt::before { + content: "\f879"; } + +.fa-phone-slash::before { + content: "\f3dd"; } + +.fa-phone-volume::before { + content: "\f2a0"; } + +.fa-volume-control-phone::before { + content: "\f2a0"; } + +.fa-photo-film::before { + content: "\f87c"; } + +.fa-photo-video::before { + content: "\f87c"; } + +.fa-piggy-bank::before { + content: "\f4d3"; } + +.fa-pills::before { + content: "\f484"; } + +.fa-pizza-slice::before { + content: "\f818"; } + +.fa-place-of-worship::before { + content: "\f67f"; } + +.fa-plane::before { + content: "\f072"; } + +.fa-plane-arrival::before { + content: "\f5af"; } + +.fa-plane-circle-check::before { + content: "\e555"; } + +.fa-plane-circle-exclamation::before { + content: "\e556"; } + +.fa-plane-circle-xmark::before { + content: "\e557"; } + +.fa-plane-departure::before { + content: "\f5b0"; } + +.fa-plane-lock::before { + content: "\e558"; } + +.fa-plane-slash::before { + content: "\e069"; } + +.fa-plane-up::before { + content: "\e22d"; } + +.fa-plant-wilt::before { + content: "\e43b"; } + +.fa-plate-wheat::before { + content: "\e55a"; } + +.fa-play::before { + content: "\f04b"; } + +.fa-plug::before { + content: "\f1e6"; } + +.fa-plug-circle-bolt::before { + content: "\e55b"; } + +.fa-plug-circle-check::before { + content: "\e55c"; } + +.fa-plug-circle-exclamation::before { + content: "\e55d"; } + +.fa-plug-circle-minus::before { + content: "\e55e"; } + +.fa-plug-circle-plus::before { + content: "\e55f"; } + +.fa-plug-circle-xmark::before { + content: "\e560"; } + +.fa-plus::before { + content: "\2b"; } + +.fa-add::before { + content: "\2b"; } + +.fa-plus-minus::before { + content: "\e43c"; } + +.fa-podcast::before { + content: "\f2ce"; } + +.fa-poo::before { + content: "\f2fe"; } + +.fa-poo-storm::before { + content: "\f75a"; } + +.fa-poo-bolt::before { + content: "\f75a"; } + +.fa-poop::before { + content: "\f619"; } + +.fa-power-off::before { + content: "\f011"; } + +.fa-prescription::before { + content: "\f5b1"; } + +.fa-prescription-bottle::before { + content: "\f485"; } + +.fa-prescription-bottle-medical::before { + content: "\f486"; } + +.fa-prescription-bottle-alt::before { + content: "\f486"; } + +.fa-print::before { + content: "\f02f"; } + +.fa-pump-medical::before { + content: "\e06a"; } + +.fa-pump-soap::before { + content: "\e06b"; } + +.fa-puzzle-piece::before { + content: "\f12e"; } + +.fa-q::before { + content: "\51"; } + +.fa-qrcode::before { + content: "\f029"; } + +.fa-question::before { + content: "\3f"; } + +.fa-quote-left::before { + content: "\f10d"; } + +.fa-quote-left-alt::before { + content: "\f10d"; } + +.fa-quote-right::before { + content: "\f10e"; } + +.fa-quote-right-alt::before { + content: "\f10e"; } + +.fa-r::before { + content: "\52"; } + +.fa-radiation::before { + content: "\f7b9"; } + +.fa-radio::before { + content: "\f8d7"; } + +.fa-rainbow::before { + content: "\f75b"; } + +.fa-ranking-star::before { + content: "\e561"; } + +.fa-receipt::before { + content: "\f543"; } + +.fa-record-vinyl::before { + content: "\f8d9"; } + +.fa-rectangle-ad::before { + content: "\f641"; } + +.fa-ad::before { + content: "\f641"; } + +.fa-rectangle-list::before { + content: "\f022"; } + +.fa-list-alt::before { + content: "\f022"; } + +.fa-rectangle-xmark::before { + content: "\f410"; } + +.fa-rectangle-times::before { + content: "\f410"; } + +.fa-times-rectangle::before { + content: "\f410"; } + +.fa-window-close::before { + content: "\f410"; } + +.fa-recycle::before { + content: "\f1b8"; } + +.fa-registered::before { + content: "\f25d"; } + +.fa-repeat::before { + content: "\f363"; } + +.fa-reply::before { + content: "\f3e5"; } + +.fa-mail-reply::before { + content: "\f3e5"; } + +.fa-reply-all::before { + content: "\f122"; } + +.fa-mail-reply-all::before { + content: "\f122"; } + +.fa-republican::before { + content: "\f75e"; } + +.fa-restroom::before { + content: "\f7bd"; } + +.fa-retweet::before { + content: "\f079"; } + +.fa-ribbon::before { + content: "\f4d6"; } + +.fa-right-from-bracket::before { + content: "\f2f5"; } + +.fa-sign-out-alt::before { + content: "\f2f5"; } + +.fa-right-left::before { + content: "\f362"; } + +.fa-exchange-alt::before { + content: "\f362"; } + +.fa-right-long::before { + content: "\f30b"; } + +.fa-long-arrow-alt-right::before { + content: "\f30b"; } + +.fa-right-to-bracket::before { + content: "\f2f6"; } + +.fa-sign-in-alt::before { + content: "\f2f6"; } + +.fa-ring::before { + content: "\f70b"; } + +.fa-road::before { + content: "\f018"; } + +.fa-road-barrier::before { + content: "\e562"; } + +.fa-road-bridge::before { + content: "\e563"; } + +.fa-road-circle-check::before { + content: "\e564"; } + +.fa-road-circle-exclamation::before { + content: "\e565"; } + +.fa-road-circle-xmark::before { + content: "\e566"; } + +.fa-road-lock::before { + content: "\e567"; } + +.fa-road-spikes::before { + content: "\e568"; } + +.fa-robot::before { + content: "\f544"; } + +.fa-rocket::before { + content: "\f135"; } + +.fa-rotate::before { + content: "\f2f1"; } + +.fa-sync-alt::before { + content: "\f2f1"; } + +.fa-rotate-left::before { + content: "\f2ea"; } + +.fa-rotate-back::before { + content: "\f2ea"; } + +.fa-rotate-backward::before { + content: "\f2ea"; } + +.fa-undo-alt::before { + content: "\f2ea"; } + +.fa-rotate-right::before { + content: "\f2f9"; } + +.fa-redo-alt::before { + content: "\f2f9"; } + +.fa-rotate-forward::before { + content: "\f2f9"; } + +.fa-route::before { + content: "\f4d7"; } + +.fa-rss::before { + content: "\f09e"; } + +.fa-feed::before { + content: "\f09e"; } + +.fa-ruble-sign::before { + content: "\f158"; } + +.fa-rouble::before { + content: "\f158"; } + +.fa-rub::before { + content: "\f158"; } + +.fa-ruble::before { + content: "\f158"; } + +.fa-rug::before { + content: "\e569"; } + +.fa-ruler::before { + content: "\f545"; } + +.fa-ruler-combined::before { + content: "\f546"; } + +.fa-ruler-horizontal::before { + content: "\f547"; } + +.fa-ruler-vertical::before { + content: "\f548"; } + +.fa-rupee-sign::before { + content: "\f156"; } + +.fa-rupee::before { + content: "\f156"; } + +.fa-rupiah-sign::before { + content: "\e23d"; } + +.fa-s::before { + content: "\53"; } + +.fa-sack-dollar::before { + content: "\f81d"; } + +.fa-sack-xmark::before { + content: "\e56a"; } + +.fa-sailboat::before { + content: "\e445"; } + +.fa-satellite::before { + content: "\f7bf"; } + +.fa-satellite-dish::before { + content: "\f7c0"; } + +.fa-scale-balanced::before { + content: "\f24e"; } + +.fa-balance-scale::before { + content: "\f24e"; } + +.fa-scale-unbalanced::before { + content: "\f515"; } + +.fa-balance-scale-left::before { + content: "\f515"; } + +.fa-scale-unbalanced-flip::before { + content: "\f516"; } + +.fa-balance-scale-right::before { + content: "\f516"; } + +.fa-school::before { + content: "\f549"; } + +.fa-school-circle-check::before { + content: "\e56b"; } + +.fa-school-circle-exclamation::before { + content: "\e56c"; } + +.fa-school-circle-xmark::before { + content: "\e56d"; } + +.fa-school-flag::before { + content: "\e56e"; } + +.fa-school-lock::before { + content: "\e56f"; } + +.fa-scissors::before { + content: "\f0c4"; } + +.fa-cut::before { + content: "\f0c4"; } + +.fa-screwdriver::before { + content: "\f54a"; } + +.fa-screwdriver-wrench::before { + content: "\f7d9"; } + +.fa-tools::before { + content: "\f7d9"; } + +.fa-scroll::before { + content: "\f70e"; } + +.fa-scroll-torah::before { + content: "\f6a0"; } + +.fa-torah::before { + content: "\f6a0"; } + +.fa-sd-card::before { + content: "\f7c2"; } + +.fa-section::before { + content: "\e447"; } + +.fa-seedling::before { + content: "\f4d8"; } + +.fa-sprout::before { + content: "\f4d8"; } + +.fa-server::before { + content: "\f233"; } + +.fa-shapes::before { + content: "\f61f"; } + +.fa-triangle-circle-square::before { + content: "\f61f"; } + +.fa-share::before { + content: "\f064"; } + +.fa-arrow-turn-right::before { + content: "\f064"; } + +.fa-mail-forward::before { + content: "\f064"; } + +.fa-share-from-square::before { + content: "\f14d"; } + +.fa-share-square::before { + content: "\f14d"; } + +.fa-share-nodes::before { + content: "\f1e0"; } + +.fa-share-alt::before { + content: "\f1e0"; } + +.fa-sheet-plastic::before { + content: "\e571"; } + +.fa-shekel-sign::before { + content: "\f20b"; } + +.fa-ils::before { + content: "\f20b"; } + +.fa-shekel::before { + content: "\f20b"; } + +.fa-sheqel::before { + content: "\f20b"; } + +.fa-sheqel-sign::before { + content: "\f20b"; } + +.fa-shield::before { + content: "\f132"; } + +.fa-shield-blank::before { + content: "\f132"; } + +.fa-shield-cat::before { + content: "\e572"; } + +.fa-shield-dog::before { + content: "\e573"; } + +.fa-shield-halved::before { + content: "\f3ed"; } + +.fa-shield-alt::before { + content: "\f3ed"; } + +.fa-shield-heart::before { + content: "\e574"; } + +.fa-shield-virus::before { + content: "\e06c"; } + +.fa-ship::before { + content: "\f21a"; } + +.fa-shirt::before { + content: "\f553"; } + +.fa-t-shirt::before { + content: "\f553"; } + +.fa-tshirt::before { + content: "\f553"; } + +.fa-shoe-prints::before { + content: "\f54b"; } + +.fa-shop::before { + content: "\f54f"; } + +.fa-store-alt::before { + content: "\f54f"; } + +.fa-shop-lock::before { + content: "\e4a5"; } + +.fa-shop-slash::before { + content: "\e070"; } + +.fa-store-alt-slash::before { + content: "\e070"; } + +.fa-shower::before { + content: "\f2cc"; } + +.fa-shrimp::before { + content: "\e448"; } + +.fa-shuffle::before { + content: "\f074"; } + +.fa-random::before { + content: "\f074"; } + +.fa-shuttle-space::before { + content: "\f197"; } + +.fa-space-shuttle::before { + content: "\f197"; } + +.fa-sign-hanging::before { + content: "\f4d9"; } + +.fa-sign::before { + content: "\f4d9"; } + +.fa-signal::before { + content: "\f012"; } + +.fa-signal-5::before { + content: "\f012"; } + +.fa-signal-perfect::before { + content: "\f012"; } + +.fa-signature::before { + content: "\f5b7"; } + +.fa-signs-post::before { + content: "\f277"; } + +.fa-map-signs::before { + content: "\f277"; } + +.fa-sim-card::before { + content: "\f7c4"; } + +.fa-sink::before { + content: "\e06d"; } + +.fa-sitemap::before { + content: "\f0e8"; } + +.fa-skull::before { + content: "\f54c"; } + +.fa-skull-crossbones::before { + content: "\f714"; } + +.fa-slash::before { + content: "\f715"; } + +.fa-sleigh::before { + content: "\f7cc"; } + +.fa-sliders::before { + content: "\f1de"; } + +.fa-sliders-h::before { + content: "\f1de"; } + +.fa-smog::before { + content: "\f75f"; } + +.fa-smoking::before { + content: "\f48d"; } + +.fa-snowflake::before { + content: "\f2dc"; } + +.fa-snowman::before { + content: "\f7d0"; } + +.fa-snowplow::before { + content: "\f7d2"; } + +.fa-soap::before { + content: "\e06e"; } + +.fa-socks::before { + content: "\f696"; } + +.fa-solar-panel::before { + content: "\f5ba"; } + +.fa-sort::before { + content: "\f0dc"; } + +.fa-unsorted::before { + content: "\f0dc"; } + +.fa-sort-down::before { + content: "\f0dd"; } + +.fa-sort-desc::before { + content: "\f0dd"; } + +.fa-sort-up::before { + content: "\f0de"; } + +.fa-sort-asc::before { + content: "\f0de"; } + +.fa-spa::before { + content: "\f5bb"; } + +.fa-spaghetti-monster-flying::before { + content: "\f67b"; } + +.fa-pastafarianism::before { + content: "\f67b"; } + +.fa-spell-check::before { + content: "\f891"; } + +.fa-spider::before { + content: "\f717"; } + +.fa-spinner::before { + content: "\f110"; } + +.fa-splotch::before { + content: "\f5bc"; } + +.fa-spoon::before { + content: "\f2e5"; } + +.fa-utensil-spoon::before { + content: "\f2e5"; } + +.fa-spray-can::before { + content: "\f5bd"; } + +.fa-spray-can-sparkles::before { + content: "\f5d0"; } + +.fa-air-freshener::before { + content: "\f5d0"; } + +.fa-square::before { + content: "\f0c8"; } + +.fa-square-arrow-up-right::before { + content: "\f14c"; } + +.fa-external-link-square::before { + content: "\f14c"; } + +.fa-square-caret-down::before { + content: "\f150"; } + +.fa-caret-square-down::before { + content: "\f150"; } + +.fa-square-caret-left::before { + content: "\f191"; } + +.fa-caret-square-left::before { + content: "\f191"; } + +.fa-square-caret-right::before { + content: "\f152"; } + +.fa-caret-square-right::before { + content: "\f152"; } + +.fa-square-caret-up::before { + content: "\f151"; } + +.fa-caret-square-up::before { + content: "\f151"; } + +.fa-square-check::before { + content: "\f14a"; } + +.fa-check-square::before { + content: "\f14a"; } + +.fa-square-envelope::before { + content: "\f199"; } + +.fa-envelope-square::before { + content: "\f199"; } + +.fa-square-full::before { + content: "\f45c"; } + +.fa-square-h::before { + content: "\f0fd"; } + +.fa-h-square::before { + content: "\f0fd"; } + +.fa-square-minus::before { + content: "\f146"; } + +.fa-minus-square::before { + content: "\f146"; } + +.fa-square-nfi::before { + content: "\e576"; } + +.fa-square-parking::before { + content: "\f540"; } + +.fa-parking::before { + content: "\f540"; } + +.fa-square-pen::before { + content: "\f14b"; } + +.fa-pen-square::before { + content: "\f14b"; } + +.fa-pencil-square::before { + content: "\f14b"; } + +.fa-square-person-confined::before { + content: "\e577"; } + +.fa-square-phone::before { + content: "\f098"; } + +.fa-phone-square::before { + content: "\f098"; } + +.fa-square-phone-flip::before { + content: "\f87b"; } + +.fa-phone-square-alt::before { + content: "\f87b"; } + +.fa-square-plus::before { + content: "\f0fe"; } + +.fa-plus-square::before { + content: "\f0fe"; } + +.fa-square-poll-horizontal::before { + content: "\f682"; } + +.fa-poll-h::before { + content: "\f682"; } + +.fa-square-poll-vertical::before { + content: "\f681"; } + +.fa-poll::before { + content: "\f681"; } + +.fa-square-root-variable::before { + content: "\f698"; } + +.fa-square-root-alt::before { + content: "\f698"; } + +.fa-square-rss::before { + content: "\f143"; } + +.fa-rss-square::before { + content: "\f143"; } + +.fa-square-share-nodes::before { + content: "\f1e1"; } + +.fa-share-alt-square::before { + content: "\f1e1"; } + +.fa-square-up-right::before { + content: "\f360"; } + +.fa-external-link-square-alt::before { + content: "\f360"; } + +.fa-square-virus::before { + content: "\e578"; } + +.fa-square-xmark::before { + content: "\f2d3"; } + +.fa-times-square::before { + content: "\f2d3"; } + +.fa-xmark-square::before { + content: "\f2d3"; } + +.fa-staff-aesculapius::before { + content: "\e579"; } + +.fa-rod-asclepius::before { + content: "\e579"; } + +.fa-rod-snake::before { + content: "\e579"; } + +.fa-staff-snake::before { + content: "\e579"; } + +.fa-stairs::before { + content: "\e289"; } + +.fa-stamp::before { + content: "\f5bf"; } + +.fa-star::before { + content: "\f005"; } + +.fa-star-and-crescent::before { + content: "\f699"; } + +.fa-star-half::before { + content: "\f089"; } + +.fa-star-half-stroke::before { + content: "\f5c0"; } + +.fa-star-half-alt::before { + content: "\f5c0"; } + +.fa-star-of-david::before { + content: "\f69a"; } + +.fa-star-of-life::before { + content: "\f621"; } + +.fa-sterling-sign::before { + content: "\f154"; } + +.fa-gbp::before { + content: "\f154"; } + +.fa-pound-sign::before { + content: "\f154"; } + +.fa-stethoscope::before { + content: "\f0f1"; } + +.fa-stop::before { + content: "\f04d"; } + +.fa-stopwatch::before { + content: "\f2f2"; } + +.fa-stopwatch-20::before { + content: "\e06f"; } + +.fa-store::before { + content: "\f54e"; } + +.fa-store-slash::before { + content: "\e071"; } + +.fa-street-view::before { + content: "\f21d"; } + +.fa-strikethrough::before { + content: "\f0cc"; } + +.fa-stroopwafel::before { + content: "\f551"; } + +.fa-subscript::before { + content: "\f12c"; } + +.fa-suitcase::before { + content: "\f0f2"; } + +.fa-suitcase-medical::before { + content: "\f0fa"; } + +.fa-medkit::before { + content: "\f0fa"; } + +.fa-suitcase-rolling::before { + content: "\f5c1"; } + +.fa-sun::before { + content: "\f185"; } + +.fa-sun-plant-wilt::before { + content: "\e57a"; } + +.fa-superscript::before { + content: "\f12b"; } + +.fa-swatchbook::before { + content: "\f5c3"; } + +.fa-synagogue::before { + content: "\f69b"; } + +.fa-syringe::before { + content: "\f48e"; } + +.fa-t::before { + content: "\54"; } + +.fa-table::before { + content: "\f0ce"; } + +.fa-table-cells::before { + content: "\f00a"; } + +.fa-th::before { + content: "\f00a"; } + +.fa-table-cells-large::before { + content: "\f009"; } + +.fa-th-large::before { + content: "\f009"; } + +.fa-table-columns::before { + content: "\f0db"; } + +.fa-columns::before { + content: "\f0db"; } + +.fa-table-list::before { + content: "\f00b"; } + +.fa-th-list::before { + content: "\f00b"; } + +.fa-table-tennis-paddle-ball::before { + content: "\f45d"; } + +.fa-ping-pong-paddle-ball::before { + content: "\f45d"; } + +.fa-table-tennis::before { + content: "\f45d"; } + +.fa-tablet::before { + content: "\f3fb"; } + +.fa-tablet-android::before { + content: "\f3fb"; } + +.fa-tablet-button::before { + content: "\f10a"; } + +.fa-tablet-screen-button::before { + content: "\f3fa"; } + +.fa-tablet-alt::before { + content: "\f3fa"; } + +.fa-tablets::before { + content: "\f490"; } + +.fa-tachograph-digital::before { + content: "\f566"; } + +.fa-digital-tachograph::before { + content: "\f566"; } + +.fa-tag::before { + content: "\f02b"; } + +.fa-tags::before { + content: "\f02c"; } + +.fa-tape::before { + content: "\f4db"; } + +.fa-tarp::before { + content: "\e57b"; } + +.fa-tarp-droplet::before { + content: "\e57c"; } + +.fa-taxi::before { + content: "\f1ba"; } + +.fa-cab::before { + content: "\f1ba"; } + +.fa-teeth::before { + content: "\f62e"; } + +.fa-teeth-open::before { + content: "\f62f"; } + +.fa-temperature-arrow-down::before { + content: "\e03f"; } + +.fa-temperature-down::before { + content: "\e03f"; } + +.fa-temperature-arrow-up::before { + content: "\e040"; } + +.fa-temperature-up::before { + content: "\e040"; } + +.fa-temperature-empty::before { + content: "\f2cb"; } + +.fa-temperature-0::before { + content: "\f2cb"; } + +.fa-thermometer-0::before { + content: "\f2cb"; } + +.fa-thermometer-empty::before { + content: "\f2cb"; } + +.fa-temperature-full::before { + content: "\f2c7"; } + +.fa-temperature-4::before { + content: "\f2c7"; } + +.fa-thermometer-4::before { + content: "\f2c7"; } + +.fa-thermometer-full::before { + content: "\f2c7"; } + +.fa-temperature-half::before { + content: "\f2c9"; } + +.fa-temperature-2::before { + content: "\f2c9"; } + +.fa-thermometer-2::before { + content: "\f2c9"; } + +.fa-thermometer-half::before { + content: "\f2c9"; } + +.fa-temperature-high::before { + content: "\f769"; } + +.fa-temperature-low::before { + content: "\f76b"; } + +.fa-temperature-quarter::before { + content: "\f2ca"; } + +.fa-temperature-1::before { + content: "\f2ca"; } + +.fa-thermometer-1::before { + content: "\f2ca"; } + +.fa-thermometer-quarter::before { + content: "\f2ca"; } + +.fa-temperature-three-quarters::before { + content: "\f2c8"; } + +.fa-temperature-3::before { + content: "\f2c8"; } + +.fa-thermometer-3::before { + content: "\f2c8"; } + +.fa-thermometer-three-quarters::before { + content: "\f2c8"; } + +.fa-tenge-sign::before { + content: "\f7d7"; } + +.fa-tenge::before { + content: "\f7d7"; } + +.fa-tent::before { + content: "\e57d"; } + +.fa-tent-arrow-down-to-line::before { + content: "\e57e"; } + +.fa-tent-arrow-left-right::before { + content: "\e57f"; } + +.fa-tent-arrow-turn-left::before { + content: "\e580"; } + +.fa-tent-arrows-down::before { + content: "\e581"; } + +.fa-tents::before { + content: "\e582"; } + +.fa-terminal::before { + content: "\f120"; } + +.fa-text-height::before { + content: "\f034"; } + +.fa-text-slash::before { + content: "\f87d"; } + +.fa-remove-format::before { + content: "\f87d"; } + +.fa-text-width::before { + content: "\f035"; } + +.fa-thermometer::before { + content: "\f491"; } + +.fa-thumbs-down::before { + content: "\f165"; } + +.fa-thumbs-up::before { + content: "\f164"; } + +.fa-thumbtack::before { + content: "\f08d"; } + +.fa-thumb-tack::before { + content: "\f08d"; } + +.fa-ticket::before { + content: "\f145"; } + +.fa-ticket-simple::before { + content: "\f3ff"; } + +.fa-ticket-alt::before { + content: "\f3ff"; } + +.fa-timeline::before { + content: "\e29c"; } + +.fa-toggle-off::before { + content: "\f204"; } + +.fa-toggle-on::before { + content: "\f205"; } + +.fa-toilet::before { + content: "\f7d8"; } + +.fa-toilet-paper::before { + content: "\f71e"; } + +.fa-toilet-paper-slash::before { + content: "\e072"; } + +.fa-toilet-portable::before { + content: "\e583"; } + +.fa-toilets-portable::before { + content: "\e584"; } + +.fa-toolbox::before { + content: "\f552"; } + +.fa-tooth::before { + content: "\f5c9"; } + +.fa-torii-gate::before { + content: "\f6a1"; } + +.fa-tornado::before { + content: "\f76f"; } + +.fa-tower-broadcast::before { + content: "\f519"; } + +.fa-broadcast-tower::before { + content: "\f519"; } + +.fa-tower-cell::before { + content: "\e585"; } + +.fa-tower-observation::before { + content: "\e586"; } + +.fa-tractor::before { + content: "\f722"; } + +.fa-trademark::before { + content: "\f25c"; } + +.fa-traffic-light::before { + content: "\f637"; } + +.fa-trailer::before { + content: "\e041"; } + +.fa-train::before { + content: "\f238"; } + +.fa-train-subway::before { + content: "\f239"; } + +.fa-subway::before { + content: "\f239"; } + +.fa-train-tram::before { + content: "\f7da"; } + +.fa-tram::before { + content: "\f7da"; } + +.fa-transgender::before { + content: "\f225"; } + +.fa-transgender-alt::before { + content: "\f225"; } + +.fa-trash::before { + content: "\f1f8"; } + +.fa-trash-arrow-up::before { + content: "\f829"; } + +.fa-trash-restore::before { + content: "\f829"; } + +.fa-trash-can::before { + content: "\f2ed"; } + +.fa-trash-alt::before { + content: "\f2ed"; } + +.fa-trash-can-arrow-up::before { + content: "\f82a"; } + +.fa-trash-restore-alt::before { + content: "\f82a"; } + +.fa-tree::before { + content: "\f1bb"; } + +.fa-tree-city::before { + content: "\e587"; } + +.fa-triangle-exclamation::before { + content: "\f071"; } + +.fa-exclamation-triangle::before { + content: "\f071"; } + +.fa-warning::before { + content: "\f071"; } + +.fa-trophy::before { + content: "\f091"; } + +.fa-trowel::before { + content: "\e589"; } + +.fa-trowel-bricks::before { + content: "\e58a"; } + +.fa-truck::before { + content: "\f0d1"; } + +.fa-truck-arrow-right::before { + content: "\e58b"; } + +.fa-truck-droplet::before { + content: "\e58c"; } + +.fa-truck-fast::before { + content: "\f48b"; } + +.fa-shipping-fast::before { + content: "\f48b"; } + +.fa-truck-field::before { + content: "\e58d"; } + +.fa-truck-field-un::before { + content: "\e58e"; } + +.fa-truck-front::before { + content: "\e2b7"; } + +.fa-truck-medical::before { + content: "\f0f9"; } + +.fa-ambulance::before { + content: "\f0f9"; } + +.fa-truck-monster::before { + content: "\f63b"; } + +.fa-truck-moving::before { + content: "\f4df"; } + +.fa-truck-pickup::before { + content: "\f63c"; } + +.fa-truck-plane::before { + content: "\e58f"; } + +.fa-truck-ramp-box::before { + content: "\f4de"; } + +.fa-truck-loading::before { + content: "\f4de"; } + +.fa-tty::before { + content: "\f1e4"; } + +.fa-teletype::before { + content: "\f1e4"; } + +.fa-turkish-lira-sign::before { + content: "\e2bb"; } + +.fa-try::before { + content: "\e2bb"; } + +.fa-turkish-lira::before { + content: "\e2bb"; } + +.fa-turn-down::before { + content: "\f3be"; } + +.fa-level-down-alt::before { + content: "\f3be"; } + +.fa-turn-up::before { + content: "\f3bf"; } + +.fa-level-up-alt::before { + content: "\f3bf"; } + +.fa-tv::before { + content: "\f26c"; } + +.fa-television::before { + content: "\f26c"; } + +.fa-tv-alt::before { + content: "\f26c"; } + +.fa-u::before { + content: "\55"; } + +.fa-umbrella::before { + content: "\f0e9"; } + +.fa-umbrella-beach::before { + content: "\f5ca"; } + +.fa-underline::before { + content: "\f0cd"; } + +.fa-universal-access::before { + content: "\f29a"; } + +.fa-unlock::before { + content: "\f09c"; } + +.fa-unlock-keyhole::before { + content: "\f13e"; } + +.fa-unlock-alt::before { + content: "\f13e"; } + +.fa-up-down::before { + content: "\f338"; } + +.fa-arrows-alt-v::before { + content: "\f338"; } + +.fa-up-down-left-right::before { + content: "\f0b2"; } + +.fa-arrows-alt::before { + content: "\f0b2"; } + +.fa-up-long::before { + content: "\f30c"; } + +.fa-long-arrow-alt-up::before { + content: "\f30c"; } + +.fa-up-right-and-down-left-from-center::before { + content: "\f424"; } + +.fa-expand-alt::before { + content: "\f424"; } + +.fa-up-right-from-square::before { + content: "\f35d"; } + +.fa-external-link-alt::before { + content: "\f35d"; } + +.fa-upload::before { + content: "\f093"; } + +.fa-user::before { + content: "\f007"; } + +.fa-user-astronaut::before { + content: "\f4fb"; } + +.fa-user-check::before { + content: "\f4fc"; } + +.fa-user-clock::before { + content: "\f4fd"; } + +.fa-user-doctor::before { + content: "\f0f0"; } + +.fa-user-md::before { + content: "\f0f0"; } + +.fa-user-gear::before { + content: "\f4fe"; } + +.fa-user-cog::before { + content: "\f4fe"; } + +.fa-user-graduate::before { + content: "\f501"; } + +.fa-user-group::before { + content: "\f500"; } + +.fa-user-friends::before { + content: "\f500"; } + +.fa-user-injured::before { + content: "\f728"; } + +.fa-user-large::before { + content: "\f406"; } + +.fa-user-alt::before { + content: "\f406"; } + +.fa-user-large-slash::before { + content: "\f4fa"; } + +.fa-user-alt-slash::before { + content: "\f4fa"; } + +.fa-user-lock::before { + content: "\f502"; } + +.fa-user-minus::before { + content: "\f503"; } + +.fa-user-ninja::before { + content: "\f504"; } + +.fa-user-nurse::before { + content: "\f82f"; } + +.fa-user-pen::before { + content: "\f4ff"; } + +.fa-user-edit::before { + content: "\f4ff"; } + +.fa-user-plus::before { + content: "\f234"; } + +.fa-user-secret::before { + content: "\f21b"; } + +.fa-user-shield::before { + content: "\f505"; } + +.fa-user-slash::before { + content: "\f506"; } + +.fa-user-tag::before { + content: "\f507"; } + +.fa-user-tie::before { + content: "\f508"; } + +.fa-user-xmark::before { + content: "\f235"; } + +.fa-user-times::before { + content: "\f235"; } + +.fa-users::before { + content: "\f0c0"; } + +.fa-users-between-lines::before { + content: "\e591"; } + +.fa-users-gear::before { + content: "\f509"; } + +.fa-users-cog::before { + content: "\f509"; } + +.fa-users-line::before { + content: "\e592"; } + +.fa-users-rays::before { + content: "\e593"; } + +.fa-users-rectangle::before { + content: "\e594"; } + +.fa-users-slash::before { + content: "\e073"; } + +.fa-users-viewfinder::before { + content: "\e595"; } + +.fa-utensils::before { + content: "\f2e7"; } + +.fa-cutlery::before { + content: "\f2e7"; } + +.fa-v::before { + content: "\56"; } + +.fa-van-shuttle::before { + content: "\f5b6"; } + +.fa-shuttle-van::before { + content: "\f5b6"; } + +.fa-vault::before { + content: "\e2c5"; } + +.fa-vector-square::before { + content: "\f5cb"; } + +.fa-venus::before { + content: "\f221"; } + +.fa-venus-double::before { + content: "\f226"; } + +.fa-venus-mars::before { + content: "\f228"; } + +.fa-vest::before { + content: "\e085"; } + +.fa-vest-patches::before { + content: "\e086"; } + +.fa-vial::before { + content: "\f492"; } + +.fa-vial-circle-check::before { + content: "\e596"; } + +.fa-vial-virus::before { + content: "\e597"; } + +.fa-vials::before { + content: "\f493"; } + +.fa-video::before { + content: "\f03d"; } + +.fa-video-camera::before { + content: "\f03d"; } + +.fa-video-slash::before { + content: "\f4e2"; } + +.fa-vihara::before { + content: "\f6a7"; } + +.fa-virus::before { + content: "\e074"; } + +.fa-virus-covid::before { + content: "\e4a8"; } + +.fa-virus-covid-slash::before { + content: "\e4a9"; } + +.fa-virus-slash::before { + content: "\e075"; } + +.fa-viruses::before { + content: "\e076"; } + +.fa-voicemail::before { + content: "\f897"; } + +.fa-volcano::before { + content: "\f770"; } + +.fa-volleyball::before { + content: "\f45f"; } + +.fa-volleyball-ball::before { + content: "\f45f"; } + +.fa-volume-high::before { + content: "\f028"; } + +.fa-volume-up::before { + content: "\f028"; } + +.fa-volume-low::before { + content: "\f027"; } + +.fa-volume-down::before { + content: "\f027"; } + +.fa-volume-off::before { + content: "\f026"; } + +.fa-volume-xmark::before { + content: "\f6a9"; } + +.fa-volume-mute::before { + content: "\f6a9"; } + +.fa-volume-times::before { + content: "\f6a9"; } + +.fa-vr-cardboard::before { + content: "\f729"; } + +.fa-w::before { + content: "\57"; } + +.fa-walkie-talkie::before { + content: "\f8ef"; } + +.fa-wallet::before { + content: "\f555"; } + +.fa-wand-magic::before { + content: "\f0d0"; } + +.fa-magic::before { + content: "\f0d0"; } + +.fa-wand-magic-sparkles::before { + content: "\e2ca"; } + +.fa-magic-wand-sparkles::before { + content: "\e2ca"; } + +.fa-wand-sparkles::before { + content: "\f72b"; } + +.fa-warehouse::before { + content: "\f494"; } + +.fa-water::before { + content: "\f773"; } + +.fa-water-ladder::before { + content: "\f5c5"; } + +.fa-ladder-water::before { + content: "\f5c5"; } + +.fa-swimming-pool::before { + content: "\f5c5"; } + +.fa-wave-square::before { + content: "\f83e"; } + +.fa-weight-hanging::before { + content: "\f5cd"; } + +.fa-weight-scale::before { + content: "\f496"; } + +.fa-weight::before { + content: "\f496"; } + +.fa-wheat-awn::before { + content: "\e2cd"; } + +.fa-wheat-alt::before { + content: "\e2cd"; } + +.fa-wheat-awn-circle-exclamation::before { + content: "\e598"; } + +.fa-wheelchair::before { + content: "\f193"; } + +.fa-wheelchair-move::before { + content: "\e2ce"; } + +.fa-wheelchair-alt::before { + content: "\e2ce"; } + +.fa-whiskey-glass::before { + content: "\f7a0"; } + +.fa-glass-whiskey::before { + content: "\f7a0"; } + +.fa-wifi::before { + content: "\f1eb"; } + +.fa-wifi-3::before { + content: "\f1eb"; } + +.fa-wifi-strong::before { + content: "\f1eb"; } + +.fa-wind::before { + content: "\f72e"; } + +.fa-window-maximize::before { + content: "\f2d0"; } + +.fa-window-minimize::before { + content: "\f2d1"; } + +.fa-window-restore::before { + content: "\f2d2"; } + +.fa-wine-bottle::before { + content: "\f72f"; } + +.fa-wine-glass::before { + content: "\f4e3"; } + +.fa-wine-glass-empty::before { + content: "\f5ce"; } + +.fa-wine-glass-alt::before { + content: "\f5ce"; } + +.fa-won-sign::before { + content: "\f159"; } + +.fa-krw::before { + content: "\f159"; } + +.fa-won::before { + content: "\f159"; } + +.fa-worm::before { + content: "\e599"; } + +.fa-wrench::before { + content: "\f0ad"; } + +.fa-x::before { + content: "\58"; } + +.fa-x-ray::before { + content: "\f497"; } + +.fa-xmark::before { + content: "\f00d"; } + +.fa-close::before { + content: "\f00d"; } + +.fa-multiply::before { + content: "\f00d"; } + +.fa-remove::before { + content: "\f00d"; } + +.fa-times::before { + content: "\f00d"; } + +.fa-xmarks-lines::before { + content: "\e59a"; } + +.fa-y::before { + content: "\59"; } + +.fa-yen-sign::before { + content: "\f157"; } + +.fa-cny::before { + content: "\f157"; } + +.fa-jpy::before { + content: "\f157"; } + +.fa-rmb::before { + content: "\f157"; } + +.fa-yen::before { + content: "\f157"; } + +.fa-yin-yang::before { + content: "\f6ad"; } + +.fa-z::before { + content: "\5a"; } + +.sr-only, +.fa-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } +:root, :host { + --fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands"; } + +@font-face { + font-family: 'Font Awesome 6 Brands'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +.fab, +.fa-brands { + font-family: 'Font Awesome 6 Brands'; + font-weight: 400; } + +.fa-42-group:before { + content: "\e080"; } + +.fa-innosoft:before { + content: "\e080"; } + +.fa-500px:before { + content: "\f26e"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-airbnb:before { + content: "\f834"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-alipay:before { + content: "\f642"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-artstation:before { + content: "\f77a"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-atlassian:before { + content: "\f77b"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-battle-net:before { + content: "\f835"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-bilibili:before { + content: "\e3d9"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-bootstrap:before { + content: "\f836"; } + +.fa-bots:before { + content: "\e340"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-buffer:before { + content: "\f837"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-buy-n-large:before { + content: "\f8a6"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-canadian-maple-leaf:before { + content: "\f785"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-centos:before { + content: "\f789"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-chromecast:before { + content: "\f838"; } + +.fa-cloudflare:before { + content: "\e07d"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-cmplid:before { + content: "\e360"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-confluence:before { + content: "\f78d"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-cotton-bureau:before { + content: "\f89e"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-creative-commons-by:before { + content: "\f4e7"; } + +.fa-creative-commons-nc:before { + content: "\f4e8"; } + +.fa-creative-commons-nc-eu:before { + content: "\f4e9"; } + +.fa-creative-commons-nc-jp:before { + content: "\f4ea"; } + +.fa-creative-commons-nd:before { + content: "\f4eb"; } + +.fa-creative-commons-pd:before { + content: "\f4ec"; } + +.fa-creative-commons-pd-alt:before { + content: "\f4ed"; } + +.fa-creative-commons-remix:before { + content: "\f4ee"; } + +.fa-creative-commons-sa:before { + content: "\f4ef"; } + +.fa-creative-commons-sampling:before { + content: "\f4f0"; } + +.fa-creative-commons-sampling-plus:before { + content: "\f4f1"; } + +.fa-creative-commons-share:before { + content: "\f4f2"; } + +.fa-creative-commons-zero:before { + content: "\f4f3"; } + +.fa-critical-role:before { + content: "\f6c9"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-d-and-d-beyond:before { + content: "\f6ca"; } + +.fa-dailymotion:before { + content: "\e052"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-deezer:before { + content: "\e077"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-dev:before { + content: "\f6cc"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-dhl:before { + content: "\f790"; } + +.fa-diaspora:before { + content: "\f791"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-ebay:before { + content: "\f4f4"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-edge-legacy:before { + content: "\e078"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-ello:before { + content: "\f5f1"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-evernote:before { + content: "\f839"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-fantasy-flight-games:before { + content: "\f6dc"; } + +.fa-fedex:before { + content: "\f797"; } + +.fa-fedora:before { + content: "\f798"; } + +.fa-figma:before { + content: "\f799"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-firefox-browser:before { + content: "\e007"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-first-order-alt:before { + content: "\f50a"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-flipboard:before { + content: "\f44d"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-flag:before { + content: "\f2b4"; } + +.fa-font-awesome-logo-full:before { + content: "\f2b4"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-fulcrum:before { + content: "\f50b"; } + +.fa-galactic-republic:before { + content: "\f50c"; } + +.fa-galactic-senate:before { + content: "\f50d"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-git-alt:before { + content: "\f841"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-golang:before { + content: "\e40f"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-google-pay:before { + content: "\e079"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-guilded:before { + content: "\e07e"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-hackerrank:before { + content: "\f5f7"; } + +.fa-hashnode:before { + content: "\e499"; } + +.fa-hips:before { + content: "\f452"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-hive:before { + content: "\e07f"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-hornbill:before { + content: "\f592"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-ideal:before { + content: "\e013"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-instagram-square:before { + content: "\e055"; } + +.fa-instalod:before { + content: "\e081"; } + +.fa-intercom:before { + content: "\f7af"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-invision:before { + content: "\f7b0"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-itch-io:before { + content: "\f83a"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-java:before { + content: "\f4e4"; } + +.fa-jedi-order:before { + content: "\f50e"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-jira:before { + content: "\f7b1"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-kaggle:before { + content: "\f5fa"; } + +.fa-keybase:before { + content: "\f4f5"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-mailchimp:before { + content: "\f59e"; } + +.fa-mandalorian:before { + content: "\f50f"; } + +.fa-markdown:before { + content: "\f60f"; } + +.fa-mastodon:before { + content: "\f4f6"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-mdb:before { + content: "\f8ca"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f23a"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-megaport:before { + content: "\f5a3"; } + +.fa-mendeley:before { + content: "\f7b3"; } + +.fa-microblog:before { + content: "\e01a"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-mixer:before { + content: "\e056"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-neos:before { + content: "\f612"; } + +.fa-nfc-directional:before { + content: "\e530"; } + +.fa-nfc-symbol:before { + content: "\e531"; } + +.fa-nimblr:before { + content: "\f5a8"; } + +.fa-node:before { + content: "\f419"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-octopus-deploy:before { + content: "\e082"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-old-republic:before { + content: "\f510"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-orcid:before { + content: "\f8d2"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-padlet:before { + content: "\e4a0"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-perbyte:before { + content: "\e083"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-phoenix-squadron:before { + content: "\f511"; } + +.fa-php:before { + content: "\f457"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-pied-piper-hat:before { + content: "\f4e5"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-pied-piper-square:before { + content: "\e01e"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-pix:before { + content: "\e43a"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-quinscape:before { + content: "\f459"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-r-project:before { + content: "\f4f7"; } + +.fa-raspberry-pi:before { + content: "\f7bb"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-reacteurope:before { + content: "\f75d"; } + +.fa-readme:before { + content: "\f4d5"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-redhat:before { + content: "\f7bc"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-researchgate:before { + content: "\f4f8"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-rev:before { + content: "\f5b2"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-rust:before { + content: "\e07a"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-salesforce:before { + content: "\f83b"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-screenpal:before { + content: "\e570"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-shopify:before { + content: "\e057"; } + +.fa-shopware:before { + content: "\f5b5"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-sith:before { + content: "\f512"; } + +.fa-sitrox:before { + content: "\e44a"; } + +.fa-sketch:before { + content: "\f7c6"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f198"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ab"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-sourcetree:before { + content: "\f7d3"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-speaker-deck:before { + content: "\f83c"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-square-font-awesome:before { + content: "\f425"; } + +.fa-square-font-awesome-stroke:before { + content: "\f35c"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-squarespace:before { + content: "\f5be"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-stackpath:before { + content: "\f842"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-suse:before { + content: "\f7d6"; } + +.fa-swift:before { + content: "\f8e1"; } + +.fa-symfony:before { + content: "\f83d"; } + +.fa-teamspeak:before { + content: "\f4f9"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f2c6"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-the-red-yeti:before { + content: "\f69d"; } + +.fa-themeco:before { + content: "\f5c6"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-think-peaks:before { + content: "\f731"; } + +.fa-tiktok:before { + content: "\e07b"; } + +.fa-trade-federation:before { + content: "\f513"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-ubuntu:before { + content: "\f7df"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-umbraco:before { + content: "\f8e8"; } + +.fa-uncharted:before { + content: "\e084"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-unity:before { + content: "\e049"; } + +.fa-unsplash:before { + content: "\e07c"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-ups:before { + content: "\f7e0"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-usps:before { + content: "\f7e1"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-watchman-monitoring:before { + content: "\e087"; } + +.fa-waze:before { + content: "\f83f"; } + +.fa-weebly:before { + content: "\f5cc"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-wirsindhandwerk:before { + content: "\e2d0"; } + +.fa-wsh:before { + content: "\e2d0"; } + +.fa-wix:before { + content: "\f5cf"; } + +.fa-wizards-of-the-coast:before { + content: "\f730"; } + +.fa-wodu:before { + content: "\e088"; } + +.fa-wolf-pack-battalion:before { + content: "\f514"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-wpressr:before { + content: "\f3e4"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-yammer:before { + content: "\f840"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-yarn:before { + content: "\f7e3"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.fa-zhihu:before { + content: "\f63f"; } +:root, :host { + --fa-font-regular: normal 400 1em/1 "Font Awesome 6 Free"; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } + +.far, +.fa-regular { + font-family: 'Font Awesome 6 Free'; + font-weight: 400; } +:root, :host { + --fa-font-solid: normal 900 1em/1 "Font Awesome 6 Free"; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 900; + font-display: block; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +.fas, +.fa-solid { + font-family: 'Font Awesome 6 Free'; + font-weight: 900; } +@font-face { + font-family: "Font Awesome 5 Brands"; + font-display: block; + font-weight: 400; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +@font-face { + font-family: "Font Awesome 5 Free"; + font-display: block; + font-weight: 900; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +@font-face { + font-family: "Font Awesome 5 Free"; + font-display: block; + font-weight: 400; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); + unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; } + +@font-face { + font-family: "FontAwesome"; + font-display: block; + src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype"); + unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F250,U+F252,U+F27A; } diff --git a/website/slides/_extensions/fontawesome/assets/css/latex-fontsize.css b/website/slides/_extensions/fontawesome/assets/css/latex-fontsize.css new file mode 100644 index 0000000..45545ec --- /dev/null +++ b/website/slides/_extensions/fontawesome/assets/css/latex-fontsize.css @@ -0,0 +1,30 @@ +.fa-tiny { + font-size: 0.5em; +} +.fa-scriptsize { + font-size: 0.7em; +} +.fa-footnotesize { + font-size: 0.8em; +} +.fa-small { + font-size: 0.9em; +} +.fa-normalsize { + font-size: 1em; +} +.fa-large { + font-size: 1.2em; +} +.fa-Large { + font-size: 1.5em; +} +.fa-LARGE { + font-size: 1.75em; +} +.fa-huge { + font-size: 2em; +} +.fa-Huge { + font-size: 2.5em; +} diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-brands-400.ttf b/website/slides/_extensions/fontawesome/assets/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000..430a02e Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-brands-400.ttf differ diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-brands-400.woff2 b/website/slides/_extensions/fontawesome/assets/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..4d904aa Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-brands-400.woff2 differ diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-regular-400.ttf b/website/slides/_extensions/fontawesome/assets/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..23e3feb Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-regular-400.ttf differ diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-regular-400.woff2 b/website/slides/_extensions/fontawesome/assets/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..80e3b12 Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-regular-400.woff2 differ diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-solid-900.ttf b/website/slides/_extensions/fontawesome/assets/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000..da90824 Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-solid-900.ttf differ diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-solid-900.woff2 b/website/slides/_extensions/fontawesome/assets/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..360ba11 Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-solid-900.woff2 differ diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-v4compatibility.ttf b/website/slides/_extensions/fontawesome/assets/webfonts/fa-v4compatibility.ttf new file mode 100644 index 0000000..e9545ed Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-v4compatibility.ttf differ diff --git a/website/slides/_extensions/fontawesome/assets/webfonts/fa-v4compatibility.woff2 b/website/slides/_extensions/fontawesome/assets/webfonts/fa-v4compatibility.woff2 new file mode 100644 index 0000000..db5b0b9 Binary files /dev/null and b/website/slides/_extensions/fontawesome/assets/webfonts/fa-v4compatibility.woff2 differ diff --git a/website/slides/_extensions/fontawesome/fontawesome.lua b/website/slides/_extensions/fontawesome/fontawesome.lua new file mode 100644 index 0000000..3939135 --- /dev/null +++ b/website/slides/_extensions/fontawesome/fontawesome.lua @@ -0,0 +1,77 @@ +local function ensureLatexDeps() + quarto.doc.useLatexPackage("fontawesome5") +end + +local function ensureHtmlDeps() + quarto.doc.addHtmlDependency({ + name = 'fontawesome6', + version = '0.1.0', + stylesheets = {'assets/css/all.css', 'assets/css/latex-fontsize.css'} + }) +end + +local function isEmpty(s) + return s == nil or s == '' +end + +local function isValidSize(size) + local validSizes = { + "tiny", + "scriptsize", + "footnotesize", + "small", + "normalsize", + "large", + "Large", + "LARGE", + "huge", + "Huge" + } + for _, v in ipairs(validSizes) do + if v == size then + return size + end + end + return "" +end + +return { + ["fa"] = function(args, kwargs) + + local group = "solid" + local icon = pandoc.utils.stringify(args[1]) + if #args > 1 then + group = icon + icon = pandoc.utils.stringify(args[2]) + end + + local title = pandoc.utils.stringify(kwargs["title"]) + if not isEmpty(title) then + title = " title=\"" .. title .. "\"" + end + + local size = pandoc.utils.stringify(kwargs["size"]) + + -- detect html (excluding epub which won't handle fa) + if quarto.doc.isFormat("html:js") then + ensureHtmlDeps() + if not isEmpty(size) then + size = " fa-" .. size + end + return pandoc.RawInline( + 'html', + "" + ) + -- detect pdf / beamer / latex / etc + elseif quarto.doc.isFormat("pdf") then + ensureLatexDeps() + if isEmpty(isValidSize(size)) then + return pandoc.RawInline('tex', "\\faIcon{" .. icon .. "}") + else + return pandoc.RawInline('tex', "{\\" .. size .. "\\faIcon{" .. icon .. "}}") + end + else + return pandoc.Null() + end + end +} diff --git a/website/slides/_extensions/insee-clair/_extension.yml b/website/slides/_extensions/insee-clair/_extension.yml new file mode 100644 index 0000000..8fc6336 --- /dev/null +++ b/website/slides/_extensions/insee-clair/_extension.yml @@ -0,0 +1,82 @@ +title: insee-clair +author: Romain Puyau +contributes: + formats: + revealjs: + header-includes: | + + + include-after-body: ["modulesCompl/footer.html"] + logo: img/licences/cc/png/by-sa.png #uncomment here to add license logo for instance or comment to hide it + + embed-resources: true + df-print: paged + slide-number: c/t + progress: true + controls-layout: bottom-right + controls-tutorial: true + controls-back-arrows: faded + background-transition: fade + navigation-mode: vertical # linear/grid or vertical + history: true + transition-speed: slow + number-sections: true + number-depth: 2 + + link-external-newwindow: true + # link-external-icon: true + # footnotes-hover: true + # reference-location: document #block / section / margin / document + code-fold: true + code-summary: "Code" + code-copy: true + code-annotations: hover #below / hover / select + highlight-style: atom-one + + menu: { + side: 'left', + width: 'wide', + markers: true, + transitions: true, + custom: [ + { + title: 'Plugins', + icon: '', + src: 'modulesCompl/customMenu.html' + } + ] + } + + width: 1920 + height: 1080 + + theme: css/Insee_Commun.scss + css: [css/insee_clair.css, css/default.css, css/stylePerso.css] + + title-slide-attributes: + data-background-image: _extensions/insee-clair/ressources/revealJs/1_Template-INSEE-Clair_premiere-page.svg + data-background-size: contain + data-background-opacity: "1" + + toc: false + toc-title: Sommaire + toc-depth: 1 + template-partials: + - "_extensions/insee-clair/toc-slide.html" + - "_extensions/insee-clair/toc-style.html" + filters: + - "toc-filter.lua" + + + odt: + reference-doc: ressources/libreOffice-template.odt + # pandoc-lua-filters: ["custom_writer.lua"] + toc: true + toc-title: "Sommaire" + toc-depth: 3 + styles: true + df-print: kable + + # pptx: + # reference-doc: ressources/powerpoint-template.pptx + # toc: true \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/custom_writer.lua b/website/slides/_extensions/insee-clair/custom_writer.lua new file mode 100644 index 0000000..b7a4e5e --- /dev/null +++ b/website/slides/_extensions/insee-clair/custom_writer.lua @@ -0,0 +1,73 @@ +-- Fonction pour centrer les images +function Image(img) + -- Retourne l'image avec un style de paragraphe centré + return pandoc.Para({ + pandoc.Image(img.attr, img.caption) + }) .. + pandoc.RawBlock('openxml', '') +end + +-- Fonction pour personnaliser les puces des listes +function BulletList(list) + -- Retourne la liste avec des styles de puces personnalisés + return pandoc.BulletList(pandoc.utils.map( + function(item) + if #item > 0 and item[1].t == "Plain" then + -- Appliquer un style de paragraphe aux éléments de la liste + return pandoc.Para(pandoc.Strong({pandoc.Str("• ")})) .. item + else + return item + end + end, + list.content + )) +end + +-- Fonction pour personnaliser les listes numérotées +function OrderedList(list) + -- Retourne la liste numérotée avec des styles personnalisés + return pandoc.OrderedList(list.attr)( + pandoc.utils.map( + function(item) + if #item > 0 and item[1].t == "Plain" then + -- Appliquer un style de paragraphe aux éléments de la liste + return pandoc.Para(pandoc.Strong({pandoc.Str("1. ")})) .. item + else + return item + end + end, + list.content + ) + ) +end + +-- Fonction pour appliquer un style de paragraphe centré aux images +function Para(el) + local has_image = false + for _, elem in ipairs(el.content) do + if elem.t == "Image" then + has_image = true + break + end + end + if has_image then + -- Ajouter un style de paragraphe centré + el.attributes = el.attributes or {} + el.attributes["custom-style"] = "ImageCentrée" + end + return el +end + +-- Fonction pour appliquer des styles de puces aux listes +function Doc(doc) + -- Ajouter des métadonnées pour les styles de liste + doc.meta["list-styles"] = pandoc.MetaMap({ + ["level-1"] = pandoc.MetaInlines({pandoc.Str("-")}), + ["level-2"] = pandoc.MetaInlines({pandoc.Str("•")}), + ["level-3"] = pandoc.MetaInlines({pandoc.Str("-")}) + ["level-4"] = pandoc.MetaInlines({pandoc.Str("•")}) + ["level-5"] = pandoc.MetaInlines({pandoc.Str("•")}) + ["level-6"] = pandoc.MetaInlines({pandoc.Str("•")}) + }) + return doc +end diff --git a/website/slides/_extensions/insee-clair/ressources/divers/Logo_Insee_SignatureAdroite.jpg b/website/slides/_extensions/insee-clair/ressources/divers/Logo_Insee_SignatureAdroite.jpg new file mode 100644 index 0000000..f9b896e Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/divers/Logo_Insee_SignatureAdroite.jpg differ diff --git a/website/slides/_extensions/insee-clair/ressources/divers/background_jobs_rstudio.png b/website/slides/_extensions/insee-clair/ressources/divers/background_jobs_rstudio.png new file mode 100644 index 0000000..8bfdcc4 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/divers/background_jobs_rstudio.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/divers/renders_quarto.png b/website/slides/_extensions/insee-clair/ressources/divers/renders_quarto.png new file mode 100644 index 0000000..73ae9c8 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/divers/renders_quarto.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/libreOffice-template.odt b/website/slides/_extensions/insee-clair/ressources/libreOffice-template.odt new file mode 100644 index 0000000..7036ba8 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/libreOffice-template.odt differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/Logo_Insee_TailleMINI.jpg b/website/slides/_extensions/insee-clair/ressources/logos/png/Logo_Insee_TailleMINI.jpg new file mode 100644 index 0000000..e1e9388 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/Logo_Insee_TailleMINI.jpg differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/Logo_StatistiquePublique.jpg b/website/slides/_extensions/insee-clair/ressources/logos/png/Logo_StatistiquePublique.jpg new file mode 100644 index 0000000..019f3b7 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/Logo_StatistiquePublique.jpg differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/flux_rss.png b/website/slides/_extensions/insee-clair/ressources/logos/png/flux_rss.png new file mode 100644 index 0000000..2897e49 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/flux_rss.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/logoLinkedIn_rond.png b/website/slides/_extensions/insee-clair/ressources/logos/png/logoLinkedIn_rond.png new file mode 100644 index 0000000..0366699 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/logoLinkedIn_rond.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/logoYoutube_rond.png b/website/slides/_extensions/insee-clair/ressources/logos/png/logoYoutube_rond.png new file mode 100644 index 0000000..58de217 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/logoYoutube_rond.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/logo_X.png b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_X.png new file mode 100644 index 0000000..d99bb93 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_X.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/logo_blog.png b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_blog.png new file mode 100644 index 0000000..eb588fd Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_blog.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/logo_bluesky.png b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_bluesky.png new file mode 100644 index 0000000..e9b0bba Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_bluesky.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/png/logo_instagram.jpg b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_instagram.jpg new file mode 100644 index 0000000..9c65e90 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/logos/png/logo_instagram.jpg differ diff --git a/website/slides/_extensions/insee-clair/ressources/logos/svg/1_inseeFR_Noir.svg b/website/slides/_extensions/insee-clair/ressources/logos/svg/1_inseeFR_Noir.svg new file mode 100644 index 0000000..a428e0b --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/logos/svg/1_inseeFR_Noir.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/logos/svg/inseeFR.svg b/website/slides/_extensions/insee-clair/ressources/logos/svg/inseeFR.svg new file mode 100644 index 0000000..117b89a --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/logos/svg/inseeFR.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/open_document/exemple_modification_writer.png b/website/slides/_extensions/insee-clair/ressources/open_document/exemple_modification_writer.png new file mode 100644 index 0000000..3c475ae Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/open_document/exemple_modification_writer.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/open_document/volet_lateral_style_et_formatage.png b/website/slides/_extensions/insee-clair/ressources/open_document/volet_lateral_style_et_formatage.png new file mode 100644 index 0000000..a812eac Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/open_document/volet_lateral_style_et_formatage.png differ diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/1_Template-INSEE-Clair_premiere-page.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/1_Template-INSEE-Clair_premiere-page.svg new file mode 100644 index 0000000..2087849 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/1_Template-INSEE-Clair_premiere-page.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire1.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire1.svg new file mode 100644 index 0000000..8d4f136 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire2.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire2.svg new file mode 100644 index 0000000..a67fbf8 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire3.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire3.svg new file mode 100644 index 0000000..47f6406 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire4.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire4.svg new file mode 100644 index 0000000..61cc404 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle1.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle1.svg new file mode 100644 index 0000000..a2c2a06 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle2.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle2.svg new file mode 100644 index 0000000..6674664 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle3.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle3.svg new file mode 100644 index 0000000..14edcf9 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle4.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle4.svg new file mode 100644 index 0000000..66fc71b --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle5.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle5.svg new file mode 100644 index 0000000..6a78dd7 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche.svg new file mode 100644 index 0000000..d67ddcc --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/6_Template-INSEE-Clair_Diapo-finale.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/6_Template-INSEE-Clair_Diapo-finale.svg new file mode 100644 index 0000000..b8ded8d --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/6_Template-INSEE-Clair_Diapo-finale.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-1.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-1.svg new file mode 100644 index 0000000..5018179 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-1.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-10.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-10.svg new file mode 100644 index 0000000..c2deb4b --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-10.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-11.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-11.svg new file mode 100644 index 0000000..61d6463 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-11.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-12.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-12.svg new file mode 100644 index 0000000..0e21d21 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-12.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-2.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-2.svg new file mode 100644 index 0000000..83b1194 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-2.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-3.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-3.svg new file mode 100644 index 0000000..0bd09a6 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-3.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-4.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-4.svg new file mode 100644 index 0000000..d07a376 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-4.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-5.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-5.svg new file mode 100644 index 0000000..4e531ff --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-5.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-6.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-6.svg new file mode 100644 index 0000000..25dff84 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-6.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-7.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-7.svg new file mode 100644 index 0000000..a0dc4c2 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-7.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-8.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-8.svg new file mode 100644 index 0000000..ab0dac5 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-8.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-9.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-9.svg new file mode 100644 index 0000000..7f6e0f8 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-9.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/revealJs/insee_embleme_couleurs_RVB.svg b/website/slides/_extensions/insee-clair/ressources/revealJs/insee_embleme_couleurs_RVB.svg new file mode 100644 index 0000000..af6a272 --- /dev/null +++ b/website/slides/_extensions/insee-clair/ressources/revealJs/insee_embleme_couleurs_RVB.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/website/slides/_extensions/insee-clair/ressources/template_writer_Final.odt b/website/slides/_extensions/insee-clair/ressources/template_writer_Final.odt new file mode 100644 index 0000000..9f0ee94 Binary files /dev/null and b/website/slides/_extensions/insee-clair/ressources/template_writer_Final.odt differ diff --git a/website/slides/_extensions/insee-clair/toc-filter.lua b/website/slides/_extensions/insee-clair/toc-filter.lua new file mode 100644 index 0000000..6031df8 --- /dev/null +++ b/website/slides/_extensions/insee-clair/toc-filter.lua @@ -0,0 +1,110 @@ + +function Pandoc(doc) + -- Vérifie si custom-toc est activé + local custom_toc_enabled = doc.meta and doc.meta["custom-toc"] == true + if not custom_toc_enabled then return doc end + + -- Récupère toc-title + local toc_title = "" -- Valeur par défaut + if doc.meta and doc.meta["toc-title"] then + toc_title = pandoc.utils.stringify(doc.meta["toc-title"]) + end + + -- Récupère toc-depth (par défaut: 1) + local toc_depth = 1 + if doc.meta and doc.meta["toc-depth"] then + toc_depth = tonumber(doc.meta["toc-depth"]) or 1 + end + + -- Fonction pour lire un fichier + local function read_file(file) + local f = io.open(file, "r") + if not f then error("Fichier introuvable: " .. file) end + local content = f:read("*a") + f:close() + return content + end + + -- Fonction pour formater les numéros avec des zéros devant + local function format_number(num) + return string.format("%02d", num) -- Ajoute un zéro si < 10 + end + + -- Génère la numérotation et les cartes + local sections = {} + local counters = {0} -- Compteur pour chaque niveau (1-based) + for i, block in ipairs(doc.blocks) do + if block.t == "Header" and block.level <= toc_depth then + -- Vérifie si le titre est marqué comme "unnumbered" + local is_unnumbered = false + for _, class in ipairs(block.classes) do + if class == "unnumbered" then + is_unnumbered = true + break + end + end + + -- Si le titre est "unnumbered", on l'ignore pour le sommaire + if is_unnumbered then + goto continue + end + + -- Met à jour les compteurs + while #counters < block.level do + table.insert(counters, 0) + end + counters[block.level] = counters[block.level] + 1 + for j = block.level + 1, #counters do + counters[j] = 0 + end + + -- Génère le numéro avec zéros devant (ex: "01.02.03") + local number_parts = {} + for k = 1, block.level do + table.insert(number_parts, format_number(counters[k])) + end + local section_number = table.concat(number_parts, ".") + table.insert(sections, { + id = block.identifier, + title = pandoc.utils.stringify(block.content), + level = block.level, + number = section_number, + level1_number = format_number(counters[1]) -- Numéro de niveau 1 + }) + end + ::continue:: + end + + -- Génère les cartes avec classe CSS pour le numéro de niveau 1 + local toc_cards = {} + for _, section in ipairs(sections) do + local card_content = string.format( + '

%s

%s

', + section.level1_number, section.number, section.title + ) + table.insert(toc_cards, string.format( + '', + section.level, section.id, card_content + )) + end + + -- Lit les templates + local html_template = read_file("_extensions/insee-clair/toc-slide.html") + local css_content = read_file("_extensions/insee-clair/toc-style.html") + + -- Remplace le titre du sommaire dans le template + html_template = html_template:gsub( + '

.-

', -- Cherche n'importe quel titre

...

+ '

' .. toc_title .. '

' -- Remplace par le titre dynamique + ) + + -- Insère les cartes dans le template + local final_html = html_template:gsub( + '
.-
', + '
' .. table.concat(toc_cards, "\n") .. '
' + ) + + -- Ajoute le tout au document + table.insert(doc.blocks, 1, pandoc.RawBlock('html', final_html .. css_content)) + return doc +end diff --git a/website/slides/_extensions/insee-clair/toc-slide.html b/website/slides/_extensions/insee-clair/toc-slide.html new file mode 100644 index 0000000..3130f7d --- /dev/null +++ b/website/slides/_extensions/insee-clair/toc-slide.html @@ -0,0 +1,7 @@ + +
+

+
+ +
+
diff --git a/website/slides/_extensions/insee-clair/toc-style.html b/website/slides/_extensions/insee-clair/toc-style.html new file mode 100644 index 0000000..7535eb8 --- /dev/null +++ b/website/slides/_extensions/insee-clair/toc-style.html @@ -0,0 +1,38 @@ + + diff --git a/website/slides/_extensions/multimodal/_extension.yml b/website/slides/_extensions/multimodal/_extension.yml new file mode 100644 index 0000000..58a8587 --- /dev/null +++ b/website/slides/_extensions/multimodal/_extension.yml @@ -0,0 +1,10 @@ +title: Multimodal +author: Martijn De Jongh (Martino) +version: 1.0.1 +quarto-required: ">=1.2.198" +contributes: + revealjs-plugins: + - + name: Multimodal + script: multimodal.js + stylesheet: multimodal.css \ No newline at end of file diff --git a/website/slides/_extensions/multimodal/multimodal.css b/website/slides/_extensions/multimodal/multimodal.css new file mode 100644 index 0000000..e2d8347 --- /dev/null +++ b/website/slides/_extensions/multimodal/multimodal.css @@ -0,0 +1,236 @@ + +/***************************************************************** + * + * Multimodal for Reveal.js + * Version 1.0.1 + * + * @author: Martijn De Jongh (Martino), martijn.de.jongh@gmail.com + * https://github.com/martinomagnifico + * + * @license + * MIT licensed + * + * Copyright (C) 2023 Martijn De Jongh (Martino) + * + ******************************************************************/ + + + +.multimodal .mm-close { + --close-stroke-color: white; + --close-stroke-width: 1.2; + --close-hover-ratio: 1.2; + --close-mainlength: 16px; + --close-hovarratio: 1.2; + padding: 0; + border-radius: 50%; + border: none; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + z-index: 2; + background: rgba(0, 0, 0, 0.28); + padding: 2px; + position: absolute; + right: 1em; + top: 1em; + width: 28px; + height: 28px; + transform-origin: top right; + transform: scale(var(--mm-inversescale, 1)); +} +.multimodal .mm-close:before { + content: ""; + position: absolute; + width: 28px; + height: 28px; + border-radius: 20px; + background: transparent; + transition: all 0.0675s linear; + pointer-events: none; + z-index: -1; +} +.multimodal .mm-close svg { + width: 24px; + height: 24px; +} +.multimodal .mm-close svg line { + transition: all 0.0675s linear; + stroke: var(--close-stroke-color); + stroke-linecap: round; + stroke-width: 1.25; +} +.multimodal .mm-close svg line, .multimodal .mm-close:active:hover svg line { + stroke-dasharray: 16 8; + stroke-dashoffset: -4; +} +.multimodal .mm-close.sm svg line, .multimodal .mm-close.sm:active:hover svg line { + stroke-dasharray: 12 12; + stroke-dashoffset: -6; +} +.multimodal .mm-close:hover:before, .multimodal .mm-close:focus:before { + width: 36px; + height: 36px; + background: rgba(0, 0, 0, 0.2); +} +.multimodal .mm-close:hover svg line { + stroke-dasharray: 20 4; + stroke-dashoffset: -2; +} +.multimodal .mm-close.sm:hover svg line { + stroke-dasharray: 16 8; + stroke-dashoffset: -4; +} +.multimodal .mm-close:focus { + outline: 3px solid #00C6CB; + outline-offset: 4px; +} +.multimodal .mm-close.sm:focus { + outline-offset: 0; +} +.multimodal .mm-close:focus:not(:focus-visible) { + outline: 0; + box-shadow: none; +} + +:root { + --mm-borderwidth: 1px; + --mm-bordercolor: white; + --mm-transspeed: 0.3s; + --mm-transtiming: ease-in-out; + --mm-overlaycolor: rgba(0,0,0,0.30); + --mm-shadow: 0 0.5em 0.75em 0.5em rgba(0, 0, 0, 0.25); + --mm-initialscale: 0.9; + --mm-minwidth: 100px; + --mm-minheight: 100px; + --mm-htmlpadding: 1em; + --mm-htmlbgcolor: var(--r-background-color); + --mm-iframepadding: 0; + --mm-iframebgcolor: var(--r-background-color); + --mm-mediapadding: none; + --mm-mediabgcolor: white; + --mm-outerradius: 0.5em; +} + +.mm-dialog.hidden, +section > .mm-dialog { + display: none; +} + +.multimodal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--mm-overlaycolor); + z-index: 9999; + opacity: 0; + transition: all var(--mm-transspeed) var(--mm-transtiming); +} +.multimodal, .multimodal * { + box-sizing: border-box; +} +.multimodal .mm-max { + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: var(--mm-maxwidth); + height: var(--mm-maxheight); + transition: all var(--mm-transspeed) var(--mm-transtiming); + inset: 50% auto auto 50%; + transform: translate(-50%, -50%) scale(calc(var(--slide-scale) * var(--mm-initialscale))); +} +.multimodal .mm-max .mm-dialog { + width: fit-content; + height: fit-content; + max-height: 100%; + max-width: 100%; + display: flex; + flex-direction: column; + position: relative; + justify-content: center; + align-items: center; + overflow: hidden; + background: var(--mm-modal-background, var(--mm-mediabgcolor)); + border-style: solid; + border-width: var(--mm-borderwidth); + border-color: var(--mm-bordercolor); + border-radius: var(--mm-outerradius); + box-shadow: var(--mm-shadow); + z-index: 0; +} +.multimodal .mm-max .mm-dialog.hidden { + display: none; +} +.multimodal .mm-max iframe { + opacity: 0; + transition: opacity 0.8s var(--mm-transtiming); + transition-delay: var(0.25s); +} +.multimodal .mm-max .mm-body { + z-index: 0; +} +.multimodal .mm-max .mm-scrollbody { + display: flex; + flex-direction: column; + overflow: hidden; +} +.multimodal[data-modal-type*=html] .mm-body { + min-width: var(--mm-minwidth); + min-height: var(--mm-minheight); + overflow-y: auto; +} +.multimodal[data-modal-type*=html] .mm-dialog { + background: var(--mm-modal-background, var(--mm-htmlbgcolor)); + padding: var(--mm-modal-padding, var(--mm-htmlpadding)); +} +.multimodal[data-modal-type=image] .mm-body { + max-width: var(--mm-maxwidth); + max-height: var(--mm-maxheight); + margin: 0; +} +.multimodal[data-modal-type=image] .mm-dialog { + background: var(--mm-modal-background, var(--mm-mediabgcolor)); + padding: var(--mm-modal-padding, var(--mm-mediapadding)); +} +.multimodal[data-modal-type=video] .mm-body { + max-width: var(--mm-maxwidth); + max-height: var(--mm-maxheight); + margin: 0; +} +.multimodal[data-modal-type=video] .mm-dialog { + background: var(--mm-modal-background, var(--mm-mediabgcolor)); + padding: var(--mm-modal-padding, var(--mm-mediapadding)); +} +.multimodal[data-modal-type*=iframe] .mm-body { + max-width: 100%; + max-height: 100%; + margin: 0; + width: calc(var(--mm-maxwidth)); + height: calc(var(--mm-maxheight)); + box-sizing: border-box; + overflow-y: auto; +} +.multimodal[data-modal-type*=iframe] .mm-dialog { + background: var(--mm-modal-background, var(--mm-iframebgcolor)); + padding: var(--mm-modal-padding, var(--mm-iframepadding)); +} +.multimodal.show { + opacity: 1; +} +.multimodal.show .mm-max { + transform: translate(-50%, -50%) scale(var(--slide-scale)); +} +.multimodal.show iframe { + opacity: 1; +} +.multimodal.show.shown .mm-max { + transition: none; +} \ No newline at end of file diff --git a/website/slides/_extensions/multimodal/multimodal.js b/website/slides/_extensions/multimodal/multimodal.js new file mode 100644 index 0000000..a9523da --- /dev/null +++ b/website/slides/_extensions/multimodal/multimodal.js @@ -0,0 +1,15 @@ +/***************************************************************** + * + * Multimodal for Reveal.js + * Version 1.0.1 + * + * @author: Martijn De Jongh (Martino), martijn.de.jongh@gmail.com + * https://github.com/martinomagnifico + * + * @license + * MIT licensed + * + * Copyright (C) 2023 Martijn De Jongh (Martino) + * + ******************************************************************/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Multimodal=t()}(this,(function(){"use strict";var e="undefined"!=typeof document?document.currentScript:null;const t=e=>e&&"object"==typeof e&&!Array.isArray(e),n=(e,...s)=>{if(!s.length)return e;const r=s.shift();if(t(e)&&t(r))for(const s in r)t(r[s])?(e[s]||Object.assign(e,{[s]:{}}),n(e[s],r[s])):Object.assign(e,{[s]:r[s]});return n(e,...s)},s=(e,t,n)=>{let s,r=document.querySelector("head"),i=!1;if("script"===t?document.querySelector(`script[src="${e}"]`)?i=!0:(s=document.createElement("script"),s.type="text/javascript",s.src=e):"stylesheet"===t&&(document.querySelector(`link[href="${e}"]`)?i=!0:(s=document.createElement("link"),s.rel="stylesheet",s.href=e)),!i){const e=()=>{"function"==typeof n&&(n.call(),n=null)};s.onload=e,s.onreadystatechange=function(){"loaded"===this.readyState&&e()},r.appendChild(s)}},r=t=>{let n,s=document.querySelector(`script[src$="${t}"]`);return n=s?s.getAttribute("src").slice(0,-1*t.length):("undefined"==typeof document&&"undefined"==typeof location?require("url").pathToFileURL(__filename).href:"undefined"==typeof document?location.href:e&&e.src||new URL("multimodal.js",document.baseURI).href).slice(0,("undefined"==typeof document&&"undefined"==typeof location?require("url").pathToFileURL(__filename).href:"undefined"==typeof document?location.href:e&&e.src||new URL("multimodal.js",document.baseURI).href).lastIndexOf("/")+1),n},i=(e,t=1)=>{const n=1/t;return Math.round(e*n)/n};const l="multimodal",o="mm-body",a="shown",c="show",d="multimodal:show",h="multimodal:shown",u="multimodal:hidden";class m{constructor(e){this.isOpen=!1,this.modalElement=e.getRevealElement().querySelector(`.${l}`),this.modalMax=this.modalElement.querySelector(".mm-max"),this.modalDialog=this.modalElement.querySelector(".mm-dialog"),this.modalBody=this.modalElement.querySelector(`.${o}`),this.modalElement.style.setProperty("display","none"),this.modalElement.setAttribute("aria-hidden","true"),this.eventListeners={},this.presetConfigs={},this.closeButtonHtml="",this.deck=e,this.modalElement.addEventListener("transitionend",(e=>{e.target==this.modalElement&&(this.isOpen?(this.modalElement.classList.add(a),this.trigger(h,"shown")):(this.modalElement.classList.remove(a),this.modalElement.classList.remove(c),this.modalElement.style.setProperty("display","none"),this.trigger(u,"hidden"),this.modalElement.classList.forEach((e=>{e!==l&&e!==c&&e!==a&&"hide"!==e&&"hidden"!==e&&this.modalElement.classList.remove(e)}))))}))}on(e,t){this.eventListeners[e]||(this.eventListeners[e]=[]),this.eventListeners[e].push(t)}trigger(e,t="show",...n){const s=this.eventListeners[e];s&&s.forEach((e=>e(t,...n)));let r={action:t,trigger:this.triggerElement,modal:this.modalElement,dialog:this.modalDialog,body:this.modalDialog.querySelector(`.${o}`),args:n};const i=new CustomEvent(e,{detail:r});this.modalElement.dispatchEvent(i)}show(){this.isOpen=!0,this.modalElement.style.removeProperty("display"),this.modalElement.removeAttribute("aria-hidden"),"0s"===window.getComputedStyle(this.modalElement).getPropertyValue("transition-duration")?(this.modalElement.classList.add(c),this.trigger(d,"show"),this.modalElement.classList.add(a),this.trigger(h,"shown")):setTimeout((()=>{this.modalElement.classList.add(c),this.trigger(d,"show")}),10)}hide(){this.isOpen=!1,this.trigger("multimodal:hide","hide"),this.modalElement.setAttribute("aria-hidden","true"),"0s"===window.getComputedStyle(this.modalElement).getPropertyValue("transition-duration")?(this.modalElement.classList.remove(a),this.modalElement.classList.remove(c),this.modalElement.style.setProperty("display","none"),this.trigger(u,"hide")):this.modalElement.classList.remove(c)}setContent(e,t){let n=e;"html"==t&&(n=`
${e}
`),this.modalDialog.innerHTML=this.closeButtonHtml+n}}let p=!1;if("undefined"!=typeof window){const e={get passive(){p=!0}};window.addEventListener("testPassive",null,e),window.removeEventListener("testPassive",null,e)}const g="undefined"!=typeof window&&window.navigator&&window.navigator.platform&&(/iP(ad|hone|od)/.test(window.navigator.platform)||"MacIntel"===window.navigator.platform&&window.navigator.maxTouchPoints>1);let f,k,w,x,y=[],b=new Map,v=!1,E=-1;const $=e=>y.some((t=>!(!t.options.allowTouchMove||!t.options.allowTouchMove(e)))),_=e=>{const t=e||window.event;return!!$(t.target)||(t.touches.length>1||(t.preventDefault&&t.preventDefault(),!1))},S=(e,t)=>{if(!e)return void console.error("disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.");if(b.set(e,(null==b?void 0:b.get(e))?(null==b?void 0:b.get(e))+1:1),y.some((t=>t.targetElement===e)))return;const n={targetElement:e,options:t||{}};y=[...y,n],g?window.requestAnimationFrame((()=>{const e=document.documentElement,t=document.body;if(void 0===w){k={...e.style},w={...t.style};const{scrollY:n,scrollX:s,innerHeight:r}=window;e.style.height="100%",e.style.overflow="hidden",t.style.position="fixed",t.style.top=-n+"px",t.style.left=-s+"px",t.style.width="100%",t.style.height="auto",t.style.overflow="hidden",setTimeout((()=>window.requestAnimationFrame((()=>{const e=r-window.innerHeight;e&&n>=r&&(t.style.top=-(n+e)+"px")}))),300)}})):(e=>{if(void 0===x){const t=!!e&&!0===e.reserveScrollBarGap,n=window.innerWidth-document.documentElement.getBoundingClientRect().width;if(t&&n>0){const e=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right"),10);x=document.body.style.paddingRight,document.body.style.paddingRight=`${e+n}px`}}void 0===f&&(f=document.body.style.overflow,document.body.style.overflow="hidden")})(t),g&&(e.ontouchstart=e=>{1===e.targetTouches.length&&(E=e.targetTouches[0].clientY)},e.ontouchmove=t=>{1===t.targetTouches.length&&((e,t)=>{const n=e.targetTouches[0].clientY-E;!$(e.target)&&(t&&0===t.scrollTop&&n>0||(e=>!!e&&e.scrollHeight-e.scrollTop<=e.clientHeight)(t)&&n<0?_(e):e.stopPropagation())})(t,e)},v||(document.addEventListener("touchmove",_,p?{passive:!1}:void 0),v=!0))},z=e=>{e?(b.set(e,(null==b?void 0:b.get(e))?(null==b?void 0:b.get(e))-1:0),0===(null==b?void 0:b.get(e))&&(y=y.filter((t=>t.targetElement!==e)),null==b||b.delete(e)),g&&(e.ontouchstart=null,e.ontouchmove=null,v&&0===y.length&&(document.removeEventListener("touchmove",_,p?{passive:!1}:void 0),v=!1)),0===y.length&&(g?(()=>{if(void 0!==w){const e=-parseInt(document.body.style.top,10),t=-parseInt(document.body.style.left,10),n=document.documentElement,s=document.body;n.style.height=(null==k?void 0:k.height)||"",n.style.overflow=(null==k?void 0:k.overflow)||"",s.style.position=w.position||"",s.style.top=w.top||"",s.style.left=w.left||"",s.style.width=w.width||"",s.style.height=w.height||"",s.style.overflow=w.overflow||"",window.scrollTo(t,e),w=void 0}})():(void 0!==x&&(document.body.style.paddingRight=x,x=void 0),void 0!==f&&(document.body.style.overflow=f,f=void 0)))):console.error("enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.")},T=e=>!!e.getRevealElement().closest(".reveal-scroll"),L=(e,t,n,s,r)=>{let l=(1-e)*t.offsetWidth/n,o=(1-e)*t.offsetHeight/n;if(s.modalElement.style.setProperty("--mm-maxwidth",`${Math.floor(l)}px`),s.modalElement.style.setProperty("--mm-maxheight",`${Math.floor(o)}px`),r.scalecorrection){let e=Math.max(1,1/n);s.modalElement.style.setProperty("--mm-inversescale",i(e,.1));let t=r.borderwidth.match(/(\d*\.?\d+)\s*(\w+)/);if(null!==t){let n=parseFloat(t[1]),r=t[2],l=i(n*e,.5)+r;s.modalElement.style.setProperty("--mm-borderwidth",l)}}};function A(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}let R={async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const P=/[&<>"']/,C=new RegExp(P.source,"g"),I=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,q=new RegExp(I.source,"g"),M={"&":"&","<":"<",">":">",'"':""","'":"'"},O=e=>M[e];function D(e,t){if(t){if(P.test(e))return e.replace(C,O)}else if(I.test(e))return e.replace(q,O);return e}const B=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function H(e){return e.replace(B,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const U=/(^|[^\[])\^/g;function Z(e,t){e="string"==typeof e?e:e.source,t=t||"";const n={replace:(t,s)=>(s=(s=s.source||s).replace(U,"$1"),e=e.replace(t,s),n),getRegex:()=>new RegExp(e,t)};return n}const N=/[^\w:]/g,j=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function Q(e,t,n){if(e){let e;try{e=decodeURIComponent(H(n)).replace(N,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!j.test(n)&&(n=function(e,t){F[" "+e]||(W.test(e)?F[" "+e]=e+"/":F[" "+e]=J(e,"/",!0));e=F[" "+e];const n=-1===e.indexOf(":");return"//"===t.substring(0,2)?n?t:e.replace(G,"$1")+t:"/"===t.charAt(0)?n?t:e.replace(V,"$1")+t:e+t}(t,n));try{n=encodeURI(n).replace(/%25/g,"%")}catch(e){return null}return n}const F={},W=/^[^:]+:\/*[^/]*$/,G=/^([^:]+:)[\s\S]*$/,V=/^([^:]+:\/*[^/]*)[\s\S]*$/;const X={exec:function(){}};function Y(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),n.length>t)n.splice(t);else for(;n.length1;)1&t&&(n+=e),t>>=1,e+=e;return n+e}function ee(e,t,n,s){const r=t.href,i=t.title?D(t.title):null,l=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:n,href:r,title:i,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,e}return{type:"image",raw:n,href:r,title:i,text:D(l)}}class te{constructor(e){this.options=e||R}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:J(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline._escapes,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=J(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *>[ \t]?/gm,""),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n,s,r,i,l,o,a,c,d,h,u,m,p=t[1].trim();const g=p.length>1,f={type:"list",raw:"",ordered:g,start:g?+p.slice(0,-1):"",loose:!1,items:[]};p=g?`\\d{1,9}\\${p.slice(-1)}`:`\\${p}`,this.options.pedantic&&(p=g?p:"[*+-]");const k=new RegExp(`^( {0,3}${p})((?:[\t ][^\\n]*)?(?:\\n|$))`);for(;e&&(m=!1,t=k.exec(e))&&!this.rules.block.hr.test(e);){if(n=t[0],e=e.substring(n.length),c=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),d=e.split("\n",1)[0],this.options.pedantic?(i=2,u=c.trimLeft()):(i=t[2].search(/[^ ]/),i=i>4?1:i,u=c.slice(i),i+=t[1].length),o=!1,!c&&/^ *$/.test(d)&&(n+=d+"\n",e=e.substring(d.length+1),m=!0),!m){const t=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),s=new RegExp(`^ {0,${Math.min(3,i-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:\`\`\`|~~~)`),l=new RegExp(`^ {0,${Math.min(3,i-1)}}#`);for(;e&&(h=e.split("\n",1)[0],d=h,this.options.pedantic&&(d=d.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!r.test(d))&&!l.test(d)&&!t.test(d)&&!s.test(e);){if(d.search(/[^ ]/)>=i||!d.trim())u+="\n"+d.slice(i);else{if(o)break;if(c.search(/[^ ]/)>=4)break;if(r.test(c))break;if(l.test(c))break;if(s.test(c))break;u+="\n"+d}o||d.trim()||(o=!0),n+=h+"\n",e=e.substring(h.length+1),c=d.slice(i)}}f.loose||(a?f.loose=!0:/\n *\n *$/.test(n)&&(a=!0)),this.options.gfm&&(s=/^\[[ xX]\] /.exec(u),s&&(r="[ ] "!==s[0],u=u.replace(/^\[[ xX]\] +/,""))),f.items.push({type:"list_item",raw:n,task:!!s,checked:r,loose:!1,text:u}),f.raw+=n}f.items[f.items.length-1].raw=n.trimRight(),f.items[f.items.length-1].text=u.trimRight(),f.raw=f.raw.trimRight();const w=f.items.length;for(l=0;l"space"===e.type)),t=e.length>0&&e.some((e=>/\n.*\n/.test(e.raw)));f.loose=t}if(f.loose)for(l=0;l$/,"$1").replace(this.rules.inline._escapes,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline._escapes,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:Y(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let n,s,r,i,l=e.align.length;for(n=0;n({text:e})));for(l=e.header.length,s=0;s/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):D(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=J(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const n=e.length;let s=0,r=0;for(;r-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),ee(t,{href:n?n.replace(this.rules.inline._escapes,"$1"):n,title:s?s.replace(this.rules.inline._escapes,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let e=(n[2]||n[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return ee(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrong.lDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;const r=s[1]||s[2]||"";if(!r||r&&(""===n||this.rules.inline.punctuation.exec(n))){const n=s[0].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=r.length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=e.slice(0,n+s.index+(s[0].length-r.length)+i);if(Math.min(n,i)%2){const e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}const a=t.slice(2,-2);return{type:"strong",raw:t,text:a,tokens:this.lexer.inlineTokens(a)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=D(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e,t){const n=this.rules.inline.autolink.exec(e);if(n){let e,s;return"@"===n[2]?(e=D(this.options.mangle?t(n[1]):n[1]),s="mailto:"+e):(e=D(n[1]),s=e),{type:"link",raw:n[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let n;if(n=this.rules.inline.url.exec(e)){let e,s;if("@"===n[2])e=D(this.options.mangle?t(n[0]):n[0]),s="mailto:"+e;else{let t;do{t=n[0],n[0]=this.rules.inline._backpedal.exec(n[0])[0]}while(t!==n[0]);e=D(n[0]),s="www."===n[1]?"http://"+n[0]:n[0]}return{type:"link",raw:n[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const n=this.rules.inline.text.exec(e);if(n){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(n[0]):D(n[0]):n[0]:D(this.options.smartypants?t(n[0]):n[0]),{type:"text",raw:n[0],text:e}}}}const ne={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:X,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};ne.def=Z(ne.def).replace("label",ne._label).replace("title",ne._title).getRegex(),ne.bullet=/(?:[*+-]|\d{1,9}[.)])/,ne.listItemStart=Z(/^( *)(bull) */).replace("bull",ne.bullet).getRegex(),ne.list=Z(ne.list).replace(/bull/g,ne.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+ne.def.source+")").getRegex(),ne._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",ne._comment=/|$)/,ne.html=Z(ne.html,"i").replace("comment",ne._comment).replace("tag",ne._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),ne.paragraph=Z(ne._paragraph).replace("hr",ne.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",ne._tag).getRegex(),ne.blockquote=Z(ne.blockquote).replace("paragraph",ne.paragraph).getRegex(),ne.normal={...ne},ne.gfm={...ne.normal,table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"},ne.gfm.table=Z(ne.gfm.table).replace("hr",ne.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",ne._tag).getRegex(),ne.gfm.paragraph=Z(ne._paragraph).replace("hr",ne.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",ne.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",ne._tag).getRegex(),ne.pedantic={...ne.normal,html:Z("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",ne._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:X,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:Z(ne.normal._paragraph).replace("hr",ne.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",ne.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()};const se={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:X,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(n="x"+n.toString(16)),s+="&#"+n+";";return s}se._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",se.punctuation=Z(se.punctuation).replace(/punctuation/g,se._punctuation).getRegex(),se.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,se.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,se._comment=Z(ne._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),se.emStrong.lDelim=Z(se.emStrong.lDelim).replace(/punct/g,se._punctuation).getRegex(),se.emStrong.rDelimAst=Z(se.emStrong.rDelimAst,"g").replace(/punct/g,se._punctuation).getRegex(),se.emStrong.rDelimUnd=Z(se.emStrong.rDelimUnd,"g").replace(/punct/g,se._punctuation).getRegex(),se._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,se._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,se._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,se.autolink=Z(se.autolink).replace("scheme",se._scheme).replace("email",se._email).getRegex(),se._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,se.tag=Z(se.tag).replace("comment",se._comment).replace("attribute",se._attribute).getRegex(),se._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,se._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,se._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,se.link=Z(se.link).replace("label",se._label).replace("href",se._href).replace("title",se._title).getRegex(),se.reflink=Z(se.reflink).replace("label",se._label).replace("ref",ne._label).getRegex(),se.nolink=Z(se.nolink).replace("ref",ne._label).getRegex(),se.reflinkSearch=Z(se.reflinkSearch,"g").replace("reflink",se.reflink).replace("nolink",se.nolink).getRegex(),se.normal={...se},se.pedantic={...se.normal,strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:Z(/^!?\[(label)\]\((.*?)\)/).replace("label",se._label).getRegex(),reflink:Z(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",se._label).getRegex()},se.gfm={...se.normal,escape:Z(se.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((function(e){s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+K("a",i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+K("a",i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.escapedEmSt.exec(a));)a=a.slice(0,i.index+i[0].length-2)+"++"+a.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex),this.tokenizer.rules.inline.escapedEmSt.lastIndex--;for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e,ie))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e,ie))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((function(e){s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r,re))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class oe{constructor(e){this.options=e||R}code(e,t,n){const s=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,s);null!=t&&t!==e&&(n=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",s?'
'+(n?e:D(e,!0))+"
\n":"
"+(n?e:D(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e){return e}heading(e,t,n,s){if(this.options.headerIds){return`${e}\n`}return`${e}\n`}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e){return`
  • ${e}
  • \n`}checkbox(e){return" "}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return this.options.xhtml?"
    ":"
    "}del(e){return`${e}`}link(e,t,n){if(null===(e=Q(this.options.sanitize,this.options.baseUrl,e)))return n;let s='",s}image(e,t,n){if(null===(e=Q(this.options.sanitize,this.options.baseUrl,e)))return n;let s=`${n}":">",s}text(e){return e}}class ae{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,n){return""+n}image(e,t,n){return""+n}br(){return""}}class ce{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let n=e,s=0;if(this.seen.hasOwnProperty(n)){s=this.seen[e];do{s++,n=e+"-"+s}while(this.seen.hasOwnProperty(n))}return t||(this.seen[e]=s,this.seen[n]=0),n}slug(e,t={}){const n=this.serialize(e);return this.getNextSafeSlug(n,t.dryrun)}}class de{constructor(e){this.options=e||R,this.options.renderer=this.options.renderer||new oe,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new ae,this.slugger=new ce}static parse(e,t){return new de(t).parse(e)}static parseInline(e,t){return new de(t).parseInline(e)}parse(e,t=!0){let n,s,r,i,l,o,a,c,d,h,u,m,p,g,f,k,w,x,y,b="";const v=e.length;for(n=0;n0&&"paragraph"===f.tokens[0].type?(f.tokens[0].text=x+" "+f.tokens[0].text,f.tokens[0].tokens&&f.tokens[0].tokens.length>0&&"text"===f.tokens[0].tokens[0].type&&(f.tokens[0].tokens[0].text=x+" "+f.tokens[0].tokens[0].text)):f.tokens.unshift({type:"text",text:x}):g+=x),g+=this.parse(f.tokens,p),d+=this.renderer.listitem(g,w,k);b+=this.renderer.list(d,u,m);continue;case"html":b+=this.renderer.html(h.text);continue;case"paragraph":b+=this.renderer.paragraph(this.parseInline(h.tokens));continue;case"text":for(d=h.tokens?this.parseInline(h.tokens):h.text;n+1{"function"==typeof s&&(r=s,s=null);const i={...s},l=function(e,t,n){return s=>{if(s.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+D(s.message+"",!0)+"
    ";return t?Promise.resolve(e):n?void n(null,e):e}if(t)return Promise.reject(s);if(!n)throw s;n(s)}}((s={...me.defaults,...i}).silent,s.async,r);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(function(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")}(s),s.hooks&&(s.hooks.options=s),r){const i=s.highlight;let o;try{s.hooks&&(n=s.hooks.preprocess(n)),o=e(n,s)}catch(e){return l(e)}const a=function(e){let n;if(!e)try{s.walkTokens&&me.walkTokens(o,s.walkTokens),n=t(o,s),s.hooks&&(n=s.hooks.postprocess(n))}catch(t){e=t}return s.highlight=i,e?l(e):r(null,n)};if(!i||i.length<3)return a();if(delete s.highlight,!o.length)return a();let c=0;return me.walkTokens(o,(function(e){"code"===e.type&&(c++,setTimeout((()=>{i(e.text,e.lang,(function(t,n){if(t)return a(t);null!=n&&n!==e.text&&(e.text=n,e.escaped=!0),c--,0===c&&a()}))}),0))})),void(0===c&&a())}if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(n):n).then((t=>e(t,s))).then((e=>s.walkTokens?Promise.all(me.walkTokens(e,s.walkTokens)).then((()=>e)):e)).then((e=>t(e,s))).then((e=>s.hooks?s.hooks.postprocess(e):e)).catch(l);try{s.hooks&&(n=s.hooks.preprocess(n));const r=e(n,s);s.walkTokens&&me.walkTokens(r,s.walkTokens);let i=t(r,s);return s.hooks&&(i=s.hooks.postprocess(i)),i}catch(e){return l(e)}}}function me(e,t,n){return ue(le.lex,de.parse)(e,t,n)}async function pe(e,t,n,s){const r=e.dataset.modalType,i=e.dataset.modalUrl||e.getAttribute("href")||null;let l,o=n.padding;if(e.dataset.modalOverlaycolor&&e.dataset.modalOverlaycolor!==s.overlaycolor&&t.modalElement.style.setProperty("--mm-overlaycolor",e.dataset.modalOverlaycolor),e.dataset.modalClass&&t.modalElement.classList.add(...e.dataset.modalClass.split(/[ ,]+/)),"html"==r){o=e.dataset.modalPadding||n.padding.html||n.padding,o!==s.padding.html&&t.modalElement.style.setProperty("--mm-modal-padding",o);let r=e.dataset.modalBackground||n.background.html||n.background;r!==s.background.html&&t.modalElement.style.setProperty("--mm-modal-background",r)}else if("iframe"==r){o=e.dataset.modalPadding||n.padding.iframe||n.padding,"0"!==o&&t.modalElement.style.setProperty("--mm-modal-padding",o);let r=e.dataset.modalBgcolor||n.background.iframe||n.background;r!==s.background.iframe&&t.modalElement.style.setProperty("--mm-modal-background",r)}else{o=e.dataset.modalPadding||n.padding.media||n.padding,"0"!==o&&t.modalElement.style.setProperty("--mm-modal-padding",o);let r=e.dataset.modalBackground||n.background.media||n.background;r!==s.background.media&&t.modalElement.style.setProperty("--mm-modal-background",r)}t.triggerElement=e;let a=!1;if("video"===r)l=await async function(e,t,n){try{const s=document.createElement("video");if(s.src=e,s.controls=n.videocontrols,s.preload="metadata",await new Promise(((e,t)=>{s.addEventListener("loadedmetadata",e),s.addEventListener("error",t)})),s.readyState>=0)return s.preload="auto",t.modalElement.dataset.modalType="video",s.classList.add("mm-body"),s.outerHTML;throw new Error("Video metadata cannot be loaded.")}catch(e){return console.error("Error loading video:",e),null}}(i,t,n);else if("image"===r){let n=i&&"#"!=i&&"#/"!=i?i:"IMG"==e.firstChild.tagName?e.firstChild.getAttribute("src"):null,s="IMG"==e.firstChild.tagName?e.firstChild.getAttribute("alt"):null;l=await async function(e,t,n){return new Promise((s=>{const r=new Image;let i="";n&&(i=` alt="${n}"`),r.onload=()=>{t.modalElement.dataset.modalType="image",s(``)},r.onerror=()=>{console.error(`Error loading image: ${e}`),s(null)},r.src=e}))}(n,t,s)}else if("html"===r)l=await function(e,t){return new Promise((async(n,s)=>{try{let s;if("string"!=typeof e)throw new Error("Invalid HTML content source. Must be a URL string or an anchor element.");if(e.startsWith("#")){const n=document.querySelector(e);if(!n)throw new Error("Anchor element with specified ID not found.");s="section"===n.firstElementChild.tagName.toLowerCase()?n.firstElementChild.innerHTML:n.innerHTML,t.modalElement.dataset.modalType="html"}else if(e.endsWith(".md")){const n=await fetch(e);if(!n.ok)throw new Error("Failed to fetch MD content.");const r=await n.text();t.modalElement.dataset.modalType="html",s=me(r)}else{if(!e.endsWith(".html"))throw new Error("Unsupported HTML content source.");{const n=await fetch(e);if(!n.ok)throw new Error("Failed to fetch HTML content.");s=await n.text(),t.modalElement.dataset.modalType="html"}}n(s)}catch(e){console.error("Error loading HTML content:",e),s(e)}}))}(i,t);else{if("iframe"!==r)return void console.error("Unknown modal type");l=``,t.modalElement.dataset.modalType="iframe"}l||(a=!0),a||(t.setContent(l,r),t.show())}me.options=me.setOptions=function(e){var t;return me.defaults={...me.defaults,...e},t=me.defaults,R=t,me},me.getDefaults=A,me.defaults=R,me.use=function(...e){const t=me.defaults.extensions||{renderers:{},childTokens:{}};e.forEach((e=>{const n={...e};if(n.async=me.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");t[e.level]?t[e.level].unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=me.defaults.renderer||new oe;for(const n in e.renderer){const s=t[n];t[n]=(...r)=>{let i=e.renderer[n].apply(t,r);return!1===i&&(i=s.apply(t,r)),i}}n.renderer=t}if(e.tokenizer){const t=me.defaults.tokenizer||new te;for(const n in e.tokenizer){const s=t[n];t[n]=(...r)=>{let i=e.tokenizer[n].apply(t,r);return!1===i&&(i=s.apply(t,r)),i}}n.tokenizer=t}if(e.hooks){const t=me.defaults.hooks||new he;for(const n in e.hooks){const s=t[n];he.passThroughHooks.has(n)?t[n]=r=>{if(me.defaults.async)return Promise.resolve(e.hooks[n].call(t,r)).then((e=>s.call(t,e)));const i=e.hooks[n].call(t,r);return s.call(t,i)}:t[n]=(...r)=>{let i=e.hooks[n].apply(t,r);return!1===i&&(i=s.apply(t,r)),i}}n.hooks=t}if(e.walkTokens){const t=me.defaults.walkTokens;n.walkTokens=function(n){let s=[];return s.push(e.walkTokens.call(this,n)),t&&(s=s.concat(t.call(this,n))),s}}me.setOptions(n)}))},me.walkTokens=function(e,t){let n=[];for(const s of e)switch(n=n.concat(t.call(me,s)),s.type){case"table":for(const e of s.header)n=n.concat(me.walkTokens(e.tokens,t));for(const e of s.rows)for(const s of e)n=n.concat(me.walkTokens(s.tokens,t));break;case"list":n=n.concat(me.walkTokens(s.items,t));break;default:me.defaults.extensions&&me.defaults.extensions.childTokens&&me.defaults.extensions.childTokens[s.type]?me.defaults.extensions.childTokens[s.type].forEach((function(e){n=n.concat(me.walkTokens(s[e],t))})):s.tokens&&(n=n.concat(me.walkTokens(s.tokens,t)))}return n},me.parseInline=ue(le.lexInline,de.parseInline),me.Parser=de,me.parser=de.parse,me.Renderer=oe,me.TextRenderer=ae,me.Lexer=le,me.lexer=le.lex,me.Tokenizer=te,me.Slugger=ce,me.Hooks=he,me.parse=me,me.options,me.setOptions,me.use,me.walkTokens,me.parseInline,de.parse,le.lex;return()=>{let e={};const t=(e,t,n)=>{const s="data-modal-type",r="data-modal-close",i=e.getRevealElement(),l=Array.from(i.querySelectorAll(`[${s}]:not(section)`)),o=e.getConfig().margin,a=e=>{const a=new m(e);((e,t)=>{t.presetConfigs={keyboard:e.getConfig().keyboard,mouseWheel:e.getConfig().mouseWheel,scrollProgress:e.getConfig().scrollProgress}})(e,a),async function(e,t,n){if(""!==t.closebuttonhtml&&(e.closeButtonHtml=t.closebuttonhtml),t.overlaycolor!==n.overlaycolor&&e.modalElement.style.setProperty("--mm-overlaycolor",t.overlaycolor),t.speed!==n.speed){let n=t.speed/1e3;e.modalElement.style.setProperty("--mm-transspeed",`${n}s`)}if(t.htmlminwidth!==n.htmlminwidth){let n=isNaN(t.htmlminwidth)?t.htmlminwidth:`${t.htmlminwidth}px`;e.modalElement.style.setProperty("--mm-minwidth",n)}if(t.htmlminheight!==n.htmlminheight){let n=isNaN(t.htmlminheight)?t.htmlminheight:`${t.htmlminheight}px`;e.modalElement.style.setProperty("--mm-minheight",n)}t.radius!==n.radius&&e.modalElement.style.setProperty("--mm-outerradius",t.radius),t.bordercolor!==n.bordercolor&&e.modalElement.style.setProperty("--mm-bordercolor",t.bordercolor),t.borderwidth!==n.borderwidth&&e.modalElement.style.setProperty("--mm-borderwidth",t.borderwidth),t.shadow!==n.shadow&&e.modalElement.style.setProperty("--mm-shadow",t.shadow),t.zoom?t.zoomfrom!==n.zoomfrom&&e.modalElement.style.setProperty("--mm-initialscale",t.zoomfrom):e.modalElement.style.setProperty("--mm-initialscale",1)}(a,t,n);const c=e=>{"Escape"!==e.key&&27!==e.keyCode||a.isOpen&&a.hide()};a.closeButtonHtml=``;const d=async e=>{e.preventDefault();const s=e.currentTarget;!s.hasAttribute("href")||"#"!==s.getAttribute("href")&&"#/"!==s.getAttribute("href")||e.stopPropagation(),await pe(s,a,t,n)};l.forEach((e=>{e.addEventListener("click",d)}));const h=new MutationObserver((e=>{e.forEach((e=>{"childList"===e.type&&e.addedNodes.forEach((e=>{1===e.nodeType&&e.hasAttribute(s)&&"section"!==e.tagName.toLowerCase()&&e.addEventListener("click",d)}))}))}));e.addEventListener("click",(async e=>{(e.target.closest(`[${r}]`)||e.target.closest('a[href="#multimodal"]'))&&(e.preventDefault(),e.stopPropagation(),a.hide())}));const u=e=>{if(e.dataset.modalType){return e.dataset.modalEvent?e.dataset.modalEvent:t.slidemodalevent}};e.on("slidechanged",(async s=>{a.isOpen&&a.hide(),function(e){let t=e.querySelectorAll("[data-modal-type]");t&&t.forEach((e=>{if("image"==e.dataset.modalType){let t=e.dataset.modalUrl||e.getAttribute("href")||null;t||(t="IMG"==e.firstChild.tagName?e.firstChild.getAttribute("src"):null),t&&((new Image).src=t)}}))}(s.currentSlide),T(e)?s.currentSlide.dataset.modalType&&"slidetransitionend"==t.slidemodalevent&&setTimeout((()=>{e.getCurrentSlide()==s.currentSlide&&pe(s.currentSlide,a,t,n)}),10):u(s.currentSlide)&&"slidechanged"==u(s.currentSlide)&&await pe(s.currentSlide,a,t,n)})),e.on("slidetransitionend",(async e=>{u(e.currentSlide)&&"slidetransitionend"==u(e.currentSlide)&&await pe(e.currentSlide,a,t,n)})),e.on("ready",(async s=>{L(o,i,e.getScale(),a,t),!u(s.currentSlide)||"slidetransitionend"!=u(s.currentSlide)&&"slidechanged"!=u(s.currentSlide)||await pe(s.currentSlide,a,t,n),h.observe(i,{childList:!0,subtree:!0})})),e.on("resize",(e=>{L(o,i,e.scale,a,t)})),a.on("multimodal:show",(()=>{i.classList.add("multimodal-open"),document.addEventListener("keydown",c),a.modalElement.closest(".reveal-scroll")||((e,t)=>{if("video"==t.modalElement.dataset.modalType){const n=t.modalDialog.querySelector("video");n&&e.configure({keyboard:{27:()=>{t.hide()},32:()=>{n.paused?n.play():n.pause()}}})}else e.configure({keyboard:{27:()=>{t.hide()},32:()=>{t.hide()}}})})(e,a);const t=e=>{a.isLocked||e.target!==a.modalElement&&e.target!==a.modalMax||a.hide()};a.modalElement.addEventListener("click",t),a.closeOnClickOutside=t})),a.on("multimodal:shown",(()=>{a.triggerElement&&a.triggerElement.dataset.modalNavblock&&"true"==a.triggerElement.dataset.modalNavblock&&((e,t)=>{t.modalElement&&S(t.modalElement),T(e)||e.configure({keyboard:!1,mouseWheel:!1,scrollProgress:!1}),t.isLocked=!0})(e,a);const n=a.modalDialog.querySelector("video");n&&(t.videoautoplay&&n.play(),t.videoautohide&&n.addEventListener("ended",(()=>{a.hide()})),n.addEventListener("webkitendfullscreen",(function(e){t.debug&&console.log("Exited fullscreen"),a.hide()})))})),a.on("multimodal:hide",(()=>{i.classList.remove("multimodal-open"),a.closeOnClickOutside&&(a.modalElement.removeEventListener("click",a.closeOnClickOutside),a.closeOnClickOutside=null),((e,t)=>{t.modalElement&&z(t.modalElement),T(e)||e.configure({keyboard:t.presetConfigs.keyboard,mouseWheel:t.presetConfigs.mouseWheel,scrollProgress:t.presetConfigs.scrollProgress}),t.isLocked=!1})(e,a),document.removeEventListener("keydown",c);const t=a.modalDialog.querySelector("video");t&&t.pause()})),a.on("multimodal:hidden",(()=>{a.modalBody.innerHTML="";const e=a.modalDialog.querySelector("iframe");e&&(e.src=""),a.modalElement.style.removeProperty("--mm-modal-background"),a.modalElement.style.removeProperty("--mm-overlaycolor"),a.modalElement.style.removeProperty("--mm-modal-padding"),a.modalElement.style.removeProperty("--mm-outerradius")})),t.debug&&(e.on("multimodal:shown",(()=>{console.log("Modal shown")})),e.on("multimodal:hidden",(()=>{console.log("Modal hidden")})),e.on("multimodal:show",(()=>{console.log("Modal show")})),e.on("multimodal:hide",(()=>{console.log("Modal hide")})))};i.querySelector(".multimodal")?a(e):function(e){return new Promise(((t,n)=>{const s='\n \n ';document.createElement("div").innerHTML=s,e.getSlidesElement().insertAdjacentHTML("afterend",s),t()}))}(e).then(a(e))};return{id:"multimodal",init:i=>{let l={background:{html:"var(--r-background-color)",iframe:"var(--r-background-color)",media:"white"},bordercolor:"white",borderwidth:"1px",closebuttonhtml:"",cssautoload:!0,csspath:"",htmlminwidth:"100px",htmlminheight:"100px",overlaycolor:"rgba(0, 0, 0, 0.30)",padding:{html:"1em",iframe:"0",media:"0"},radius:"0.5em",scalecorrection:!0,shadow:"0 0.5em 0.75em 0.5em rgba(0, 0, 0, 0.25)",slidemodalevent:"slidetransitionend",speed:300,videoautoplay:!0,videocontrols:!0,videoautohide:!0,zoom:!0,zoomfrom:.9},o=JSON.parse(JSON.stringify(l));if(e=n(l,i.getConfig().multimodal||{}),e.indexbase=i.getConfig().hashOneBasedIndex?1:0,e.cssautoload){let t=e.csspath.multimodal?e.csspath.multimodal:e.csspath?e.csspath:`${r()}multimodal.css`||"plugin/multimodal/multimodal.css";e.debug&&(console.log(`Plugin path = ${r()}`),console.log(`Multimodal CSS path = ${t}`));const n=document.querySelector("[name=generator]");n&&n.content.includes("quarto")||s(t,"stylesheet")}if(e.debug){let e=document.createElement("style");e.innerHTML=".reveal .slides, .scroll-page-content:has(section.present) { box-shadow: inset 0 0 0 1px orange} .mm-max { box-shadow: inset 0 0 0 1px red}",document.head.appendChild(e)}t(i,e,o)}}}})); diff --git a/website/slides/_extensions/pointer/_extension.yml b/website/slides/_extensions/pointer/_extension.yml new file mode 100644 index 0000000..40554c9 --- /dev/null +++ b/website/slides/_extensions/pointer/_extension.yml @@ -0,0 +1,15 @@ +title: Pointer +author: Charles Teague +version: 0.1.0 +quarto-required: ">=1.2.198" +contributes: + revealjs-plugins: + - name: RevealPointer + script: pointer.js + stylesheet: pointer.css + config: + pointer: + key: "q" + color: "red" + pointerSize: 16 + alwaysVisible: false diff --git a/website/slides/_extensions/pointer/pointer.css b/website/slides/_extensions/pointer/pointer.css new file mode 100644 index 0000000..bd55511 --- /dev/null +++ b/website/slides/_extensions/pointer/pointer.css @@ -0,0 +1 @@ +.cursor-dot,.cursor-dot-outline{pointer-events:none;position:absolute;top:0;left:0;border-radius:50%;opacity:0;transform:translate(-50%,-50%);transition:opacity 0.3s ease-in-out,transform 0.3s ease-in-out;}.cursor-dot{width:12px;height:12px;background-color:red;z-index:99;}.no-cursor{cursor:none;}.no-cursor a,.no-cursor div,.no-cursor span{cursor:none;} diff --git a/website/slides/_extensions/pointer/pointer.js b/website/slides/_extensions/pointer/pointer.js new file mode 100644 index 0000000..96da2f3 --- /dev/null +++ b/website/slides/_extensions/pointer/pointer.js @@ -0,0 +1 @@ +var RevealPointer=function(){"use strict";var e={backspace:8,tab:9,enter:13,shift:16,ctrl:17,alt:18,pausebreak:19,capslock:20,esc:27,space:32,pageup:33,pagedown:34,end:35,home:36,leftarrow:37,uparrow:38,rightarrow:39,downarrow:40,insert:45,delete:46,0:48,1:49,2:50,3:51,4:52,5:53,6:54,7:55,8:56,9:57,a:65,b:66,c:67,d:68,e:69,f:70,g:71,h:72,i:73,j:74,k:75,l:76,m:77,n:78,o:79,p:80,q:81,r:82,s:83,t:84,u:85,v:86,w:87,x:88,y:89,z:90,leftwindowkey:91,rightwindowkey:92,selectkey:93,numpad0:96,numpad1:97,numpad2:98,numpad3:99,numpad4:100,numpad5:101,numpad6:102,numpad7:103,numpad8:104,numpad9:105,multiply:106,add:107,subtract:109,decimalpoint:110,divide:111,f1:112,f2:113,f3:114,f4:115,f5:116,f6:117,f7:118,f8:119,f9:120,f10:121,f11:122,f12:123,numlock:144,scrolllock:145,semicolon:186,equalsign:187,comma:188,dash:189,period:190,forwardslash:191,graveaccent:192,openbracket:219,backslash:220,closebracket:221,singlequote:222};return function(){var t={},o=!1,a=null,n={x:0,y:0,isVisible:!1},i={x:0,y:0,scale:1};function l(o){var a;null==(t=o.pointer||{}).key?t.key="q":t.key=t.key.toLowerCase(),null!=t.pointerSize&&"number"==typeof t.pointerSize||(t.pointerSize=12),null!=t.tailLength&&"number"==typeof t.tailLength||(t.tailLength=10),null!=t.color&&"string"==typeof t.color||(t.color="red"),null!=t.alwaysVisible&&"boolean"==typeof t.alwaysVisible||(t.alwaysVisible=!1),t.keyCode=(a=t.key,e[a])}function s(){a.style.top="".concat((n.y-i.y)/i.scale,"px"),a.style.left="".concat((n.x-i.x)/i.scale,"px"),n.isVisible?a.style.opacity="0.8":a.style.opacity="0",1!==i.scale?(a.style.width="".concat(t.pointerSize/i.scale,"px"),a.style.height="".concat(t.pointerSize/i.scale,"px")):(a.style.width="".concat(t.pointerSize,"px"),a.style.height="".concat(t.pointerSize,"px"))}function c(e){n.x=e.pageX,n.y=e.pageY;var t=document.body.style.transform;""!==t?(i.x=Number.parseInt(/translate\((.*)px,/gm.exec(t)[1]),i.y=Number.parseInt(/px,\s(.*)px\)/gm.exec(t)[1]),i.scale=Number.parseFloat(/scale\((.)\)/gm.exec(t)[1])):(i.x=0,i.y=0,i.scale=1),requestAnimationFrame(s)}function r(){(o=!o)?(document.addEventListener("mousemove",c),document.body.classList.add("no-cursor"),n.isVisible=!0):(document.removeEventListener("mousemove",c),document.body.classList.remove("no-cursor"),n.isVisible=!1,requestAnimationFrame(s))}return{id:"pointer",init:function(e){var o;l(e.getConfig()),t.alwaysVisible?r():e.addKeyBinding({keyCode:t.keyCode,key:t.key,description: "Toggle pointer"},(function(){r()})),(o=document.createElement("div")).className="cursor-dot",o.style.width="".concat(t.pointerSize,"px"),o.style.height="".concat(t.pointerSize,"px"),o.style.backgroundColor=t.color,t.alwaysVisible&&(o.style.opacity="0.8"),document.body.appendChild(o),a=o}}}}(); diff --git a/website/slides/_extensions/verticator/_extension.yml b/website/slides/_extensions/verticator/_extension.yml new file mode 100644 index 0000000..fc15da2 --- /dev/null +++ b/website/slides/_extensions/verticator/_extension.yml @@ -0,0 +1,22 @@ +title: Verticator +author: Martijn De Jongh (Martino) +version: 1.2.3 +quarto-required: ">=1.2.198" +contributes: + revealjs-plugins: + - + name: Verticator + script: verticator.js + stylesheet: verticator.css + config: + verticator: + themetag: 'h1' + color: '' + inversecolor: '' + skipuncounted: false + clickable: true + position: 'auto' + offset: '3vmin' + autogenerate: true + tooltip: false + scale: 1 \ No newline at end of file diff --git a/website/slides/_extensions/verticator/verticator.css b/website/slides/_extensions/verticator/verticator.css new file mode 100644 index 0000000..74f12e7 --- /dev/null +++ b/website/slides/_extensions/verticator/verticator.css @@ -0,0 +1,186 @@ +ul.verticator { + opacity: 0; + will-change: color; + color: white; + color: var(--v-forcecolor, var(--v-color, var(--c-theme-color, white))); + -webkit-transition: opacity 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + -o-transition: opacity 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + transition: opacity 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + list-style: none; + right: auto; + left: auto; + margin: 0; + top: 50%; + -webkit-transform: translateY(-50%) translateZ(0); + transform: translateY(-50%) translateZ(0); + z-index: 10; + position: absolute; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding: 12px; + padding: calc(12px * var(--verticator-scale)); +} +ul.verticator.visible { + opacity: 0.6; +} +ul.verticator.visible:hover { + opacity: 1; +} +ul.verticator.no-click { + pointer-events: none; +} +ul.verticator li { + display: block; + position: relative; + will-change: color; + color: inherit; + margin: 0; + -webkit-transition: opacity 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + -o-transition: opacity 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + transition: opacity 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + opacity: 0; +} +ul.verticator li:before { + display: none; +} +ul.verticator li a { + color: inherit; + will-change: color; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + text-decoration: none; + width: calc(18px * var(--verticator-scale)); + height: calc(18px * var(--verticator-scale)); +} +ul.verticator li a:after { + content: ""; + will-change: color; + color: inherit; + background-color: currentColor; + display: block; + width: calc(12px * var(--verticator-scale)); + height: calc(12px * var(--verticator-scale)); + opacity: 0.6; + -webkit-transform: translateZ(0) scale(0.4); + transform: translateZ(0) scale(0.4); + border-radius: 100%; + -webkit-transition: opacity 200ms cubic-bezier(0.26, 0.86, 0.44, 0.985), -webkit-transform 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + transition: opacity 200ms cubic-bezier(0.26, 0.86, 0.44, 0.985), -webkit-transform 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + -o-transition: transform 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 200ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + transition: transform 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 200ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + transition: transform 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 200ms cubic-bezier(0.26, 0.86, 0.44, 0.985), -webkit-transform 300ms cubic-bezier(0.26, 0.86, 0.44, 0.985); + -webkit-transition-delay: 50ms; + -o-transition-delay: 50ms; + transition-delay: 50ms; +} +ul.verticator li a:hover { + color: inherit; +} +ul.verticator li a:hover:after { + opacity: 1; + -webkit-transform: translateZ(0) scale(0.75); + transform: translateZ(0) scale(0.75); +} +ul.verticator li.active a { + cursor: default; +} +ul.verticator li.active a:after { + -webkit-transform: translateZ(0) scale(1); + transform: translateZ(0) scale(1); + opacity: 1; +} +ul.verticator.visible li { + opacity: 1; +} + +.print-pdf .verticator, +.overview .verticator { + display: none; +} + +.verticator { + --verticator-tooltip-scale: 1; +} + +.verticator .tooltip { + position: absolute; + top: 9px; + right: 30px; + top: calc(9px * var(--verticator-scale)); + right: calc(30px * var(--verticator-scale) + 1.5px * var(--verticator-tooltip-scale) * var(--verticator-tooltip-scale)); + -webkit-transform: translate3d(0, -50%, 1px); + transform: translate3d(0, -50%, 1px); + -webkit-box-shadow: calc(4px * var(--verticator-scale)) calc(6px * var(--verticator-scale)) calc(16px * var(--verticator-scale)) rgba(0, 0, 0, 0.25); + box-shadow: calc(4px * var(--verticator-scale)) calc(6px * var(--verticator-scale)) calc(16px * var(--verticator-scale)) rgba(0, 0, 0, 0.25); + pointer-events: none; + background: white; + font-size: calc(1.25rem * var(--verticator-tooltip-scale) * var(--verticator-scale)); + padding: 0.4em 0.6em; + border-radius: 0.2em; + min-width: 30px; + max-width: 15em; + display: block; + -webkit-font-smoothing: antialiased; + color: black; +} +.verticator .tooltip span { + white-space: nowrap; + display: block; + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + line-height: 1.25; +} +.verticator .tooltip::after { + content: ""; + display: block; + width: 0.66em; + height: 0.66em; + background: white; + position: absolute; + right: 0; + top: 50%; + -webkit-transform: translate3d(45%, -50%, -1px) rotate(45deg); + transform: translate3d(45%, -50%, -1px) rotate(45deg); +} + +.verticator.left .tooltip { + right: auto; + left: calc(30px * var(--verticator-scale) + 1.5px * var(--verticator-tooltip-scale) * var(--verticator-tooltip-scale)); + -webkit-box-shadow: calc(-4px * var(--verticator-scale)) calc(6px * var(--verticator-scale)) calc(16px * var(--verticator-scale)) rgba(0, 0, 0, 0.25); + box-shadow: calc(-4px * var(--verticator-scale)) calc(6px * var(--verticator-scale)) calc(16px * var(--verticator-scale)) rgba(0, 0, 0, 0.25); +} +.verticator.left .tooltip::after { + right: auto; + left: 0; + -webkit-transform: translate3d(-45%, -50%, -1px) rotate(45deg); + transform: translate3d(-45%, -50%, -1px) rotate(45deg); +} + +.verticator li a + .tooltip { + pointer-events: none; + display: block; + opacity: 0; + -webkit-transition: opacity 0.125s ease-in-out; + -o-transition: opacity 0.125s ease-in-out; + transition: opacity 0.125s ease-in-out; + -webkit-transition-delay: 0.0625s; + -o-transition-delay: 0.0625s; + transition-delay: 0.0625s; +} + +.verticator li a:hover + .tooltip { + opacity: 1; +} \ No newline at end of file diff --git a/website/slides/_extensions/verticator/verticator.js b/website/slides/_extensions/verticator/verticator.js new file mode 100644 index 0000000..335db21 --- /dev/null +++ b/website/slides/_extensions/verticator/verticator.js @@ -0,0 +1,439 @@ + +/***************************************************************** + * @author: Martijn De Jongh (Martino), martijn.de.jongh@gmail.com + * https://github.com/Martinomagnifico + * + * Verticator.js for Reveal.js + * Version 1.2.3 + * + * @license + * MIT licensed + * + * Thanks to: + * - Hakim El Hattab, Reveal.js + ******************************************************************/ + + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Verticator = factory()); +})(this, (function () { 'use strict'; + + var Plugin = function Plugin() { + var lightClass = "has-light-background"; + var darkClass = "has-dark-background"; + var themeColorVar = "--c-theme-color"; + var vertiColorVar = "--v-color"; + var forceColorVar = "--v-forcecolor"; + var activeclass = 'active'; + + var loadStyle = function loadStyle(url, title, callback) { + var head = document.querySelector('head'); + var style = document.querySelector("[title=\"".concat(title, "\"]")); + + if (typeof style == 'undefined' || style == null) { + style = document.createElement('link'); + style.rel = 'stylesheet'; + style.href = url; + style.title = title; + + var finish = function finish() { + if (typeof callback === 'function') { + callback.call(); + callback = null; + } + }; + + style.onload = finish; + + style.onreadystatechange = function () { + if (this.readyState === 'loaded') { + finish(); + } + }; + + head.appendChild(style); + } + }; + + var findThemeColors = function findThemeColors(parent, tag) { + var themeColors = {}; + var colorSection = document.createElement("section"); + var sectionChild = document.createElement(tag); + parent.getElementsByClassName('slides')[0].appendChild(colorSection).appendChild(sectionChild); + themeColors.regular = getComputedStyle(sectionChild).getPropertyValue('color'); + colorSection.classList.add(lightClass); + themeColors.inverse = getComputedStyle(sectionChild).getPropertyValue('color'); + + if (themeColors.regular == themeColors.inverse) { + themeColors.theme = "light"; + colorSection.classList.remove(lightClass); + colorSection.classList.add(darkClass); + themeColors.inverse = getComputedStyle(sectionChild).getPropertyValue('color'); + } else { + themeColors.theme = "dark"; + } + + colorSection.remove(); + return themeColors; + }; + + var swapColors = function swapColors(needToSwap, options, colors, revealElement, theVerticator) { + if (needToSwap) { + revealElement.style.setProperty(themeColorVar, colors.themeinverse); + + if (options.inversecolor || options.oppositecolor) { + theVerticator.style.setProperty(vertiColorVar, colors.verticatorinverse); + } else { + theVerticator.style.removeProperty(vertiColorVar); + } + } else { + revealElement.style.setProperty(themeColorVar, colors.themeregular); + + if (options.color) { + theVerticator.style.setProperty(vertiColorVar, colors.verticatorregular); + } else { + theVerticator.style.removeProperty(vertiColorVar); + } + } + }; + + var verticate = function verticate(deck, options) { + var userScale = options.scale; + userScale = userScale > 2 ? 2 : userScale < 0.5 ? 0.5 : userScale; + var revealElement = deck.getRevealElement(); + var theVerticator = revealElement.querySelector('ul.verticator'); + + if (!theVerticator) { + if (!options.autogenerate) return; + var ul = document.createElement('ul'); + ul.className += "verticator"; + revealElement.insertBefore(ul, revealElement.childNodes[0]); + theVerticator = revealElement.querySelector('ul.verticator'); + } + + if (!options.clickable) { + theVerticator.classList.add('no-click'); + } + + var revealScale = deck.getScale(); + var totalScale = revealScale > 1 ? revealScale * userScale : userScale; + theVerticator.style.setProperty('--verticator-scale', totalScale.toFixed(2)); + var tooltipScaleDamper = 1 / Math.sqrt(totalScale); + theVerticator.style.setProperty('--verticator-tooltip-scale', tooltipScaleDamper.toFixed(2)); + var colors = {}; + var themeColors = findThemeColors(revealElement, options.themetag ? options.themetag : 'section'); + colors.theme = themeColors.theme; + colors.themeregular = themeColors.regular; + colors.themeinverse = themeColors.inverse; + colors.verticatorregular = options.color ? options.color : themeColors.regular; + colors.verticatorinverse = options.inversecolor ? options.inversecolor : options.oppositecolor ? options.oppositecolor : themeColors.inverse; + + if (options.debug) { + console.log("Theme regular color is: \"".concat(colors.themeregular, "\"")); + console.log("Theme inverse color is: \"".concat(colors.themeinverse, "\"")); + + if (options.color) { + console.log("Verticator regular color is: \"".concat(colors.verticatorregular, "\"")); + } + + if (options.inversecolor || options.oppositecolor) { + console.log("Verticator inverse color is: \"".concat(colors.verticatorinverse, "\"")); + } + } + + revealElement.style.setProperty(themeColorVar, colors.themeregular); + + if (options.color) { + theVerticator.style.setProperty(vertiColorVar, colors.verticatorregular); + } + + var sectionState = {}; + sectionState.dark = revealElement.classList.contains(darkClass); + sectionState.light = revealElement.classList.contains(lightClass); + sectionState.rtl = revealElement.classList.contains('rtl'); + var revealElementObserver = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + mutation.target; + + if (mutation.attributeName === 'class') { + var hasLightBg = mutation.target.classList.contains(lightClass); + var hasDarkBg = mutation.target.classList.contains(darkClass); + var leftAligned = mutation.target.classList.contains('rtl'); + + if (sectionState.dark !== hasDarkBg || sectionState.light !== hasLightBg) { + if (colors.theme == "dark" && sectionState.light !== hasLightBg) { + sectionState.light = hasLightBg; + swapColors(hasLightBg, options, colors, revealElement, theVerticator); + } + + if (colors.theme == "light" && sectionState.dark !== hasDarkBg) { + sectionState.dark = hasDarkBg; + swapColors(hasDarkBg, options, colors, revealElement, theVerticator); + } + } + + if (options.position == "auto") { + if (sectionState.rtl !== leftAligned) { + sectionState.rtl = leftAligned; + + if (leftAligned) { + theVerticator.style.left = options.offset; + theVerticator.style.right = "auto"; + theVerticator.classList.add('left'); + } else { + theVerticator.style.right = options.offset; + theVerticator.style.left = "auto"; + theVerticator.classList.remove('left'); + } + } + } + } + }); + }); + revealElementObserver.observe(revealElement, { + attributes: true, + attributeFilter: ['class'] + }); + deck.on('slidechanged', function (event) { + if (event.currentSlide.dataset.verticator) { + if (event.currentSlide.dataset.verticator == "regular") { + theVerticator.style.setProperty(forceColorVar, colors.verticatorregular); + + if (options.debug) { + console.log("Verticator forced to: \"".concat(colors.verticatorregular, "\"")); + } + } else if (event.currentSlide.dataset.verticator == "inverse") { + theVerticator.style.setProperty(forceColorVar, colors.verticatorinverse); + + if (options.debug) { + console.log("Verticator forced to: \"".concat(colors.verticatorinverse, "\"")); + } + } else { + theVerticator.style.setProperty(forceColorVar, deck.getCurrentSlide().dataset.verticator); + + if (options.debug) { + console.log("Verticator forced to: \"".concat(deck.getCurrentSlide().dataset.verticator, "\"")); + } + } + } else { + theVerticator.style.removeProperty(forceColorVar); + } + }); + deck.on('resize', function (event) { + revealScale = event.scale; + totalScale = revealScale > 1 ? revealScale * userScale : userScale; + theVerticator.style.setProperty('--verticator-scale', totalScale.toFixed(2)); + }); + + if (options.offset != '3vmin') { + theVerticator.style.right = options.offset; + } + + theVerticator.style.right = options.offset; + + if (options.position !== "auto") { + var opposite = options.position == "left" ? "right" : "left"; + theVerticator.style[options.position] = options.offset; + theVerticator.style[opposite] = "auto"; + + if (options.position == "left" && options.tooltip !== false) { + theVerticator.classList.add('left'); + } + } + + var selectionArray = function selectionArray(container, selectors) { + var selections = container.querySelectorAll(selectors); + var selectionarray = Array.prototype.slice.call(selections); + return selectionarray; + }; + + var clickBullet = function clickBullet(event) { + if (event.target.matches('.verticator li a')) { + var currIndexh = deck.getIndices().h; + var currIndexf = deck.getIndices().v; + var i = getNodeindex(event.target.parentNode); + event.preventDefault(); + deck.slide(currIndexh, i, currIndexf); + } + }; + + var activateBullet = function activateBullet(event) { + var listItems = selectionArray(theVerticator, 'li'); + var bestMatch = options.indexbase - 1; + listItems.forEach(function (listItem, i) { + if (parseInt(listItem.dataset.index) <= event.indexv + options.indexbase) { + bestMatch = i; + } + + listItem.classList.remove(activeclass); + }); + + if (bestMatch >= 0) { + listItems[bestMatch].classList.add(activeclass); + } + }; + + var ttName = function ttName(element) { + if (element.dataset.verticatorTooltip && (element.dataset.verticatorTooltip == "none" || element.dataset.verticatorTooltip == "false") || element.classList.contains('no-verticator-tooltip')) { + return; + } else if (options.tooltip != "auto" && element.getAttribute("".concat(options.tooltip))) { + return element.getAttribute("".concat(options.tooltip)); + } else if (options.tooltip == "auto") { + for (var _i = 0, _arr = ["data-verticator-tooltip", "data-name", "title"]; _i < _arr.length; _i++) { + var attr = _arr[_i]; + + if (element.getAttribute(attr)) { + return element.getAttribute(attr); + } + } + + for (var _i2 = 0, _arr2 = ["h1", "h2", "h3", "h4"]; _i2 < _arr2.length; _i2++) { + var slctr = _arr2[_i2]; + + if (element.querySelector(slctr)) { + return element.querySelector(slctr).textContent; + } + } + } else return false; + }; + + var createBullets = function createBullets(event, sections) { + theVerticator.classList.remove('visible'); + var listHtml = ''; + sections.forEach(function (section) { + var i = section[0]; + var tooltipname = section[1]; + var link = "href=\"#/".concat(event.indexh + options.indexbase, "/").concat(i + options.indexbase, "\""); + var dataname = tooltipname ? "data-name=\"".concat(tooltipname, "\"") : ''; + var tooltip = tooltipname ? "
    ".concat(tooltipname, "
    ") : ''; + listHtml += "
  • ").concat(tooltip, "
  • "); + }); + theVerticator.innerHTML = listHtml; + activateBullet(event); + setTimeout(function () { + theVerticator.classList.add('visible'); + }, 300); + }; + + var slideAppear = function slideAppear(event) { + var slide = event.currentSlide; + var parent = slide.parentNode; + var sections = Array.from(parent.children).map(function (elem, index) { + return [index, elem]; + }).filter(function (indexedElem) { + var issection = indexedElem[1].tagName == 'SECTION' && indexedElem[1].parentNode.tagName == 'SECTION'; + var isuncounted = options.skipuncounted && indexedElem[1].getAttribute('data-visibility') == 'uncounted'; + return issection && !isuncounted; + }).map(function (indexedElem) { + var ttname = ''; + + if (options.tooltip) { + ttname = ttName(indexedElem[1]); + } + + return [indexedElem[0], ttname]; + }); + + if (sections.length < 2) { + theVerticator.classList.remove('visible'); + theVerticator.innerHTML = ''; + } else { + if (event.previousSlide) { + var lastParent = event.previousSlide.parentNode; + + if (parent != lastParent) { + createBullets(event, sections); + } + } else { + createBullets(event, sections); + } + + activateBullet(event); + } + }; + + if (theVerticator) { + var eventnames = ['ready', 'slidechanged']; + eventnames.forEach(function (eventname) { + return deck.on(eventname, function (event) { + slideAppear(event); + }); + }); + + if (deck.getConfig().embedded) { + deck.on('click', function (event) { + clickBullet(event); + }); + } + } + }; + + var init = function init(deck) { + var es5Filename = "verticator.js"; + var defaultOptions = { + themetag: 'h1', + color: '', + inversecolor: '', + skipuncounted: false, + clickable: true, + position: 'auto', + offset: '3vmin', + autogenerate: true, + tooltip: false, + scale: 1, + csspath: '', + debug: false + }; + + var defaults = function defaults(options, defaultOptions) { + for (var i in defaultOptions) { + if (!options.hasOwnProperty(i)) { + options[i] = defaultOptions[i]; + } + } + }; + + var options = deck.getConfig().verticator || {}; + options.indexbase = deck.getConfig().hashOneBasedIndex ? 1 : 0; + defaults(options, defaultOptions); + + function pluginPath() { + var path; + var pluginScript = document.querySelector("script[src$=\"".concat(es5Filename, "\"]")); + + if (pluginScript) { + path = pluginScript.getAttribute("src").slice(0, -1 * es5Filename.length); + } else { + path = (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('verticator.js', document.baseURI).href)).slice(0, (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('verticator.js', document.baseURI).href)).lastIndexOf('/') + 1); + } + + return path; + } + + var VerticatorStylePath = options.csspath.verticator ? options.csspath.verticator : options.csspath ? options.csspath : "".concat(pluginPath(), "verticator.css") || 'plugin/verticator/verticator.css'; + + if (options.debug) { + console.log("Plugin path = ".concat(pluginPath())); + console.log("Verticator CSS path = ".concat(VerticatorStylePath)); + } + + var generator = document.querySelector('[name=generator]'); + + if (!(generator && generator.content.includes("quarto"))) { + loadStyle(VerticatorStylePath, 'verticatorstyle'); + } + + verticate(deck, options); + }; + + return { + id: 'verticator', + init: init + }; + }; + + return Plugin; + +})); diff --git a/website/slides/css/Insee_Commun.scss b/website/slides/css/Insee_Commun.scss new file mode 100644 index 0000000..ed8dd5e --- /dev/null +++ b/website/slides/css/Insee_Commun.scss @@ -0,0 +1,628 @@ +/*-- scss:defaults --*/ + +$font-family-sans-serif: "Open Sans", "Arial", "Helvetica" ; +$presentation-heading-font: $font-family-sans-serif; +$presentation-heading-color: #383d3d !default; +$h1-font-size: 2em; +$h2-font-size: 1.6em; +$h3-font-size: 1.3em; +$h4-font-size: 0.9em; +$h5-font-size: 0.6em; +$h6-font-size: 0.5em; + + +// colors +$body-bg: #f0f1eb !default; +$body-color: #000 !default; +$link-color: #3ea0e5 !default; +$selection-bg: #26351c !default; +$navigation-tools-color: #3ea0e5 !default; +$main-color-text: #0F417A !default; +$second-color-text: #5B8ED4 !default; +$yellow-color: #ffda5a !default; +$grey-color-endPage: #646363 !default; +$color-text-menu: #bebebe !default ; + +/*-- scss:rules --*/ + +/**************GENERIQUE*******************/ + +h1{ + font-size: $h1-font-size; +} + +.level2{ + h2{ + font-size: $h2-font-size ; +/* padding-top: 1%;*/ +} + + h3{ + font-size: $h3-font-size; + font-weight: normal; + border-left: solid 5px #268DFF; + padding-left: 40px; + } + + h4{ + font-size: $h4-font-size; + font-weight: normal; + border-left: solid 5px #9827B5; + padding-left: 40px; + } + + h5{ + font-size: $h5-font-size; + } + + h6{ + font-size: $h6-font-size; + } +} + + +li { + font-family: $font-family-sans-serif; + color: $main-color-text; +} + +p { + color: $body-color; +} + +.reveal { + .controls, .progress, .verticator { + color: $navigation-tools-color; + } + a { + line-height: 1.3em; + font-weight: bold; + } + a:hover { + text-decoration: underline; + color: #5a95cf; + } + a:active { + color: blueviolet; + } + h1, h4, h5, h6 { + color: $main-color-text; + } + h2 { + color: $body-color; + } + h3 { + color: $body-color; + } + h4 { + color: $body-color; + } +} + +.slide-number{ + top: 2%; + height: fit-content; + color: $body-color !important; + font-weight: bold; + font-size: 20px; +} + +.level2{ + ul { + li {//liste de niveau 1 + list-style-type: "– "; + font-size: 1em !important; + } + ul { + li {//liste de niveau 2 + list-style-type: disc; + font-size: 0.9em !important; + } + ul { + li {//liste de niveau 3 + list-style-type: "– "; + font-size: 0.8em !important; + } + ul{ + li{//liste de niveau 4 + list-style-type: disc; + font-size: 0.9em !important; + } + ul{ + li{//liste de niveau 5 + list-style-type: disc; + font-size: 0.9em !important; + } + ul{ + li{//liste de niveau 6 + list-style-type: disc; + font-size: 1em !important; + } + } + } + } + } + } + } +} + + +/***********premiere page**********/ + +p.date{ + font-size: 65%; + text-align: left; + position: absolute; + left: 0em; + top: 35em; + color: $second-color-text; +} + +.reveal h1 { + font-style: normal; + font-weight: normal; + text-align: left; +/* font-size: 1.5em;*/ +} + +#title-slide{ + hyphens: auto; + width: 100%; + height: 100%; + left: 2%; +} + +.reveal .title { + padding-top: 20%; + font-weight: bold; + color: $body-color; + width: 50%; + font-size: 2em; +} + +.reveal .subtitle { + text-align: left; + font-weight: bold; + padding-top: 2%; + color: $second-color-text; +} + +.slide-logo{ + max-height: 20% !important; + right: 5% !important; + bottom: -10px !important; +} + +.quarto-title-authors{ + position: relative; + left: -0.5em; + justify-content: left !important; +} + +.quarto-title-author{ + color: $main-color-text; +} + +.quarto-title-affiliation{ + text-align: left; + color: $main-color-text; +} + + +/***********Page Sommaire ************/ + +#custom-toc-slide { + height: 100% !important; + background-repeat: no-repeat; + background-size: 100% 100%; +} + +#custom-toc-slide h2{ + color: $body-color; + margin-top: 1%; + text-align: left; + padding-left: 8%; +} + +.card-number{ + text-align: left; + padding-left: 20%; + font-size: 235% !important; + position: relative; + min-height: 30px; /* Assure que l'icône a de l'espace */ + font-weight: bold; + font-size: 1.2em; + margin-bottom: 5px; +} + +.card-title{ + text-align: left; + font-size: xx-large; +} + + +/***********Page Titre ************/ + +div.backgroundTitre_bleu, div.backgroundTitre_violet, div.backgroundTitre_jaune, div.backgroundTitre_vert{ + max-width: 100%; + height: auto; + background-size: 100% 100%; + background-repeat: no-repeat; +} + +section.backgroundTitre_bleu h1 span.header-section-number, section.backgroundTitre_bleu h1 span.title-text{ + color: #0F417A !important; +} + +section.backgroundTitre_violet h1 span.header-section-number, section.backgroundTitre_violet h1 span.title-text{ + color: #5F1972 !important; +} + +section.backgroundTitre_jaune h1 span.header-section-number, section.backgroundTitre_jaune h1 span.title-text{ + color: #A36914 !important; +} + +section.backgroundTitre_vert h1 span.header-section-number, section.backgroundTitre_vert h1 span.title-text{ + color: #069F9C !important; +} + + + + +.level1 { +/* display: flex !important; + align-items: center; + justify-content: center;*/ + + h1 { + font-weight: bold; + /*text-decoration: underline;*/ + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + } + .header-section-number{ + font-size: 7em; + top: -0.28em; + position: relative; + left: .35em; + } + .title-text{ + font-size: 1.7em; + position: relative; + top: -1em; + display: block; + margin-top: 0; + } +} + + +h1:not(.title):not(:has(span)) { + font-size: 3em !important; + top: 2em; + position: relative; +} + +.logoTopRightTitre{ + width: 20%; + top: -40%; + position: absolute; + right: -2em; +} + + +/***********Page Standard************/ + +div.backgroundStandard, div.backgroundStandard_bleu, div.backgroundStandard_violet +,div.backgroundStandard_jaune, div.backgroundStandard_vert, div.backgroundStandard_multi { + background-repeat: no-repeat; + background-size: 100% 100%; +} + + +.level2 { + h2 { + margin-top: 6%; + } +} + +.enteteAvecTitre { + top: -5.5em; + position: relative; + text-align: center; + font-weight: bold; + text-transform: uppercase; + left: 6.5em; + width: 65%; + + p { + color: $grey-color-endPage; + } +} + +.enteteAvecTitreSur2lignes { + top: -6.5em; + position: relative; + text-align: center; + font-weight: bold; + text-transform: uppercase; + left: 6.5em; + width: 65%; + + p { + color: $grey-color-endPage; + } +} + +.logoTopLeftStandard{ + width: 15%; + top: -9%; + position: absolute; + left: -1em; +} + +/***********Page de fin************/ + +div.backgroundPageFinale { + background-repeat: round; + background-size: cover; +} + +.coordonneesFinales{ + position: relative; + top: 2em; + left: 1em; +} + +.coordonnees { + font-size: 0.7em; + margin-top: 2em; + + ul li { + color: white; + font-weight: bold; + list-style-type: none; + } +} + +.gauche{ + top: 8em; + position: relative; +} + +.milieu { + top: 17em; + position: relative; + left: 2em; +} + +.droite { + left: 5em; + position: relative; +} + +li:has(> span.ligneBlancheApres) { + margin-bottom: 3em !important; /* Espace après le
  • */ +} + + +/******************MENU****************/ + +.fa-sticky-note::before { + content: "\f249" !important; +} + +.fa-adjust::before { + content: "\f042" !important; +} + +.defaultCursor { + li { + cursor: default !important; + } + a { + cursor: default; + } +} + +.messageInfoMenu { + padding-bottom: 15px; + border-bottom: 2px dotted; + p { + color: $color-text-menu; + } + + ul { + padding-left: 5px; + li { + list-style-type: "- "; + color: $color-text-menu; + } + } +} + +/************FOOTER PRINCIPAL**************/ + +#footerPersonnalise { + position: fixed; + bottom: 18px; + width: 100%; + margin: 0 auto; + text-align: center; + font-size: 18px; + z-index: 2; + height: 40px; +} + +img.logo { + width: 40px !important; + height: 40px !important; + margin-bottom: -1% !important; +} + +img.logo.logo_insee{ + width: 70px !important; +} + +.slide-logo { + height: 50px !important; + width: unset !important; + max-width: unset !important; + max-height: unset !important; + margin-bottom: 2% !important; + margin-right: 1% !important; + image-resolution: from-image; +} + +.divCachee{ + display: none !important; +} + + +/***************WIDGETS*******************/ + +.plotly { + padding: 2%; +} + +/***************AUTRES*******************/ + +.aside-footnotes { + font-size: medium !important; + left: 9em; + position: relative; +} + +blockquote { + font-size: 50%; + font-style: italic !important; +} + +summary{ + cursor: pointer; +} + +/***************FENETRE MODALE*******************/ + +.taille_iframe iframe{ + height: 500px; +} + +/******************packages DT**********************/ + +.datatables, .cell-output-display { + font-size: 70%; +} + +.dataTables_length select { + font-size: 100%; +} + +.dt-buttons { + float: right !important; + padding: 0 5px; +} + + +/******************SCROLL VIEW MODE**********************/ + +.scroll-page-content #title-slide{ + left: 51% !important; +} + +.scroll-page-content .toc-cards { + left: 8%; + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + padding: 1rem; +} + +.scroll-page-content .toc-card { + flex: 0 0 calc(20% - 1rem); /* 5 cartes par ligne */ +/* background: #f8f9fa;*/ + background: none; + border-radius: 8px; + padding: 1rem; +/* box-shadow: 0 2px 4px rgba(0,0,0,0.1);*/ + transition: all 0.2s; + text-align: center; +} + +.scroll-page-content h1:not(.title) { + top: 3em; + position: relative; +} + +/*****FIN Scroll View Mode******/ + + + +/******************PDF EXPORT VIEW MODE**********************/ +/*CSS quand on tape sur la touche e*/ + +.pdf-page { + position: relative !important; +} + +.pdf-page #title-slide{ + left: 7% !important; +} + +.pdf-page>div.backgroundPageFinale{ + position: relative !important; +} + +html.print-pdf div#footerPersonnalise { + visibility: hidden; +} + + +.pdf-page p.date{ + top: 35em; +} + +.print-pdf .slide-logo{ + display: none !important; +} + +.pdf-page .logoTopRightTitre{ + display: none; +} + +.pdf-page .logoTopLeftStandard{ + display: none; +} + +/******************CSS quand on fait fichier>Imprimer... quand on est sur le PDF EXPORT VIEW MODE**********************/ + +@media print { + + .level2 h2 { + margin-top: 6%; + } + + [id^="chapters_"] { + top: 7em !important; + left: 2em; + position: relative !important; +} + + #pageDeFin{ + position: relative !important; + top: -28em !important; + } + + + .print_code details{ + display: contents !important; + } + + .no-print-pdf { + display: none !important; + } + +} + diff --git a/website/slides/css/default.css b/website/slides/css/default.css new file mode 100644 index 0000000..ddf1439 --- /dev/null +++ b/website/slides/css/default.css @@ -0,0 +1,668 @@ +/****************Positionnement des éléments*********************/ + +.titreTransparent h2{ + opacity: 0; +} + + +.rehausserContenuSansTitre { + margin-top: -7%; +} + +.rehausserContenuAvecTitre { + margin-top: -3%; +} + +/****************Taille de la police*********************/ + +.Large { + font-size: 144%; +} + +.large { + font-size: 120%; +} + +.small { + font-size: 90%; +} + +.footnotesize { + font-size: 80%; +} + +.scriptsize { + font-size: 70%; +} + +.tiny { + font-size: 60%; +} + +/********************Style du texte*********************/ + +.bold { + font-weight: bold; +} + +.bolder { + font-weight: 800; +} + +.italic { + font-style: italic; +} + + +/*couleurs par défaut*/ + +.black { + color: black; +} + +.red { + color: red; +} + +.blue { + color: blue; +} + +.green { + color: green; +} + +.yellow { + color: yellow; +} + +.orange { + color: orange; +} + +.purple { + color: purple; +} + +.gray { + color: gray; +} + +.grey { + color: gray; +} + + +/******************couleurs palettes Insee**********************/ + +.dot { + height: 55px; + width: 55px; + border-radius: 50%; + display: inline-block; + padding: 4px 3px 0 3px; + text-align: center; +} + +.nomPaletteCouleur { + text-align: center; + top: 50%; + font-size: 60%; +} + +.B6 { + color: white; + background-color: #0C3A5A; +} + +.B5 { + color: white; + background-color: #0F417A; +} + +.B4 { + color: white; + background-color: #286AC7; +} + +.B3 { + color: white; + background-color: #5B8ED4; +} + +.B2 { + color: black; + background-color: #8DB0E1; +} + +.B1demi { + color: black; + background-color: #AACAF1; +} + +.B1 { + color: black; + background-color: #C4E1FF; +} + +.J6 { + color: white; + background-color: #D98C07; +} + +.J5 { + color: white; + background-color: #E89E0B; +} + +.J4 { + color: white; + background-color: #FFC300; +} + +.J3 { + color: white; + background-color: #FBD616; +} + +.J2tiers { + color: black; + background-color: #ffda5a; +} + +.J2 { + color: black; + background-color: #FFE58E; +} + +.J1 { + color: black; + background-color: #FDF0C9; +} + +.G6 { + color: white; + background-color: #2D363F; +} + +.G5 { + color: white; + background-color: #646363; +} + +.G4 { + color: white; + background-color: #868686; +} + +.G3 { + color: white; + background-color: #B0AEAE; +} + +.G2 { + color: black; + background-color: #C6C6C6; +} + +.G1demi { + color: black; + background-color: #dadada; +} + +.G1 { + color: black; + background-color: #E8E8E8; +} + + +.R5 { + color: white; + background-color: #5D1212; +} + +.R4 { + color: white; + background-color: #AD1638; +} + +.R3 { + color: white; + background-color: #E4003A; +} + +.R2 { + color: white; + background-color: #EB617F; +} + +.R1demi { + color: white; + background-color: #F190A5; +} + +.R1 { + color: black; + background-color: #F5B8C2; +} + +.R0 { + color: black; + background-color: #fbdfe4; +} + + +.T5 { + color: white; + background-color: #1c4b5f; +} + +.T4 { + color: white; + background-color: #0C6876; +} + +.T3 { + color: white; + background-color: #008085; +} + +.T2 { + color: white; + background-color: #0AACAB; +} + +.T1demi { + color: white; + background-color: #71c3c7; +} + +.T1 { + color: black; + background-color: #97CFD0; +} + +.T0 { + color: black; + background-color: #c6e3e4; +} + +.P5 { + color: white; + background-color: #4f2742; +} + +.P4 { + color: white; + background-color: #633251; +} + +.P3 { + color: white; + background-color: #954577; +} + +.P2 { + color: white; + background-color: #B06293; +} + +.P1 { + color: black; + background-color: #CA8CB3; +} + +.Pdemi { + color: black; + background-color: #dcacca; +} + +.P0 { + color: black; + background-color: #e5c2da; +} + +.V5 { + color: white; + background-color: #036237; +} + +.V4 { + color: white; + background-color: #068043; +} + +.V3 { + color: white; + background-color: #1CA459; +} + +.V2 { + color: white; + background-color: #4BB375; +} + +.V1 { + color: black; + background-color: #8BC8A4; +} + +.Vdemi { + color: black; + background-color: #a2d3b6; +} + +.V0 { + color: black; + background-color: #c0e0ca; +} + + +.jaune_insee { + color: #fdc218; +} + +.rouge_insee { + color: #e4003a; +} + +.vert_insee { + color: #1ca459; +} + +.bleu_insee{ + color: #0f417a; +} + +.rougeBordeaux { + color: #610316; +} + +.souligneRouge{ + text-decoration-line: underline; + text-decoration-color: red; +} + +.bordureRouge { --mm-bordercolor: red;} + + +/****************Content-box***********************/ + +.content-box { + box-sizing: content-box; + background-color: #e2e2e2; +} + + +.content-box-blue, +.content-box-gray, +.content-box-grey, +.content-box-army, +.content-box-green, +.content-box-purple, +.content-box-red, +.content-box-yellow { + border-radius: 10px; + margin: 0 0 15px; + overflow: hidden; + padding: 5px; + width: 100%; +} + +.content-box-blue { + background-color: #F0F8FF; +} + +.content-box-gray { + background-color: #e2e2e2; +} + +.content-box-grey { + background-color: #F5F5F5; +} + +.content-box-army { + background-color: #737a36; +} + +.content-box-green { + background-color: #e2faec; + border: solid 2px #1ca459; + box-shadow: 5px 5px 5px #aaa; +} + +.content-box-purple { + background-color: #e2e2f9; +} + +.content-box-red { + background-color: #ffe1e9; + border: solid 2px #e4003a; + box-shadow: 5px 5px 5px #aaa; +} + +.content-box-yellow { + background-color: #fdf0c9; + border: solid 2px #fdc218; + box-shadow: 5px 5px 5px #aaa; +} + + +.full-width { + display: flex; + width: 100%; + flex: 1 1 auto; +} + +.pull-left-70 { float: left; width: 66.5%;} +.pull-right-30 { float: right; width: 28.5%; } +.pull-right-30 ~ * { clear: both; } + +.pull-left-60 { float: left; width: 57%; } +.pull-right-40 { float: right; width: 38%; } +.pull-right-40 ~ * { clear: both; } + +.pull-left-50 { float: left; width: 47.5%; } +.pull-right-50 { float: right; width: 47.5%;} +.pull-right-50 ~ * { clear: both; } + +.pull-left-40 { float: left; width: 38%; } +.pull-right-60 { float: right; width: 57%; } +.pull-right-60 ~ * { clear: both; } + +.pull-left-30 { float: left; width: 28.5%; } +.pull-right-70 { float: right; width: 66.5%; } +.pull-right-70 ~ * { clear: both; } + + +.listeSansPuceNiveau1 ul>li{ + list-style-type: none; +} + +.listeSansPuceNiveau2 ul>li>ul>li{ + list-style-type: none; +} + +.listeSansPuceNiveau3 ul>li>ul>li>ul>li { + list-style-type: none; +} + +.puceListFleche::before { + content: "\2192"; + color: #610316; + padding-right: 5px; +} + +.puceDiscNiveau1 ul li { + list-style-type: disc; +} + +.puceTiretNiveau2 ul li ul li{ + list-style-type: "- "; +} + +.interligne1 ul li:not(:last-child) { + margin-bottom: 1% !important; +} + +.interligne2 ul li:not(:last-child) { + margin-bottom: 2% !important; +} + +.interligne3 ul li:not(:last-child) { + margin-bottom: 3% !important; +} + +.interligne4 ul li:not(:last-child) { + margin-bottom: 3% !important; +} + +.interligne5 ul li:not(:last-child) { + margin-bottom: 5% !important; +} + +.interligne1 ol li:not(:last-child) { + margin-bottom: 1% !important; +} + +.interligne2 ol li:not(:last-child) { + margin-bottom: 2% !important; +} + +.interligne3 ol li:not(:last-child) { + margin-bottom: 3% !important; +} + +.interligne4 ol li:not(:last-child) { + margin-bottom: 3% !important; +} + +.interligne5 ol li:not(:last-child) { + margin-bottom: 5% !important; +} + + + + +.espacementHaut5{ + padding-top: 5%; + display: block; +} + +/***************IMAGES*******************/ + +.centerImage img{ + display: block; + margin-left: auto; + margin-right: auto; +} + +.imgWidth100 img{ + width: 100%; +} + +.imgWidth90 img{ + width: 90%; +} + +.imgWidth85 img{ + width: 85%; +} + +.imgWidth70 img{ + width: 70%; +} + +.imgWidth60 img{ + width: 60%; +} + +.imgWidth50 img{ + width: 50%; +} + +.play_icone::before { + content: ''; + display: block; + position: absolute; + background: url(../img/icones/play.svg) no-repeat center center; + width: 100%; + height: 100%; +} + +/***************WIDGETS*******************/ + +.centerWidget { + display: block; + margin-left: auto; + margin-right: auto; +} + +.widgetWidth100 { + width: 100% !important; +} + +.widgetWidth90 { + width: 90% !important; +} + +.widgetWidth85 { + width: 85% !important; +} + +.widgetWidth80 { + width: 80% !important; +} + +.widgetWidth70 { + width: 70% !important; +} + +.widgetWidth60 { + width: 60% !important; +} + +.widgetWidth50 { + width: 50% !important; +} + +.widgetWidth40 { + width: 40% !important; +} + +/***************AUTRES*******************/ + + +.reveal .example p { + border: 4px solid var(--code-full-bg); + background: none; + padding: 3vmin; + border-radius: 0.5vmin; + display: flex; + flex-direction: row; + gap: 1em; + justify-content: center; + align-items: center; + font-style: initial; + margin: 0; + +} + +.reveal .example img{ + width: 20em; +} + + +.center { + display: block; + margin-left: auto; + margin-right: auto; + width: 50%; +} + +@media print { + .no-print-pdf { + display: none !important; + } +} + diff --git a/website/slides/css/insee_clair.css b/website/slides/css/insee_clair.css new file mode 100644 index 0000000..3e0e88b --- /dev/null +++ b/website/slides/css/insee_clair.css @@ -0,0 +1,202 @@ + + +/*Sommaire*/ +#custom-toc-slide { + background-image: url(../_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche.svg); +} + + +.card-number-01::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-1.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-02::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-2.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-03::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-3.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-04::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-4.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-05::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-5.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-06::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-6.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-07::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-7.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-08::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-8.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-09::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-9.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-10::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-10.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-11::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-11.svg); + background-size: contain; + background-repeat: no-repeat; +} + +.card-number-12::before{ + content: ""; + position: absolute; + left: 0; + top: 0; + width: 70px; + height: 100px; + background-image: url(../_extensions/insee-clair/ressources/revealJs/elements_sommaire/Element-sommaire-12.svg); + background-size: contain; + background-repeat: no-repeat; +} + + +/*pages Titre*/ +div.backgroundTitre_bleu{ + background-image: url(../_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire1.svg); +} + +div.backgroundTitre_violet{ + background-image: url(../_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire2.svg); +} + +div.backgroundTitre_jaune{ + background-image: url(../_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire3.svg); +} + +div.backgroundTitre_vert{ + background-image: url(../_extensions/insee-clair/ressources/revealJs/2_Template-INSEE-Clair_Intercalaire4.svg); +} + + +/*Pages standard*/ +div.backgroundStandard { + background-image: url(../_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche.svg); +} + +div.backgroundStandard_bleu { + background-image: url(../_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle1.svg); +} + +div.backgroundStandard_violet { + background-image: url(../_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle2.svg); +} + +div.backgroundStandard_jaune { + background-image: url(../_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle3.svg); +} + +div.backgroundStandard_vert { + background-image: url(../_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle4.svg); +} + +div.backgroundStandard_multi { + background-image: url(../_extensions/insee-clair/ressources/revealJs/3_Template-INSEE-Clair_Page-blanche-cercle5.svg); +} + +/*Page finale*/ +div.backgroundPageFinale { + background-image: url(../_extensions/insee-clair/ressources/revealJs/6_Template-INSEE-Clair_Diapo-finale.svg); +} + + diff --git a/website/slides/css/stylePerso.css b/website/slides/css/stylePerso.css new file mode 100644 index 0000000..ada1324 --- /dev/null +++ b/website/slides/css/stylePerso.css @@ -0,0 +1,46 @@ +/**********************CSS PERSONNALISE****************************/ + +/*.reveal section { + border: 5px solid black; +}*/ + +.border { + border: solid 2px black; +} + +.titreAnimeAvant { + margin-top: 200px; + font-size: 2.5em; + font-weight: bold; + color: #0F417A; +} + +.titreAnimeApres { + color: #0F417A; + position: absolute; + top: -0.5em; + left: 20%; + text-align: left; + font-weight: bold; +} + +.logoPageFin { + width: 200px; + height: 200px; + bottom: 50px; + position: relative; +} + +#imageLogoInseeModal{ + width: 70%; + left: 15%; + position: relative; +} + +.width70{ + width: 70%; +} + +.width65{ + width: 65%; +} \ No newline at end of file diff --git a/website/slides/img/icones/play.svg b/website/slides/img/icones/play.svg new file mode 100644 index 0000000..91072ca --- /dev/null +++ b/website/slides/img/icones/play.svg @@ -0,0 +1,10 @@ + + + play + + + + + + + \ No newline at end of file diff --git a/website/slides/img/licences/cc/png/by-sa.png b/website/slides/img/licences/cc/png/by-sa.png new file mode 100644 index 0000000..8770732 Binary files /dev/null and b/website/slides/img/licences/cc/png/by-sa.png differ diff --git a/website/slides/img/licences/cc/svg/by-sa.svg b/website/slides/img/licences/cc/svg/by-sa.svg new file mode 100644 index 0000000..f850297 --- /dev/null +++ b/website/slides/img/licences/cc/svg/by-sa.svg @@ -0,0 +1,199 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/slides/js/include.js b/website/slides/js/include.js new file mode 100644 index 0000000..87d0aff --- /dev/null +++ b/website/slides/js/include.js @@ -0,0 +1,29 @@ + +function includeHTML() { + var z, i, elmnt, file, xhttp; + /*loop through a collection of all HTML elements:*/ + z = document.getElementsByTagName("*"); + for (i = 0; i < z.length; i++) { + elmnt = z[i]; + /*search for elements with a certain atrribute:*/ + file = elmnt.getAttribute("w3-include-html"); + if (file) { + /*make an HTTP request using the attribute value as the file name:*/ + xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4) { + if (this.status == 200) {elmnt.innerHTML = this.responseText;} + if (this.status == 404) {elmnt.innerHTML = "Page not found.";} + /*remove the attribute, and call this function once more:*/ + elmnt.removeAttribute("w3-include-html"); + includeHTML(); + } + }; + xhttp.open("GET", file, true); + xhttp.send(); + /*exit the function:*/ + return; + } + } +} + \ No newline at end of file diff --git a/website/slides/js/miseEnPage.js b/website/slides/js/miseEnPage.js new file mode 100644 index 0000000..25a405a --- /dev/null +++ b/website/slides/js/miseEnPage.js @@ -0,0 +1,80 @@ + +/*Gestion de l'apparition du footer et du nom de la réunion*/ + + var $footerPerso = document.getElementById('footerPersonnalise'); + var $pageDebut = document.getElementById('title-slide'); + var $pageFin = document.getElementById('pageDeFin'); + var $toc = document.getElementById("custom-toc-slide"); + var $logoParDefault = document.getElementsByClassName("slide-logo"); + var $header = document.getElementsByClassName("reveal-header"); + + /*reveal-header*/ + + Reveal.on( 'slidechanged', event => { + + /*gestion footer*/ + + if (!$footerPerso.classList.contains("divCachee")) { + $footerPerso.classList.add("divCachee"); + } + + if ($pageDebut.classList.contains("present") + || $pageFin.classList.contains("present")) { + $footerPerso.classList.remove("divCachee"); + } + + /*gestion logo*/ + + if (!$logoParDefault[0].classList.contains("divCachee")) { + $logoParDefault[0].classList.add("divCachee"); + } + + if ($pageDebut.classList.contains("present") + || $pageFin.classList.contains("present")) { + $logoParDefault[0].classList.remove("divCachee"); + } + + }); + + +// modif_number_titre1 +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('h1 .header-section-number').forEach(function(el) { + let num = el.textContent.trim(); + if (num.length === 1) { + el.textContent = '0' + num; + } + }); +}); + + +// modif_libellé_titre1 +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.reveal h1').forEach(function(h1) { + // On cherche le numéro de section (supposé être le premier enfant ou dans un .slide-number) + let numberElement = h1.querySelector('.header-section-number'); + let numberText = numberElement ? numberElement.textContent.trim() : ''; + let fullText = h1.textContent.trim(); + + // Si le numéro est présent, on le sépare du reste du titre + if (numberElement) { + // On extrait le libellé du titre (après le numéro) + let titleText = fullText.replace(numberText, '').trim(); + + // On vide le h1 + h1.innerHTML = ''; + + // On ajoute le numéro (déjà dans un .slide-number) + h1.appendChild(numberElement); + + // On ajoute un saut de ligne (br) pour séparer visuellement + h1.appendChild(document.createElement('br')); + + // On enveloppe le libellé dans une span avec la classe title-text + let titleSpan = document.createElement('span'); + titleSpan.className = 'title-text'; + titleSpan.textContent = titleText; + h1.appendChild(titleSpan); + } + }); +}); \ No newline at end of file diff --git a/website/slides/modulesCompl/customMenu.html b/website/slides/modulesCompl/customMenu.html new file mode 100644 index 0000000..f9b3487 --- /dev/null +++ b/website/slides/modulesCompl/customMenu.html @@ -0,0 +1,21 @@ + + + + +
    +

    Contrairement aux autres rubriques dans le menu, ces raccourcis sont à titre informatif, c'est-à-dire :

    +
      +
    • non cliquables dans le menu
    • +
    • utilisables directement sur les slides
    • +
    +
    + +
    +

    Il est possible d'utiliser la barre d'espace ou les flèches directionnelles pour naviguer dans le support. Pour plus d'information, consulter la rubrique d'aide en cliquant sur "?".

    +

    Il est possible de faire un zoom sur la partie d'une slide en cliquant sur alt+clique gauche de la souris. Il faut refaire la même chose pour dézoomer.

    +
    \ No newline at end of file diff --git a/website/slides/modulesCompl/footer.html b/website/slides/modulesCompl/footer.html new file mode 100644 index 0000000..9a7b96b --- /dev/null +++ b/website/slides/modulesCompl/footer.html @@ -0,0 +1,17 @@ + + +
    +

    + + + + + + + + +

    +
    + + + diff --git a/website/slides/presentation-generale.qmd b/website/slides/presentation-generale.qmd new file mode 100644 index 0000000..48d5ef7 --- /dev/null +++ b/website/slides/presentation-generale.qmd @@ -0,0 +1,397 @@ +--- +title: "Évaluation automatique de la dictée" +subtitle: "Développer un outil IA pour aider à l'évaluation des écrits d'élèves" +author: "Laura Gaimard" +institute: "SSP Lab (Insee) × DEPP" +date: today +date-format: "DD/MM/YYYY" +description: | + Présentation générale du projet d'évaluation automatique de la dictée : + données d'entrée, choix de conception, pipeline et résultats. + +lang: fr + +slide-tone: false # lecture d'écran +chalkboard: false # touche B pour activer le tableau blanc +jump-to-slide: true # touche G puis numéro de slide +mouse-wheel: false +transition: slide + +custom-toc: true + +format: + insee-clair-revealjs: + output-file: presentation-generale.html + +revealjs-plugins: + - pointer + - confetti + - verticator + - multimodal +--- + +# Les données d'entrée {#chapitre_inputs .backgroundTitre_bleu} + +## De quoi part-on ? {#inputs_vue_ensemble .backgroundStandard_bleu} + +:::{.enteteAvecTitre} +Les données d'entrée +::: + +::::{.rehausserContenuAvecTitre} + +Dans un premier temps : analyse des copies de dictées seulement (dictée 2015). +Pourquoi ? : car le texte de référence est connu à l'avance, ce qui facilite l'évaluation + +Trois objets alimentent la chaîne de traitement : + +:::{.listeSansPuceNiveau1} +- [**L'image de la copie** : l'imagette de la dictée manuscrite d'un élève de CM2]{.puceListFleche} +- [**Le texte de référence** : la dictée telle qu'elle aurait dû être écrite]{.puceListFleche} +- [**La grille de codage** : pour chaque item, le mot attendu et les fautes connues associées]{.puceListFleche} +::: + +Un **item** = un mot ou un signe de ponctuation. La dictée 2015 en compte **83**, +sur **3 469 imagettes**, toutes annotées par un expert. + +:::: + +## Le corpus {#inputs_corpus .backgroundStandard_bleu} + +:::{.enteteAvecTitre} +Les données d'entrée +::: + +::::{.rehausserContenuAvecTitre} + +| | Dictée **2015** | Dictée 2021 | +|---|:--:|:--:| +| Niveau | CM2 | CM2 | +| Imagettes disponibles | **3 469** | 6 793 | +| Copies annotées par un expert | **3 469** | 6 793 | +| Items par copie | 83 | 83 | +| Codes experts au total | **287 728** | 563 819 | + +: Le corpus « dictée » — les travaux portent pour l'instant sur **2015** {.striped} + +:::{.listeSansPuceNiveau1} +- [**Toutes** les imagettes 2015 sont annotées : appariement 1 pour 1, aucune image sans code, aucun code sans image]{.puceListFleche} +::: + +:::: + +::: {.notes} +Chiffres relevés sur le bucket S3 du projet (`dictee_2015/` et +`resultat_dictee_2015.csv`). 3 466 copies sur 3 469 ont les 83 items renseignés. +::: + +## Point de vigilance : le format des images {#inputs_images .backgroundStandard_bleu} + +:::{.enteteAvecTitre} +Les données d'entrée +::: + +::::{.rehausserContenuAvecTitre} + +:::{.listeSansPuceNiveau1} +- [Les fichiers `.png` fournis sont en réalité des **TIFF bi-level 1 bit** (compression G4, 1594 × 2044)]{.puceListFleche} +- [Chaque pixel est **noir ou blanc** : aucune nuance de gris]{.puceListFleche} +- [Conséquence : les **accents** se confondent avec le bruit et deviennent difficiles à lire]{.puceListFleche} +- [Le chargement (`data/loaders.py`) **normalise systématiquement** le format en entrée]{.puceListFleche} +::: + +:::: + +# Les choix effectués {#chapitre_choix .backgroundTitre_vert} + +## Choix n°1 — Une grille de codage simplifiée {#choix_grille .backgroundStandard_vert} + +:::{.enteteAvecTitre} +Les choix effectués +::: + +::::{.rehausserContenuAvecTitre} + +:::: {.columns} + +::: {.column width="48%"} +**Grille complète (6 codes)** + +| Code | Signification | +|:--:|---| +| 1 | Orthographe correcte | +| 3 | Erreur lexicale | +| 4 | Erreur grammaticale | +| 5 | Lexicale **et** grammaticale | +| 0 | Mot absent | +::: + +::: {.column width="48%"} +**Grille simplifiée (cible principale)** + +| Code | Signification | +|:--:|---| +| 1 | Orthographe correcte | +| 9 | Erreur (3, 4 ou 5 regroupés) | +| 0 | Mot absent | +::: + +:::: + +**Pourquoi ?** Sur le test pilote, le regroupement 3/4/5 fait passer le kappa de +**0,785 à 0,879** : il supprime exactement la zone où les correcteurs humains +divergent le plus entre eux. + +:::: + +## Choix n°2 — Deux approches comparées {#choix_approches .backgroundStandard_vert} + +:::{.enteteAvecTitre} +Les choix effectués +::: + +::::{.rehausserContenuAvecTitre} + +:::: {.columns} + +::: {.column width="48%"} +### End-to-end *(par défaut)* + +Un modèle multimodal reçoit image + référence + grille et produit **directement** +le code de chaque item. + +- Un seul appel : simple, rapide +- Risque de **sur-correction** +::: + +::: {.column width="48%"} +### Deux étapes + +1. **HTR** : transcrire l'image en texte brut, fautes comprises +2. **Codage** : comparer ce texte à la référence + +- Transcription **inspectable** +- Deux sources d'erreur cumulées +::: + +:::: + +Les deux implémentent la **même interface** `Scorer` → évaluées par **exactement +le même code de métriques**. On compare bien l'architecture, pas deux tuyaux +différents. + +:::: + +## Choix n°3 — Conception du code {#choix_code .backgroundStandard_vert} + +:::{.enteteAvecTitre} +Les choix effectués +::: + +::::{.rehausserContenuAvecTitre} + +:::{.listeSansPuceNiveau1} +- [**Tout passe par la configuration** : un fichier YAML validé (Pydantic) = un run reproductible. Aucun chemin en dur.]{.puceListFleche} +- [**Un score de confiance par item** : l'objectif opérationnel est de ne renvoyer vers un correcteur humain que les items incertains.]{.puceListFleche} +- [**Règle des ratures** (tranchée par la DEPP) : on lit l'**état final**, la version corrigée par l'élève. Consigne présente dans tous les prompts.]{.puceListFleche} +- [**Modèles open weight** servis sur le SSP Cloud (vLLM / `llm.lab`) : les données ne sortent pas.]{.puceListFleche} +::: + +:::: + +::: {.notes} +Insister sur le livrable décisionnel : la courbe « taux de renvoi humain +vs taux d'erreur résiduel ». +::: + +# Le pipeline {#chapitre_pipeline .backgroundTitre_violet} + +## Vue d'ensemble {#pipeline_vue .backgroundStandard_violet} + +:::{.enteteAvecTitre} +Le pipeline +::: + +::::{.rehausserContenuAvecTitre} + +```{mermaid} +flowchart LR + A["Image
    (S3)"] --> S["Scorer"] + R["Texte de référence
    + grille"] --> S + S --> P["Codes prédits
    + confiance"] + P --> N["Normalisation
    au schéma de codage"] + E["Codes experts"] --> N + N --> M["Métriques"] +``` + +Un *Scorer* expose une seule méthode : `score_copy(copie, référence) → prédictions`. +`VLMScorer` (end-to-end) et `TwoStageScorer` produisent la même sortie. + +:::: + +## Le déroulé d'un run {#pipeline_run .backgroundStandard_violet} + +:::{.enteteAvecTitre} +Le pipeline +::: + +::::{.rehausserContenuAvecTitre} + +Orchestré par `pipeline/benchmark.py` : + +1. **Charger les données** — imagettes depuis S3 et CSV des codes experts +2. **Charger la grille** — mot attendu par item et texte de référence +3. **Coder chaque copie** via le `Scorer` +4. **Normaliser les codes** des **deux** côtés (modèle et expert), même échelle +5. **Écrire les prédictions** au fil de l'eau dans un `.jsonl` +6. **Calculer les métriques** + +:::: + +## Robustesse et traçabilité {#pipeline_robustesse .backgroundStandard_violet} + +:::{.enteteAvecTitre} +Le pipeline +::: + +::::{.rehausserContenuAvecTitre} + +:::: {.columns} + +::: {.column width="48%"} +### Survivre aux incidents + +Un run complet dure ~30 h (3 469 copies × ~30 s). + +- Écriture après chaque copie (`flush + fsync`) +- **Reprise automatique** : les copies déjà traitées sont sautées +- Les erreurs d'API sont journalisées, le run continue +::: + +::: {.column width="48%"} +### Suivi des expériences + +- **Langfuse** (inférence) : session → trace (une copie) → génération (un appel) +- **MLflow** (fine-tuning GPU) : courbes d'entraînement +::: + +:::: + +:::: + +# Les résultats {#chapitre_resultats .backgroundTitre_jaune} + +## Ce que l'on mesure {#resultats_metriques .backgroundStandard_jaune} + +:::{.enteteAvecTitre} +Les résultats +::: + +::::{.rehausserContenuAvecTitre} + +| Famille | Métriques | +|---|---| +| **Codage** | Accord brut, kappa de Cohen, QWK, matrice de confusion, F1 pondéré | +| **Transcription (HTR)** | CER, WER, taux d'illisibles, **taux de sur-correction** | +| **Confiance** | Courbe de fiabilité, ECE | +| **Référence** | Variabilité inter-codeurs humains (Dawid & Skene) | + +: La grille de métriques commune {.striped} + +Le point de comparaison n'est pas « 100 % » mais **l'accord entre correcteurs +humains**. + +:::: + +## Résultats — codage {#resultats_codage .backgroundStandard_jaune} + +:::{.enteteAvecTitre} +Les résultats +::: + +::::{.rehausserContenuAvecTitre} + +| Approche | Copies | Accord brut | Kappa | F1 pondéré | +|---|--:|--:|--:|--:| +| End-to-end | — | — | — | — | +| Deux étapes | — | — | — | — | +| *Référence humaine* | — | — | — | — | + +: À compléter à l'issue du benchmark {.striped} + +::: {.callout-note} +Les tableaux de la page [Résultats](../resultats.qmd) du site sont calculés +automatiquement à partir des prédictions exportées sur S3. Le seul run disponible +à ce jour est un échantillon de démonstration (~20 copies), **non représentatif**. +::: + +:::: + +## Résultats — transcription et confiance {#resultats_htr .backgroundStandard_jaune} + +:::{.enteteAvecTitre} +Les résultats +::: + +::::{.rehausserContenuAvecTitre} + +:::: {.columns} + +::: {.column width="48%"} +### HTR + +| Modèle | CER | WER | Sur-correction | +|---|--:|--:|--:| +| Base | — | — | — | +| Fine-tuné | — | — | — | +::: + +::: {.column width="48%"} +### Renvoi humain + +Le livrable décisionnel : la courbe **taux de renvoi humain vs taux d'erreur +résiduel**. + +*Graphique à insérer.* +::: + +:::: + +:::: + +## Prochaines étapes {#resultats_suite .backgroundStandard_jaune} + +:::{.enteteAvecTitre} +Les résultats +::: + +::::{.rehausserContenuAvecTitre} + +:::{.listeSansPuceNiveau1} +- [Constituer le jeu d'évaluation « gold » (200–300 copies)]{.puceListFleche} +- [Lancer le benchmark HTR sur la dictée]{.puceListFleche} +- [Lancer le benchmark de codage (end-to-end vs deux étapes)]{.puceListFleche} +- [Fine-tuning LoRA sur les ~3 000 copies annotées]{.puceListFleche} +- [Étendre à la production écrite libre (pas de texte de référence)]{.puceListFleche} +::: + +:::: + +## {#pageDeFin data-menu-title="Page finale" .unnumbered .backgroundPageFinale} + +:::: {.columns .coordonneesFinales} + +::: {.column .gauche .coordonnees width=30%} +- Laura Gaimard +- [SSP Lab]{.ligneBlancheApres} +- [Insee]{.ligneBlancheApres} +- laura.gaimard@insee.fr +::: + +::: {.column .coordonnees .milieu width=30%} +::: + +::: {.column .coordonnees .droite width=30%} +::: + +:::: diff --git a/website/style/listing-grid.ejs b/website/style/listing-grid.ejs new file mode 100644 index 0000000..87df6b2 --- /dev/null +++ b/website/style/listing-grid.ejs @@ -0,0 +1,99 @@ +```{=html} + +<% +// Number of columns +const cols = 3; + +// Card alignment +const align = "left"; + +// Borders +const hideBorders = false; + +// Cap Options +const imgHeight = "100%"; +%> + + + +``` \ No newline at end of file diff --git a/website/style/styles.scss b/website/style/styles.scss new file mode 100644 index 0000000..1fc2866 --- /dev/null +++ b/website/style/styles.scss @@ -0,0 +1,113 @@ +/*-- scss:rules --*/ + +/* Grille de cartes de la page « Présentations » (listing-grid.ejs) */ + +.grid-item-link { + color: inherit; + text-decoration: none; + transition: transform 0.2s ease-in-out; +} + +.grid-item-link:hover { + transform: scale(1.03); +} + +.no-anchor.card-title.listing-title { + font-weight: 600 !important; +} + +.listing-categories .listing-category .listing-href { + text-decoration: none; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Bandeaux de codage item par item de la page « Écarts IA – humain ». + Une « puce » = un item de la dictée : mot attendu, code attribué, et un détail + optionnel (code de l'expert, mot réellement lu par le modèle). + Convention de couleur : VERT = accord modèle/expert, ROUGE = désaccord. + ───────────────────────────────────────────────────────────────────────────── */ + +.bandeau-puces { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin: 0.6rem 0 1.2rem; +} + +.puce { + display: inline-flex; + flex-direction: column; + align-items: center; + padding: 3px 6px; + border: 1px solid #ccc; + border-radius: 5px; + min-width: 28px; + line-height: 1.25; + cursor: help; +} + +.puce-mot { + font-size: 0.85rem; +} + +.puce-code { + font-size: 0.68rem; + font-weight: 700; + color: #444; +} + +.puce-detail { + font-size: 0.62rem; + color: #7a1020; + font-style: italic; + max-width: 11rem; + text-align: center; +} + +/* Bandeau du modèle : l'accord avec l'expert donne la couleur. */ +.puce-accord { + background: #d9efd9; + border-color: #8bc48b; +} + +.puce-desaccord { + background: #f8d7da; + border-color: #c96a72; +} + +/* Bandeau de l'expert : la couleur porte le code, pas l'accord. */ +.puce-code-1 { + background: #f6f6f6; +} + +.puce-code-9 { + background: #fff3cd; +} + +.puce-code-0 { + background: #e2e2e2; +} + +/* Code hors grille simplifiée (vide, « i », « v » — héritage de la saisie AGATE). */ +.puce-code-autre { + background: #ede7f6; +} + +/* Item où le modèle de référence diverge de l'expert. */ +.puce-divergente { + border-color: #b00020; + border-width: 2px; +} + +.legende-puces { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin: 1rem 0; + font-size: 0.85rem; +} + +.legende-puces .puce { + min-width: 0; +}