#import "@preview/polylux:0.4.0": *
#import "@preview/cetz:0.5.0"
#import "@preview/cetz-plot:0.1.3": plot
#import "@preview/cetz:0.5.0": canvas

#set math.mat(delim: "[")

// Infos
#let auteur = "Anthony PERRONI"
#let numero = "49871"
#let titre = "Codes LDPC"
#let annee = "2025 - 2026"

// Template
#let myslide(partie, contenu) = slide[
  // Header
  #block(width: 100%, fill: black, inset: (top: 0.6cm, bottom: 0.6cm, left: 1.5cm, right: 1.5cm))[
    #set text(fill: white, size: 28pt, weight: "bold")
    #partie
  ]
  // Contenu
  #pad(x: 1.5cm, top: 0.2cm)[
    #set text(size: 20pt)
    #contenu
  ]

  // Footer
  #v(1fr)
  #context {
    let cur = counter("logical-slide").get().first()
    let fins = query(<fin>)
    let tot = if fins.len() > 0 {
      counter("logical-slide").at(fins.first().location()).first()
    } else {
      counter("logical-slide").final().first()
    }

    if cur > 1 {
      block(width: 100%, fill: black, inset: (top: 0.2cm, bottom: 0.2cm, left: 1.5cm, right: 1.5cm))[
        #set text(fill: white, size: 12pt, weight: "bold")
        #grid(
          columns: (1.5fr, 2fr, 1fr, auto),
          align: (left, center, center, right),
          [#auteur n°#numero], [#titre], [#annee], if cur <= tot [#(cur - 1) / #(tot - 1)] else [Annexe],
        )
      ]
    }
  }
]

// Plan
#let design_plan(items) = {
  let r = 6pt
  let epaisseur = 2pt
  let hauteur_ligne = 2.5em
  let espace_vertical = 20pt

  pad(left: 1cm, top: 1cm)[
    #block[
      #place(dx: r, dy: hauteur_ligne / 2)[
        #line(
          length: (items.len() - 1) * (hauteur_ligne + espace_vertical),
          angle: 90deg,
          stroke: epaisseur + black,
        )
      ]

      #grid(
        columns: (2 * r, auto),
        column-gutter: 20pt,
        row-gutter: espace_vertical,
        ..items
          .map(item => (
            box(height: hauteur_ligne, align(center + horizon)[
              #circle(radius: r, fill: white, stroke: epaisseur + black)
            ]),
            box(height: hauteur_ligne, align(left + horizon)[
              #text(weight: "bold", size: 1.1em)[#item]
            ]),
          ))
          .flatten()
      )
    ]
  ]
}

// Graphe de Tanner en filigrane
#let graphe_tanner_fond(taille, spread) = {
  cetz.canvas(length: taille, {
    import cetz.draw: *

    let n_var = 18
    let n_chk = 9
    let var_y = 11
    let chk_y = -8

    // Calcul automatique pour que le graphe soit centré sur X=0
    let dist_v = (n_var - 1) * spread
    let v_start = -dist_v / 2
    let vpos = range(n_var).map(i => (v_start + i * spread, var_y))

    let spread_c = dist_v / (n_chk - 1)
    let c_start = -dist_v / 2
    let cpos = range(n_chk).map(i => (c_start + i * spread_c, chk_y))

    let edges = (
      (0, 0),
      (0, 4),
      (0, 8),
      (1, 1),
      (1, 5),
      (1, 0),
      (2, 2),
      (2, 6),
      (2, 1),
      (3, 3),
      (3, 7),
      (3, 2),
      (4, 4),
      (4, 8),
      (4, 3),
      (5, 5),
      (5, 0),
      (5, 4),
      (6, 6),
      (6, 1),
      (6, 5),
      (7, 7),
      (7, 2),
      (7, 6),
      (8, 8),
      (8, 3),
      (8, 7),
      (9, 0),
      (9, 3),
      (9, 6),
      (10, 1),
      (10, 4),
      (10, 7),
      (11, 2),
      (11, 5),
      (11, 8),
      (12, 0),
      (12, 5),
      (12, 7),
      (13, 1),
      (13, 3),
      (13, 8),
      (14, 2),
      (14, 4),
      (14, 6),
      (15, 0),
      (15, 4),
      (15, 7),
      (16, 1),
      (16, 5),
      (16, 8),
      (17, 2),
      (17, 3),
      (17, 6),
    )

    let darken_ration = 20%

    for e in edges {
      let vp = vpos.at(e.at(0))
      let cp = cpos.at(e.at(1))
      line((vp.at(0), vp.at(1)), (cp.at(0), cp.at(1)), stroke: 0.45pt + rgb("#e0e0e0").darken(darken_ration))
    }

    for p in vpos {
      circle((p.at(0), p.at(1)), radius: 0.25, fill: white, stroke: 0.8pt + rgb("#cccccc").darken(darken_ration))
    }

    for p in cpos {
      let (x, y) = p
      rect(
        (x - 0.25, y - 0.25),
        (x + 0.25, y + 0.25),
        fill: white,
        stroke: 0.8pt + rgb("#cccccc").darken(darken_ration),
      )
    }
  })
}

// Schema Shannon
// #let canal_shannon_intro() = {
//   cetz.canvas({
//     import cetz.draw: *
//
//     // Styles
//     let s = (stroke: 2pt + black)
//     let s_fleche = (stroke: 2pt + black)
//     let pointe_pleine = (end: "stealth", fill: black)
//
//     let espacement = 5.5
//
//     // Blocs
//     content((0, 0), [Source], name: "src", frame: "rect", ..s, padding: .3)
//     content((espacement, 0), [Emetteur], name: "em", frame: "rect", ..s, padding: .3)
//     content((espacement * 2, 0), [Canal], name: "chan", frame: "rect", ..s, padding: .3)
//     content((espacement * 3, 0), [Récepteur], name: "rec", frame: "rect", ..s, padding: .3)
//     content((espacement * 4, 0), [Destinataire], name: "dest", frame: "rect", ..s, padding: .3)
//
//     // Bruit
//     content((rel: (0, 1.8), to: "chan"), [Bruit], name: "bruit", frame: "rect", ..s, padding: .3)
//
//     // Codage / Décodage
//     content((rel: (0, 1), to: "em"), [*Codage*])
//     content((rel: (0, 1), to: "rec"), [*Décodage*])
//
//     // Flèches (On utilise s_fleche et pointe_pleine)
//     line("src.east", "em.west", mark: pointe_pleine, ..s_fleche)
//     line("em.east", "chan.west", mark: pointe_pleine, ..s_fleche)
//     line("chan.east", "rec.west", mark: pointe_pleine, ..s_fleche)
//     line("rec.east", "dest.west", mark: pointe_pleine, ..s_fleche)
//     line("bruit.south", "chan.north", mark: pointe_pleine, ..s_fleche)
//
//     // Annotation
//     let note-style = (size: 0.75em, style: "italic")
//     content((espacement * 0.5, -0.5), text(..note-style)[Message])
//     content((espacement * 1.5, -0.5), text(..note-style)[Signal])
//     content((espacement * 2.5, -0.5), text(..note-style)[Signal])
//     content((espacement * 3.5, -0.5), text(..note-style)[Message])
//   })
// }

#let canal_shannon_intro() = {
  // Couleurs
  let col-u = blue // Signal propre
  let col-p = orange // Signal bruité
  let C_MAIN = black
  let C_BG = rgb("#f8f9fa") // Le gris très clair demandé
  let C_LABEL = black

  cetz.canvas(length: 1cm, {
    import cetz.draw: *

    let x_left = 4.0
    let x_right = 16.0
    let x_noise = 22.5
    let y_top = 14.5
    let y_mid = 9.0
    let y_bot = 3.5

    // Style des lignes avec flèches
    let s_ligne = (stroke: 1.5pt + C_MAIN, mark: (end: "stealth", fill: C_MAIN, size: 0.25))
    let s_bruit_fleche = (
      stroke: (paint: black, thickness: 1.5pt, dash: "dashed"),
      mark: (end: "stealth", fill: black, size: 0.25),
    )

    // Fonction de bloc avec le nouveau fond
    let bloc(pos, txt, stxt, n) = {
      content(
        pos,
        [#set align(center); #text(C_MAIN, weight: "bold", size: 1.2em)[#txt] \ #text(
            C_MAIN,
            size: 0.85em,
            style: "italic",
          )[#stxt]],
        frame: "rect",
        fill: C_BG,
        stroke: C_MAIN + 1.5pt,
        padding: 0.7,
        radius: 0.3,
        name: n,
      )
    }

    // --- ONDES (Bruit dosé : "tremblement" au lieu de "chaos") ---
    let wave_vert(x, y_start, y_end, is_noisy) = {
      let n_points = 200
      let color = if is_noisy { col-p } else { col-u }

      line(
        ..range(n_points + 1).map(i => {
          let t = i / n_points
          let y = y_start + t * (y_end - y_start)

          // Enveloppe pour raccord propre
          let env = if t < 0.05 { t / 0.05 } else if t > 0.95 { (1 - t) / 0.05 } else { 1.0 }

          let signal = 0.35 * calc.sin(t * 6 * calc.pi)

          // Bruit dosé : on a baissé les fréquences et l'amplitude
          let bruit = if is_noisy {
            (0.08 * calc.sin(t * 45 * calc.pi) + 0.05 * calc.cos(t * 97 * calc.pi))
          } else { 0 }

          (x + (signal + bruit) * env, y)
        }),
        stroke: color + 1.6pt,
      )
    }

    wave_vert(x_right, y_top - 1.5, y_mid + 1, false) // Bleu
    wave_vert(x_right, y_mid - 1, y_bot + 1.5, true) // Orange

    // --- BLOCS ---
    bloc((x_left, y_top), "Source", "Information", "src")
    bloc((x_right, y_top), "Émetteur", "Codage", "em")
    bloc((x_right, y_mid), "Canal", "", "canal")
    bloc((x_noise, y_mid), "Bruit", "", "bruit")
    bloc((x_right, y_bot), "Récepteur", "Décodage", "rec")
    bloc((x_left, y_bot), "Destinataire", "Information", "dest")

    // --- CONNEXIONS ---
    line("src.east", "em.west", ..s_ligne)
    line("bruit.west", "canal.east", ..s_bruit_fleche)
    line("rec.west", "dest.east", ..s_ligne)

    // --- ANNOTATIONS ---
    let lab(pos, body, color: C_LABEL, anchor: "center") = content(
      pos,
      text(size: 0.95em, fill: color, style: "italic", weight: "medium")[#body],
      anchor: anchor,
    )

    lab(((x_left + x_right) / 2 - 0.3, y_top + 0.7), "Message")
    lab((x_right + 1.2, (y_top + y_mid) / 2), "Signal", color: col-u, anchor: "west")
    lab((x_right + 1.2, (y_mid + y_bot) / 2), "Signal + Bruit", color: col-p, anchor: "west")
    lab(((x_left + x_right) / 2 + 0.3, y_bot - 0.7), "Message reçu")
  })
}

#let plongement_schema() = {
  cetz.canvas(length: 1.5cm, {
    import cetz.draw: *

    // Styles
    let style-pointille = (stroke: (paint: black, thickness: 1.2pt, dash: "dashed"))
    let style-point = (fill: black, stroke: none)
    let rayon-point = 0.08
    let pointe-fleche = (end: "stealth", fill: black)

    let m = (
      p00: (-4, 0),
      p01: (-2, 0),
      p11: (-2, -2),
      p10: (-4, -2),
    )

    // Arêtes pointillés
    line(m.p00, m.p01, m.p11, m.p10, close: true, ..style-pointille)

    // Points sur les sommets
    circle(m.p00, radius: rayon-point, ..style-point)
    circle(m.p01, radius: rayon-point, ..style-point)
    circle(m.p11, radius: rayon-point, ..style-point)
    circle(m.p10, radius: rayon-point, ..style-point)

    // Étiquettes
    content(m.p00, [00], anchor: "south-east", padding: .2)
    content(m.p01, [01], anchor: "south-west", padding: .2)
    content(m.p11, [11], anchor: "north-west", padding: .2)
    content(m.p10, [10], anchor: "north-east", padding: .2)

    content((-3, -2.6), [$FF_2^2$])

    line((-1.2, -1), (1.2, -1), mark: pointe-fleche, stroke: 1.5pt + black)
    content((0, -0.6), [*Plongement*])

    // Sommets face arrière
    let cb = (p000: (2.5, 0), p001: (4.5, 0), p011: (4.5, -2), p010: (2.5, -2))
    // Sommets face avant (décalés)
    let cf = (p100: (3.5, 1), p101: (5.5, 1), p111: (5.5, -1), p110: (3.5, -1))

    // Arêtes pointillés du cube
    line(cb.p000, cb.p001, cb.p011, cb.p010, close: true, ..style-pointille) // Face arrière
    line(cf.p100, cf.p101, cf.p111, cf.p110, close: true, ..style-pointille) // Face avant
    line(cb.p000, cf.p100, ..style-pointille) // Liaisons
    line(cb.p001, cf.p101, ..style-pointille)
    line(cb.p011, cf.p111, ..style-pointille)
    line(cb.p010, cf.p110, ..style-pointille)

    // Points sur les sommets du cube
    circle(cb.p000, radius: rayon-point, ..style-point)
    circle(cb.p001, radius: rayon-point, ..style-point)
    circle(cb.p011, radius: rayon-point, ..style-point)
    circle(cb.p010, radius: rayon-point, ..style-point)
    circle(cf.p100, radius: rayon-point, ..style-point)
    circle(cf.p101, radius: rayon-point, ..style-point)
    circle(cf.p111, radius: rayon-point, ..style-point)
    circle(cf.p110, radius: rayon-point, ..style-point)

    // Étiquettes du cube
    content(cb.p000, [000], anchor: "south-east", padding: .2)
    // content(cb.p001, [001], anchor: "south-west", padding: .2)
    content((rel: (-0.85, 0.1), to: cb.p001), [001], anchor: "south-west", padding: .2)
    content(cb.p011, [011], anchor: "north-west", padding: .2)
    content(cb.p010, [010], anchor: "north-east", padding: .2)
    content(cf.p100, [100], anchor: "south-east", padding: .2)
    content(cf.p101, [101], anchor: "south-west", padding: .2)
    content(cf.p111, [111], anchor: "north-west", padding: .2)
    // content(cf.p110, [110], anchor: "north-east", padding: .2)
    content((rel: (0.85, -0.1), to: cf.p110), [110], anchor: "north-east", padding: .2)

    content((4, -2.6), [$FF_2^3$])
  })
}

#let definition(
  titre: "Définition",
  accent: black,
  titre_taille: 1.13em,
  corps_taille: 1.2em,
  compact: false,
  contenu,
) = {
  let largeur = if compact { auto } else { 100% }
  let colonne = if compact { (5pt, auto) } else { (5pt, 1fr) }

  block(
    width: largeur,
    fill: rgb("#f8f9fa"),
    stroke: 0.5pt + gray.lighten(60%),
    radius: 8pt,
    clip: true,
    grid(
      columns: colonne,
      rows: auto,
      fill: (col, row) => if col == 0 { accent },
      [],
      block(
        width: largeur,
        inset: (x: 16pt, y: 15pt),
        {
          {
            set text(fill: accent, weight: 800, size: titre_taille, tracking: 0.5pt)
            show math.equation: set text(weight: 700, size: 1.05em)
            titre
          }
          v(18pt, weak: true)
          {
            set text(size: corps_taille)
            set par(leading: 0.7em, justify: false)
            contenu
          }
        },
      ),
    ),
  )
}

#let limite_shannon_graphique_old() = {
  let col-shannon = red.darken(10%)
  let col-code-long = blue.darken(20%)
  let col-code-short = gray.lighten(10%)
  set text(size: 11pt)

  move(dx: -5pt, dy: 0pt)[
    #cetz.canvas({
      import cetz.draw: *

      plot.plot(
        size: (23, 15),

        // 1. On grossit les labels d'axes (15pt)
        x-label: pad(bottom: 30pt)[#move(dy: 80pt)[#text(size: 18pt)[$E_b/N_0$ (dB)]]],
        y-label: move(dx: -20pt, dy: -10pt)[#text(size: 18pt)[Bit Error Rate (BER)]],

        x-min: 0,
        x-max: 10,
        y-min: -8.5,
        y-max: 0.2,
        x-tick-step: 1,
        y-tick-step: 2,

        // Axis
        x-format: x => move(dy: 14pt)[#text(size: 16pt)[#x]],
        y-format: y => move(dx: -5pt)[#text(size: 16pt)[$10^(#y)$]],

        x-grid: true,
        y-grid: true,

        // Légende positionnée en bas.
        legend: "north-east",
        legend-style: (
          stroke: 0.5pt + gray,
          fill: white,
          padding: 0.2,
          offset: (-161.4pt, -45.1pt),
        ),
        {
          // 1. Limite de Shannon (Asymptote)
          plot.add(
            ((1.5, -8.5), (1.5, 0.2)),
            style: (stroke: (paint: col-shannon, thickness: 2pt, dash: "dashed")),
            label: [Limite de Shannon],
          )

          // 2. Petit bloc linéaire (n ≈ 100) - Ultra-densifié (smooth)
          plot.add(
            (
              (0.0, 0.00),
              (0.2, -0.01),
              (0.4, -0.03),
              (0.6, -0.05),
              (0.8, -0.07),
              (1.0, -0.10),
              (1.2, -0.13),
              (1.4, -0.17),
              (1.6, -0.21),
              (1.8, -0.25),
              (2.0, -0.30),
              (2.2, -0.36),
              (2.4, -0.43),
              (2.6, -0.51),
              (2.8, -0.60),
              (3.0, -0.70),
              (3.2, -0.80),
              (3.4, -0.91),
              (3.6, -1.03),
              (3.8, -1.16),
              (4.0, -1.30),
              (4.2, -1.44),
              (4.4, -1.59),
              (4.6, -1.75),
              (4.8, -1.92),
              (5.0, -2.10),
              (5.2, -2.28),
              (5.4, -2.47),
              (5.6, -2.67),
              (5.8, -2.88),
              (6.0, -3.10),
              (6.2, -3.32),
              (6.4, -3.55),
              (6.6, -3.79),
              (6.8, -4.04),
              (7.0, -4.30),
              (7.2, -4.56),
              (7.4, -4.83),
              (7.6, -5.11),
              (7.8, -5.40),
              (8.0, -5.70),
              (8.2, -6.00),
              (8.4, -6.31),
              (8.6, -6.63),
              (8.8, -6.96),
              (9.0, -7.30),
              (9.2, -7.62),
              (9.4, -7.95),
              (9.6, -8.29),
              (9.8, -8.64),
              (10.0, -9.00),
            ),
            style: (stroke: (paint: col-code-short, thickness: 1.5pt)),
            label: [Code court ($n approx 100$)],
          )

          // 3. Grand bloc linéaire (n = 64 800) - Ultra-densifié (smooth & waterfall net)
          plot.add(
            (
              (0.0, 0.0),
              (0.2, -0.005),
              (0.4, -0.01),
              (0.6, -0.02),
              (0.8, -0.035),
              (1.0, -0.05),
              (1.1, -0.07),
              (1.2, -0.10),
              (1.3, -0.14),
              (1.4, -0.20),
              (1.45, -0.28),
              (1.50, -0.40),
              (1.52, -0.45),
              (1.54, -0.52),
              (1.56, -0.60),
              (1.58, -0.69),
              (1.60, -0.80),
              (1.62, -0.92),
              (1.64, -1.05),
              (1.66, -1.19),
              (1.68, -1.34),
              (1.70, -1.50),
              (1.72, -1.69),
              (1.74, -1.89),
              (1.76, -2.11),
              (1.78, -2.35),
              (1.80, -2.60),
              (1.82, -2.88),
              (1.84, -3.18),
              (1.86, -3.50),
              (1.88, -3.84),
              (1.90, -4.20),
              (1.92, -4.52),
              (1.94, -4.86),
              (1.96, -5.22),
              (1.98, -5.60),
              (2.00, -6.00),
              (2.02, -6.32),
              (2.04, -6.64),
              (2.06, -6.94),
              (2.08, -7.23),
              (2.10, -7.50),
              (2.12, -7.66),
              (2.14, -7.81),
              (2.16, -7.95),
              (2.18, -8.08),
              (2.20, -8.20),
              (2.22, -8.32),
              (2.24, -8.44),
              (2.25, -8.50),
            ),
            style: (stroke: (paint: col-code-long, thickness: 3.5pt)),
            label: [Code long ($n = 64\,800$)],
          )
        },
      )
    })
  ]
}

#let limite_shannon_graphique() = {
  let col-shannon = red.darken(10%)
  let col-code-long = blue.darken(20%)
  let col-code-short = gray.lighten(10%)
  set text(size: 11pt)
  let label_size = 18pt

  move(dx: -5pt, dy: 0pt)[
    #cetz.canvas({
      import cetz.draw: *

      plot.plot(
        size: (23, 15),

        x-label: pad(bottom: 30pt)[#move(dy: 80pt)[#text(size: 18pt)[$E_b/N_0$ (dB)]]],
        y-label: move(dx: -20pt, dy: -10pt)[#text(size: 18pt)[Bit Error Rate (BER)]],

        x-min: 0,
        x-max: 10,
        y-min: -8.5,
        y-max: 0.2,
        x-tick-step: 1,
        y-tick-step: 2,

        x-format: x => move(dy: 14pt)[#text(size: 16pt)[#x]],
        y-format: y => move(dx: -5pt)[#text(size: 16pt)[$10^(#y)$]],

        x-grid: true,
        y-grid: true,

        legend: "north-east",
        legend-style: (
          stroke: 0.5pt + gray,
          fill: white,
          padding: 0.2,
          offset: (-204.8pt, -66.1pt),
          spacing: 0.4,
        ),

        {
          // 1. Limite de Shannon
          plot.add(
            ((1.5, -8.5), (1.5, 0.2)),
            style: (stroke: (paint: col-shannon, thickness: 2.5pt, dash: "dashed")),
            label: text(size: label_size)[Limite de Shannon],
          )

          // 2. Code court (n ≈ 100)
          plot.add(
            (
              (0.0, 0.00),
              (0.75, -0.06),
              (1.5, -0.21),
              (2.25, -0.38),
              (3.0, -0.70),
              (3.75, -1.08),
              (4.5, -1.67),
              (5.25, -2.33),
              (6.0, -3.10),
              (6.75, -3.92),
              (7.5, -4.97),
              (8.25, -6.08),
              (9.0, -7.30),
              (9.7, -8.50),
            ),
            style: (stroke: (paint: col-code-short, thickness: 1.5pt, dash: "dashed")),
            mark: "+",
            mark-style: (stroke: (paint: col-code-short, thickness: 0.08), size: 0.6),
            label: text(size: label_size)[$n approx 100$],
          )

          // 3. Code long (n = 64 800)
          plot.add(
            (
              (0.0, 0.0),
              (0.4, -0.01),
              (0.8, -0.035),
              (1.2, -0.10),
              (1.4, -0.20),
              (1.50, -0.40),
              (1.60, -0.80),
              (1.70, -1.50),
              (1.80, -2.60),
              (1.90, -4.20),
              (2.00, -6.00),
              (2.10, -7.50),
              (2.20, -8.20),
              (2.25, -8.50),
            ),
            style: (stroke: (paint: col-code-long, thickness: 2.5pt)),
            mark: "square",
            mark-style: (stroke: (paint: col-code-long, thickness: 0.08), fill: white, size: 0.5),
            label: text(size: label_size)[$n = 64800$],
          )
        },
      )
    })
  ]
}

#let decor_matrice_etoilee() = {
  // Ajuste length (ex: 0.6mm ou 0.7mm) selon ta diapo
  cetz.canvas(length: 0.7mm, {
    import cetz.draw: *

    // --- 1. Paramètres de la Matrice ---
    let nx = 430
    let ny = 265

    // --- 2. Paramètres de la Loupe ---
    let loupe_x = nx * 0.70
    let loupe_y = -ny / 2.0
    let R = 50.0
    let zoom = 3.0

    // --- 3. Paramètre d'Estompage (Gauche uniquement) ---
    let fade_left = 220.0

    // Palette de couleurs
    let col_focus = rgb("#0284c7")
    let col_fade = rgb("#64748b")
    let col_grid = rgb("#e2e8f0")

    // --- Fonction pseudo-aléatoire ---
    let pseudo_rand(x, y) = {
      let v = x * 7919 + y * 104729 + (x * x) * 313 + (y * y) * 991 + (x * y) * 101
      calc.rem(calc.abs(v), 100)
    }

    // --- ÉTAPE A : Dessin de la matrice (Fade à gauche uniquement) ---
    for x in range(nx) {
      for y in range(ny) {
        let hash = pseudo_rand(x, y)
        let is_active = hash < 12

        if is_active {
          // L'intensité ne dépend plus que de la position X
          let intensity = calc.min(1.0, x / fade_left)

          if intensity > 0.05 {
            let radius = 0.20 + 0.15 * intensity
            let c = col_fade.lighten((1.0 - intensity) * 80%)
            circle((x, -y), radius: radius, fill: c, stroke: none)
          }
        }
      }
    }

    // --- ÉTAPE B : Dessin de la Loupe ---
    circle((loupe_x, loupe_y), radius: R, fill: white, stroke: none)

    // Grille zoomée
    let limit = 20
    for k in range(-limit, limit + 1) {
      let offset = k * zoom
      if calc.abs(offset) < R {
        let half_chord = calc.sqrt(R * R - offset * offset)
        line(
          (loupe_x + offset, loupe_y + half_chord),
          (loupe_x + offset, loupe_y - half_chord),
          stroke: 0.5pt + col_grid,
        )
        line(
          (loupe_x - half_chord, loupe_y + offset),
          (loupe_x + half_chord, loupe_y + offset),
          stroke: 0.5pt + col_grid,
        )
      }
    }

    // Points zoomés
    let search_r = int(R / zoom) + 2
    let lx_int = int(loupe_x)
    let ly_idx = int(-loupe_y)

    for x in range(lx_int - search_r, lx_int + search_r + 1) {
      for y in range(ly_idx - search_r, ly_idx + search_r + 1) {
        let hash = pseudo_rand(x, y)
        if hash < 12 {
          let dx = x - loupe_x
          let dy = -y - loupe_y
          let d_orig = calc.sqrt(dx * dx + dy * dy)
          let d_zoom = d_orig * zoom

          if d_zoom <= R - 1.2 {
            circle((loupe_x + dx * zoom, loupe_y + dy * zoom), radius: 1.1, fill: col_focus, stroke: none)
          }
        }
      }
    }

    // Bordures de la Loupe
    circle((loupe_x, loupe_y), radius: R, stroke: 1.8pt + col_focus.lighten(20%))
    circle((loupe_x, loupe_y), radius: R + 1.5, stroke: 1.0pt + col_focus.lighten(50%))
    circle((loupe_x, loupe_y), radius: R + 3.5, stroke: 0.5pt + col_focus.lighten(80%))
  })
}

#let hldpc() = {
  import cetz.draw: *

  let points = (
    (7, 0),
    (10, 0),
    (15, 0),
    (22, 0),
    (24, 0),
    (29, 0),
    (3, 1),
    (6, 1),
    (18, 1),
    (19, 1),
    (25, 1),
    (27, 1),
    (5, 2),
    (9, 2),
    (13, 2),
    (14, 2),
    (17, 2),
    (28, 2),
    (0, 3),
    (8, 3),
    (11, 3),
    (16, 3),
    (20, 3),
    (26, 3),
    (1, 4),
    (2, 4),
    (4, 4),
    (12, 4),
    (21, 4),
    (23, 4),
    (5, 5),
    (6, 5),
    (8, 5),
    (10, 5),
    (21, 5),
    (29, 5),
    (14, 6),
    (15, 6),
    (16, 6),
    (18, 6),
    (22, 6),
    (28, 6),
    (0, 7),
    (1, 7),
    (4, 7),
    (9, 7),
    (20, 7),
    (26, 7),
    (2, 8),
    (3, 8),
    (11, 8),
    (12, 8),
    (17, 8),
    (19, 8),
    (7, 9),
    (13, 9),
    (23, 9),
    (24, 9),
    (25, 9),
    (27, 9),
    (0, 10),
    (6, 10),
    (15, 10),
    (18, 10),
    (21, 10),
    (26, 10),
    (2, 11),
    (7, 11),
    (10, 11),
    (17, 11),
    (22, 11),
    (27, 11),
    (8, 12),
    (11, 12),
    (14, 12),
    (20, 12),
    (23, 12),
    (29, 12),
    (4, 13),
    (5, 13),
    (9, 13),
    (13, 13),
    (16, 13),
    (19, 13),
    (1, 14),
    (3, 14),
    (12, 14),
    (24, 14),
    (25, 14),
    (28, 14),
  )

  cetz.canvas(length: 0.45cm, {
    let nx = 30
    let ny = 15
    let cell_size = 1.0
    let h_width = nx * cell_size
    let h_height = ny * cell_size

    // Palette unifiée
    let color_blue = orange
    let color_orange = blue
    let col_dot_base = gray.darken(30%)

    // Définition des couleurs de fond (lighten)
    let col_row_bg = color_blue.lighten(90%)
    let col_col_bg = color_orange.lighten(90%)
    // Calcul du mélange pour l'intersection
    let col_mix_bg = col_row_bg.mix(col_col_bg)

    // Label "H ="
    content((-3.4, -h_height / 2), text(size: 1.6em, weight: "bold")[$bold(H) =$])

    // Sélection visuelle (Focus)
    let sel_row = 7
    let sel_col = 10

    // Rectangles de fond
    rect((0, -sel_row), (h_width, -sel_row - 1), fill: col_row_bg, stroke: none)
    rect((sel_col, 0), (sel_col + 1, -h_height), fill: col_col_bg, stroke: none)
    rect((sel_col, -sel_row), (sel_col + 1, -sel_row - 1), fill: col_mix_bg, stroke: none)

    // Grands crochets matriciels
    let b_w = 0.6
    set-style(stroke: (thickness: 1.5pt, cap: "round"))
    line((b_w, 0.3), (0, 0.3), (0, -h_height - 0.3), (b_w, -h_height - 0.3))
    line((h_width - b_w, 0.3), (h_width, 0.3), (h_width, -h_height - 0.3), (h_width - b_w, -h_height - 0.3))

    // Dessin des points
    for (x, y) in points {
      let px = x + 0.5
      let py = -y - 0.5

      let is_row = (y == sel_row)
      let is_col = (x == sel_col)

      let d_col = col_dot_base
      let r = 0.16

      if is_row {
        d_col = color_blue
        r = 0.24
      } else if is_col {
        d_col = color_orange
        r = 0.24
      }

      circle((px, py), radius: r, fill: d_col, stroke: none)
    }

    // 5. Légendes w_r et w_c
    content((h_width + 0.5, -sel_row - 0.5), anchor: "west", text(
      fill: color_blue,
      weight: "bold",
      size: 1.1em,
    )[$w_r = 6$])
    content((sel_col + 0.5, 0.8), anchor: "south", text(fill: color_orange, weight: "bold", size: 1.1em)[$w_c = 3$])
  })
}

#let hldpc_dual(row1: 0, row2: 14) = {
  import cetz.draw: *
  let points = (
    (7, 0),
    (10, 0),
    (15, 0),
    (22, 0),
    (24, 0),
    (29, 0),
    (3, 1),
    (6, 1),
    (18, 1),
    (19, 1),
    (25, 1),
    (27, 1),
    (5, 2),
    (9, 2),
    (13, 2),
    (14, 2),
    (17, 2),
    (28, 2),
    (0, 3),
    (8, 3),
    (11, 3),
    (16, 3),
    (20, 3),
    (26, 3),
    (1, 4),
    (2, 4),
    (4, 4),
    (12, 4),
    (21, 4),
    (23, 4),
    (5, 5),
    (6, 5),
    (8, 5),
    (10, 5),
    (21, 5),
    (29, 5),
    (14, 6),
    (15, 6),
    (16, 6),
    (18, 6),
    (22, 6),
    (28, 6),
    (0, 7),
    (1, 7),
    (4, 7),
    (9, 7),
    (20, 7),
    (26, 7),
    (2, 8),
    (3, 8),
    (11, 8),
    (12, 8),
    (17, 8),
    (19, 8),
    (7, 9),
    (13, 9),
    (23, 9),
    (24, 9),
    (25, 9),
    (27, 9),
    (0, 10),
    (6, 10),
    (15, 10),
    (18, 10),
    (21, 10),
    (26, 10),
    (2, 11),
    (7, 11),
    (10, 11),
    (17, 11),
    (22, 11),
    (27, 11),
    (8, 12),
    (11, 12),
    (14, 12),
    (20, 12),
    (23, 12),
    (29, 12),
    (4, 13),
    (5, 13),
    (9, 13),
    (13, 13),
    (16, 13),
    (19, 13),
    (1, 14),
    (3, 14),
    (12, 14),
    (24, 14),
    (25, 14),
    (28, 14),
  )

  cetz.canvas(length: 0.35cm, {
    let nx = 30
    let ny = 15
    let col_1 = orange
    let col_2 = blue

    content((-3.4, -7.5), text(size: 1.6em)[$H =$])

    // Fonds de lignes
    rect((0, -row1), (nx, -row1 - 1), fill: col_1.lighten(90%), stroke: none)
    if row2 != none { rect((0, -row2), (nx, -row2 - 1), fill: col_2.lighten(90%), stroke: none) }

    // Crochets
    set-style(stroke: (thickness: 1.2pt))
    line((0.5, 0.3), (0, 0.3), (0, -ny - 0.3), (0.5, -ny - 0.3))
    line((nx - 0.5, 0.3), (nx, 0.3), (nx, -ny - 0.3), (nx - 0.5, -ny - 0.3))

    for (x, y) in points {
      let d_col = gray.darken(30%)
      let r = 0.15
      if y == row1 {
        d_col = col_1
        r = 0.22
      } else if y == row2 {
        d_col = col_2
        r = 0.22
      }
      circle((x + 0.5, -y - 0.5), radius: r, fill: d_col, stroke: none)
    }
  })
}

#let hldpc_triple(row1: 0, row2: 9, row3: 14) = {
  import cetz.draw
  let points = (
    (7, 0),
    (10, 0),
    (15, 0),
    (22, 0),
    (24, 0),
    (29, 0),
    (3, 1),
    (6, 1),
    (18, 1),
    (19, 1),
    (25, 1),
    (27, 1),
    (5, 2),
    (9, 2),
    (13, 2),
    (14, 2),
    (17, 2),
    (28, 2),
    (0, 3),
    (8, 3),
    (11, 3),
    (16, 3),
    (20, 3),
    (26, 3),
    (1, 4),
    (2, 4),
    (4, 4),
    (12, 4),
    (21, 4),
    (23, 4),
    (5, 5),
    (6, 5),
    (8, 5),
    (10, 5),
    (21, 5),
    (29, 5),
    (14, 6),
    (15, 6),
    (16, 6),
    (18, 6),
    (22, 6),
    (28, 6),
    (0, 7),
    (1, 7),
    (4, 7),
    (9, 7),
    (20, 7),
    (26, 7),
    (2, 8),
    (3, 8),
    (11, 8),
    (12, 8),
    (17, 8),
    (19, 8),
    (7, 9),
    (13, 9),
    (23, 9),
    (24, 9),
    (25, 9),
    (27, 9), // Ligne 9 contient 24
    (0, 10),
    (6, 10),
    (15, 10),
    (18, 10),
    (21, 10),
    (26, 10),
    (2, 11),
    (7, 11),
    (10, 11),
    (17, 11),
    (22, 11),
    (27, 11),
    (8, 12),
    (11, 12),
    (14, 12),
    (20, 12),
    (23, 12),
    (29, 12),
    (4, 13),
    (5, 13),
    (9, 13),
    (13, 13),
    (16, 13),
    (19, 13),
    (1, 14),
    (3, 14),
    (12, 14),
    (24, 14),
    (25, 14),
    (28, 14), // Ligne 14 contient 24
  )

  cetz.canvas(length: 0.35cm, {
    let nx = 30
    let ny = 15
    let col_1 = orange
    let col_2 = green.darken(15%)
    let col_3 = blue

    draw.content((-3.4, -7.5), text(size: 1.6em)[$H =$])

    // Fonds de lignes (Highlight)
    draw.rect((0, -row1), (nx, -row1 - 1), fill: col_1.lighten(90%), stroke: none)
    draw.rect((0, -row2), (nx, -row2 - 1), fill: col_2.lighten(90%), stroke: none)
    draw.rect((0, -row3), (nx, -row3 - 1), fill: col_3.lighten(90%), stroke: none)

    // Crochets
    draw.set-style(stroke: (thickness: 1.2pt))
    draw.line((0.5, 0.3), (0, 0.3), (0, -ny - 0.3), (0.5, -ny - 0.3))
    draw.line((nx - 0.5, 0.3), (nx, 0.3), (nx, -ny - 0.3), (nx - 0.5, -ny - 0.3))

    for (x, y) in points {
      let d_col = gray.darken(30%)
      let r = 0.15

      if y == row1 {
        d_col = col_1
        r = 0.22
      } else if y == row2 {
        d_col = col_2
        r = 0.22
      } else if y == row3 {
        d_col = col_3
        r = 0.22
      }

      draw.circle((x + 0.5, -y - 0.5), radius: r, fill: d_col, stroke: none)
    }
  })
}

#let hldpc_col24(row1: 0, row2: 9, row3: 14, col: 24) = {
  import cetz.draw
  let points = (
    (7, 0),
    (10, 0),
    (15, 0),
    (22, 0),
    (24, 0),
    (29, 0),
    (3, 1),
    (6, 1),
    (18, 1),
    (19, 1),
    (25, 1),
    (27, 1),
    (5, 2),
    (9, 2),
    (13, 2),
    (14, 2),
    (17, 2),
    (28, 2),
    (0, 3),
    (8, 3),
    (11, 3),
    (16, 3),
    (20, 3),
    (26, 3),
    (1, 4),
    (2, 4),
    (4, 4),
    (12, 4),
    (21, 4),
    (23, 4),
    (5, 5),
    (6, 5),
    (8, 5),
    (10, 5),
    (21, 5),
    (29, 5),
    (14, 6),
    (15, 6),
    (16, 6),
    (18, 6),
    (22, 6),
    (28, 6),
    (0, 7),
    (1, 7),
    (4, 7),
    (9, 7),
    (20, 7),
    (26, 7),
    (2, 8),
    (3, 8),
    (11, 8),
    (12, 8),
    (17, 8),
    (19, 8),
    (7, 9),
    (13, 9),
    (23, 9),
    (24, 9),
    (25, 9),
    (27, 9),
    (0, 10),
    (6, 10),
    (15, 10),
    (18, 10),
    (21, 10),
    (26, 10),
    (2, 11),
    (7, 11),
    (10, 11),
    (17, 11),
    (22, 11),
    (27, 11),
    (8, 12),
    (11, 12),
    (14, 12),
    (20, 12),
    (23, 12),
    (29, 12),
    (4, 13),
    (5, 13),
    (9, 13),
    (13, 13),
    (16, 13),
    (19, 13),
    (1, 14),
    (3, 14),
    (12, 14),
    (24, 14),
    (25, 14),
    (28, 14),
  )
  cetz.canvas(length: 0.35cm, {
    let nx = 30
    let ny = 15
    let col_1 = orange
    let col_2 = green.darken(15%)
    let col_3 = blue
    let col_v = red

    draw.content((-3.4, -7.5), text(size: 1.6em)[$H =$])

    // Fonds de lignes horizontales
    draw.rect((0, -row1), (nx, -row1 - 1), fill: col_1.lighten(90%), stroke: none)
    draw.rect((0, -row2), (nx, -row2 - 1), fill: col_2.lighten(90%), stroke: none)
    draw.rect((0, -row3), (nx, -row3 - 1), fill: col_3.lighten(90%), stroke: none)

    // Fond de colonne verticale rouge
    draw.rect((col, 0.3), (col + 1, -ny - 0.3), fill: col_v.lighten(88%), stroke: none)

    // Label r_24 sous la colonne
    draw.content(
      (col + 0.5, -ny - 1.0),
      text(size: 0.85em, fill: col_v)[$r_(24)$],
    )

    // Crochets
    draw.set-style(stroke: (thickness: 1.2pt))
    draw.line((0.5, 0.3), (0, 0.3), (0, -ny - 0.3), (0.5, -ny - 0.3))
    draw.line((nx - 0.5, 0.3), (nx, 0.3), (nx, -ny - 0.3), (nx - 0.5, -ny - 0.3))

    // Points
    for (x, y) in points {
      let d_col = gray.darken(30%)
      let r = 0.15

      let is_highlighted_row = (y == row1 or y == row2 or y == row3)
      let is_col = (x == col)

      if is_col and is_highlighted_row {
        // Intersection : rouge, plus gros, prioritaire
        d_col = col_v
        r = 0.30
      } else if y == row1 {
        d_col = col_1
        r = 0.22
      } else if y == row2 {
        d_col = col_2
        r = 0.22
      } else if y == row3 {
        d_col = col_3
        r = 0.22
      }
      draw.circle((x + 0.5, -y - 0.5), radius: r, fill: d_col, stroke: none)
    }
  })
}

// Données de H partagées (mêmes points que hldpc / hldpc_dual)
#let _h_pts = (
  (7, 0),
  (10, 0),
  (15, 0),
  (22, 0),
  (24, 0),
  (29, 0),
  (3, 1),
  (6, 1),
  (18, 1),
  (19, 1),
  (25, 1),
  (27, 1),
  (5, 2),
  (9, 2),
  (13, 2),
  (14, 2),
  (17, 2),
  (28, 2),
  (0, 3),
  (8, 3),
  (11, 3),
  (16, 3),
  (20, 3),
  (26, 3),
  (1, 4),
  (2, 4),
  (4, 4),
  (12, 4),
  (21, 4),
  (23, 4),
  (5, 5),
  (6, 5),
  (8, 5),
  (10, 5),
  (21, 5),
  (29, 5),
  (14, 6),
  (15, 6),
  (16, 6),
  (18, 6),
  (22, 6),
  (28, 6),
  (0, 7),
  (1, 7),
  (4, 7),
  (9, 7),
  (20, 7),
  (26, 7),
  (2, 8),
  (3, 8),
  (11, 8),
  (12, 8),
  (17, 8),
  (19, 8),
  (7, 9),
  (13, 9),
  (23, 9),
  (24, 9),
  (25, 9),
  (27, 9),
  (0, 10),
  (6, 10),
  (15, 10),
  (18, 10),
  (21, 10),
  (26, 10),
  (2, 11),
  (7, 11),
  (10, 11),
  (17, 11),
  (22, 11),
  (27, 11),
  (8, 12),
  (11, 12),
  (14, 12),
  (20, 12),
  (23, 12),
  (29, 12),
  (4, 13),
  (5, 13),
  (9, 13),
  (13, 13),
  (16, 13),
  (19, 13),
  (1, 14),
  (3, 14),
  (12, 14),
  (24, 14),
  (25, 14),
  (28, 14),
)

#let tricolor-j = context {
  let w = measure($bold(j)$).width
  let h = measure($bold(j)$).height
  let d = measure($x$).height * 0.6
  let total_h = h + d

  // Marge gauche pour absorber le débordement italique du j
  let margin = w * 0.25
  let full_w = w + margin
  let t = full_w / 3

  // Biais vertical : la frontière se décale de `bias` entre haut et bas
  let bias = w * 0.2
  let n = 40
  let stripe = total_h / n

  box(baseline: d, width: w, height: total_h, clip: false, place(dx: -margin, dy: 0pt, stack(
    dir: ttb,
    ..range(n).map(i => {
      let frac = i / (n - 1)
      // Frontières qui bougent uniquement verticalement
      let f1 = full_w * 0.33 + bias * frac
      let f2 = full_w * 0.66 + bias * frac

      box(
        width: full_w,
        height: stripe,
        clip: false,
        place(dx: 0pt, dy: 0pt, box(width: f1, height: stripe, clip: true, move(dx: margin, dy: -stripe * i, text(
          fill: orange,
          weight: "bold",
        )[$j$])))
          + place(dx: f1, dy: 0pt, box(width: f2 - f1, height: stripe, clip: true, move(
            dx: margin - f1,
            dy: -stripe * i,
            text(fill: green.darken(20%), weight: "bold")[$j$],
          )))
          + place(dx: f2, dy: 0pt, box(width: full_w - f2, height: stripe, clip: true, move(
            dx: margin - f2,
            dy: -stripe * i,
            text(fill: blue, weight: "bold")[$j$],
          ))),
      )
    }),
  )))
}


#let tanner_illustration_old() = {
  cetz.canvas(length: 1.1cm, {
    import cetz.draw: *

    let vy = 2.6
    let cy = 0.0
    let vxs = (0.0, 1.2, 2.4, 3.6)
    let cxs = (0.9, 2.7)

    let edges = ((0, 0), (1, 0), (2, 0), (1, 1), (2, 1), (3, 1))

    // Dessin des arêtes
    for (vi, ci) in edges {
      line(
        (vxs.at(vi), vy - 0.28),
        (cxs.at(ci), cy + 0.32),
        stroke: 0.9pt + gray.darken(10%),
      )
    }

    // Nœuds de contrôle (carrés, orange)
    let cnames = ([$c_0$], [$c_1$])
    for j in range(2) {
      let cx = cxs.at(j)
      let s = 0.30
      rect(
        (cx - s, cy - s),
        (cx + s, cy + s),
        fill: orange.lighten(75%),
        stroke: 1.8pt + orange,
        name: "c" + str(j),
      )
      content(
        (cx, cy - s - 0.3),
        anchor: "north",
        text(size: 0.8em, fill: orange, weight: "bold")[#cnames.at(j)],
      )
    }

    // Nœuds de variable (cercles, bleu)
    let vnames = ([$v_0$], [$v_1$], [$v_2$], [$v_3$])
    for i in range(4) {
      let vx = vxs.at(i)
      let r = 0.28
      circle(
        (vx, vy),
        radius: r,
        fill: blue.lighten(75%),
        stroke: 1.8pt + blue,
        name: "v" + str(i),
      )
      content(
        (vx, vy + r + 0.25),
        anchor: "south",
        text(size: 0.8em, fill: blue, weight: "bold")[#vnames.at(i)],
      )
    }
  })
}

#let tanner_illustration() = {
  cetz.canvas(length: 1.1cm, {
    import cetz.draw: *
    let vy = 2.6
    let cy = 0.0
    let vxs = (0.0, 1.2, 2.4, 3.6)
    let cxs = (0.9, 2.7)
    let edges = ((0, 0), (1, 0), (2, 0), (1, 1), (2, 1), (3, 1))
    let vr = 0.28
    let cs = 0.30

    let circle_border(cx, cy_n, r, dx, dy) = {
      let len = calc.sqrt(dx * dx + dy * dy)
      (cx + r * dx / len, cy_n + r * dy / len)
    }

    let square_border(cx, cy_n, s, dx, dy) = {
      let ax = calc.abs(dx)
      let ay = calc.abs(dy)
      let t = if ax == 0.0 { s / ay } else if ay == 0.0 { s / ax } else { calc.min(s / ax, s / ay) }
      (cx + t * dx, cy_n + t * dy)
    }

    // Dessin des arêtes
    for (vi, ci) in edges {
      let vx = vxs.at(vi)
      let cx = cxs.at(ci)
      let dx = cx - vx
      let dy = cy - vy
      let (px1, py1) = circle_border(vx, vy, vr, dx, dy)
      let (px2, py2) = square_border(cx, cy, cs, -dx, -dy)
      line((px1, py1), (px2, py2), stroke: 0.9pt + gray.darken(10%))
    }

    // Nœuds de contrôle (carrés, orange)
    let cnames = ([$c_0$], [$c_1$])
    for j in range(2) {
      let cx = cxs.at(j)
      let s = cs
      rect(
        (cx - s, cy - s),
        (cx + s, cy + s),
        fill: orange.lighten(75%),
        stroke: 1.8pt + orange,
        name: "c" + str(j),
      )
      content(
        (cx, cy - s - 0.3),
        anchor: "north",
        text(size: 0.8em, fill: orange, weight: "bold")[#cnames.at(j)],
      )
    }

    // Nœuds de variable (cercles, bleu)
    let vnames = ([$v_0$], [$v_1$], [$v_2$], [$v_3$])
    for i in range(4) {
      let vx = vxs.at(i)
      let r = vr
      circle(
        (vx, vy),
        radius: r,
        fill: blue.lighten(75%),
        stroke: 1.8pt + blue,
        name: "v" + str(i),
      )
      content(
        (vx, vy + r + 0.25),
        anchor: "south",
        text(size: 0.8em, fill: blue, weight: "bold")[#vnames.at(i)],
      )
    }
  })
}

// Mini matrice H avec surbrillance d'une ligne et/ou colonne
#let h_mini_tanner(hl_row: none, hl_col: none) = {
  let pts = _h_pts
  cetz.canvas(length: 0.215cm, {
    import cetz.draw: *
    let nx = 30
    let ny = 15
    content((-7, -7.5), text(size: 1.5em, weight: "bold")[$H =$])
    if hl_row != none {
      rect((0, -hl_row), (nx, -hl_row - 1), fill: orange.lighten(85%), stroke: none)
    }
    if hl_col != none {
      rect((hl_col, 0), (hl_col + 1, -ny), fill: blue.lighten(85%), stroke: none)
    }
    set-style(stroke: (thickness: 0.9pt, cap: "round"))
    line((0.4, 0.25), (0, 0.25), (0, -ny - 0.25), (0.4, -ny - 0.25))
    line((nx - 0.4, 0.25), (nx, 0.25), (nx, -ny - 0.25), (nx - 0.4, -ny - 0.25))
    for (x, y) in pts {
      let r = 0.13
      let col = gray.darken(25%)
      if hl_row != none and y == hl_row {
        col = orange
        r = 0.22
      } else if hl_col != none and x == hl_col {
        col = blue
        r = 0.22
      }
      circle((x + 0.5, -y - 0.5), radius: r, fill: col, stroke: none)
    }
    if hl_row != none {
      content((nx + 0.5, -hl_row - 0.5), anchor: "west", text(fill: orange, size: 1.0em, weight: "bold")[$c_#hl_row$])
    }
    if hl_col != none {
      content((hl_col + 0.5, 0.70), anchor: "south", text(fill: blue, size: 1.0em, weight: "bold")[$v_#hl_col$])
    }
  })
}

#let hldpc_dynamic(hl_rows: (), hl_cols: (), show_labels: true, h_show: true) = {
  import cetz.draw: *

  let points = (
    (7, 0),
    (10, 0),
    (15, 0),
    (22, 0),
    (24, 0),
    (29, 0),
    (3, 1),
    (6, 1),
    (18, 1),
    (19, 1),
    (25, 1),
    (27, 1),
    (5, 2),
    (9, 2),
    (13, 2),
    (14, 2),
    (17, 2),
    (28, 2),
    (0, 3),
    (8, 3),
    (11, 3),
    (16, 3),
    (20, 3),
    (26, 3),
    (1, 4),
    (2, 4),
    (4, 4),
    (12, 4),
    (21, 4),
    (23, 4),
    (5, 5),
    (6, 5),
    (8, 5),
    (10, 5),
    (21, 5),
    (29, 5),
    (14, 6),
    (15, 6),
    (16, 6),
    (18, 6),
    (22, 6),
    (28, 6),
    (0, 7),
    (1, 7),
    (4, 7),
    (9, 7),
    (20, 7),
    (26, 7),
    (2, 8),
    (3, 8),
    (11, 8),
    (12, 8),
    (17, 8),
    (19, 8),
    (7, 9),
    (13, 9),
    (23, 9),
    (24, 9),
    (25, 9),
    (27, 9),
    (0, 10),
    (6, 10),
    (15, 10),
    (18, 10),
    (21, 10),
    (26, 10),
    (2, 11),
    (7, 11),
    (10, 11),
    (17, 11),
    (22, 11),
    (27, 11),
    (8, 12),
    (11, 12),
    (14, 12),
    (20, 12),
    (23, 12),
    (29, 12),
    (4, 13),
    (5, 13),
    (9, 13),
    (13, 13),
    (16, 13),
    (19, 13),
    (1, 14),
    (3, 14),
    (12, 14),
    (24, 14),
    (25, 14),
    (28, 14),
  )

  cetz.canvas(length: 0.4cm, {
    let nx = 30
    let ny = 15

    let col_row = orange
    let col_col = blue
    let col_dot_base = gray.darken(30%)

    // Marge pour réduire l'épaisseur des bandes (crée le gap visuel)
    let gap = 0.1

    if h_show {
      content((-3.4, -ny / 2), text(size: 1.6em, weight: "bold")[$H =$])
    } else {
      content((-3.4, -ny / 2), text(size: 1.6em, weight: "bold")[$space space space$])
    }

    // 1. Dessiner les fonds des lignes (avec gap)
    for r in hl_rows {
      rect((0, -r - gap), (nx, -r - 1 + gap), fill: col_row.lighten(85%), stroke: none)
    }

    // 2. Dessiner les fonds des colonnes (avec gap)
    for c in hl_cols {
      rect((c + gap, 0), (c + 1 - gap, -ny), fill: col_col.lighten(85%), stroke: none)
    }

    // 3. Dessiner les intersections
    for r in hl_rows {
      for c in hl_cols {
        rect(
          (c + gap, -r - gap),
          (c + 1 - gap, -r - 1 + gap),
          fill: col_row.lighten(85%).mix(col_col.lighten(85%)),
          stroke: none,
        )
      }
    }

    // 4. Les grands crochets matriciels
    let b_w = 0.6
    set-style(stroke: (thickness: 1.5pt, cap: "round"))
    line((b_w, 0.3), (0, 0.3), (0, -ny - 0.3), (b_w, -ny - 0.3))
    line((nx - b_w, 0.3), (nx, 0.3), (nx, -ny - 0.3), (nx - b_w, -ny - 0.3))

    // 5. Dessin des points
    for (x, y) in points {
      let px = x + 0.5
      let py = -y - 0.5

      let is_row = y in hl_rows
      let is_col = x in hl_cols

      let d_col = col_dot_base
      let r = 0.15

      // Détermine la couleur et la taille si le point est dans une zone surlignée
      if is_row and is_col {
        d_col = col_row.mix(col_col) // Point à l'intersection
        r = 0.24
      } else if is_row {
        d_col = col_row
        r = 0.24
      } else if is_col {
        d_col = col_col
        r = 0.24
      }

      circle((px, py), radius: r, fill: d_col, stroke: none)
    }

    // 6. Affichage optionnel des labels
    if show_labels {
      for r in hl_rows {
        content((nx + 0.5, -r - 0.5), anchor: "west", text(fill: col_row, weight: "bold", size: 1.1em)[$c_#r$])
      }
      for c in hl_cols {
        content((c + 0.5, -ny - 0.8), anchor: "north", text(fill: col_col, weight: "bold", size: 1.1em)[$v_#c$])
      }
    }
  })
}

// Graphe de Tanner paramétrable
//   scale    : taille d'une unité cetz
//   hl_row   : index du check node à mettre en évidence (none = aucun)
//   hl_col   : index du variable node à mettre en évidence (none = aucun)
//   show_all : afficher toutes les arêtes
//   colored  : colorier tous les nœuds (bleu/orange) même sans highlight
#let tanner_canvas(
  scale: 0.43cm,
  hl_row: none,
  hl_col: none,
  show_all: false,
  colored: false,
  v_c_show: true,
) = {
  let pts = _h_pts
  cetz.canvas(length: scale, {
    import cetz.draw: *
    let n_var = 30
    let n_chk = 15
    let vy = 5.5
    let cy = 0.0

    // --- Logique de centrage ---
    let gap_v = 1.0 // Espacement des cercles
    let gap_c = 1.8 // Espacement des carrés (un peu plus large pour l'équilibre visuel)

    let width_v = (n_var - 1) * gap_v
    let width_c = (n_chk - 1) * gap_c
    let offset_c = (width_v - width_c) / 2 // Calcul du décalage pour centrer C sous V

    // Noeuds connectés au check ou variable mis en évidence
    let hl_var_set = if hl_row != none {
      pts.filter(p => p.at(1) == hl_row).map(p => p.at(0))
    } else { () }
    let hl_chk_set = if hl_col != none {
      pts.filter(p => p.at(0) == hl_col).map(p => p.at(1))
    } else { () }

    // — ARÊTES —
    for (vj, ci) in pts {
      let vx = vj * gap_v
      let ccx = offset_c + (ci * gap_c)
      let is_row = hl_row != none and ci == hl_row
      let is_col = hl_col != none and vj == hl_col
      let do_draw = is_row or is_col or show_all
      if do_draw {
        let s = if is_row { 1.5pt + orange } else if is_col { 2.0pt + blue } else { 0.50pt + gray.lighten(20%) }
        line((vx, vy), (ccx, cy), stroke: s)
      }
    }

    // — NŒUDS DE CONTRÔLE (carrés) —
    for j in range(n_chk) {
      let ccx = offset_c + (j * gap_c)
      let sz = 0.40
      let is_main = hl_row != none and j == hl_row
      let is_conn = hl_chk_set.contains(j)
      let fc = if is_main { orange.lighten(48%) } else if is_conn { blue.lighten(60%) } else if colored {
        orange.lighten(76%)
      } else { white }
      let sc = if is_main { 1.8pt + orange } else if is_conn { 1.8pt + blue } else if colored { 1.2pt + orange } else {
        1.0pt + black
      }
      rect((ccx - sz, cy - sz), (ccx + sz, cy + sz), fill: fc, stroke: sc)
      if is_main {
        content((ccx, cy - sz - 0.33), anchor: "north", text(size: 0.52em, fill: orange, weight: "bold")[$c_#j$])
      } else if is_conn {
        content((ccx, cy - sz - 0.33), anchor: "north", text(size: 0.52em, fill: blue, weight: "bold")[$c_#j$])
      }
    }

    // — NŒUDS DE VARIABLE (cercles) —
    for i in range(n_var) {
      let vx = i * gap_v
      let r = 0.33
      let is_main = hl_col != none and i == hl_col
      let is_conn = hl_var_set.contains(i)
      let fc = if is_main { blue.lighten(48%) } else if is_conn { orange.lighten(60%) } else if colored {
        blue.lighten(76%)
      } else { white }
      let sc = if is_main { 1.5pt + blue } else if is_conn { 1.5pt + orange } else if colored { 1.2pt + blue } else {
        1.0pt + black
      }
      circle((vx, vy), radius: r, fill: fc, stroke: sc)
      if is_main {
        content((vx, vy + r + 0.30), anchor: "south", text(size: 0.52em, fill: blue, weight: "bold")[$v_#i$])
      } else if is_conn {
        content((vx, vy + r + 0.30), anchor: "south", text(size: 0.52em, fill: orange, weight: "bold")[$v_#i$])
      }
    }

    // Légendes des couches
    if v_c_show {
      content((-2.0, vy), anchor: "east", text(size: 0.65em, weight: "bold")[$V$])
      content((-2.0, cy), anchor: "east", text(size: 0.65em, weight: "bold")[$C$])
    }
  })
}

// Diagramme passage de messages (Belief Propagation)
#let bp_diagram() = {
  cetz.canvas(length: 1.05cm, {
    import cetz.draw: *
    let vy = 3.0
    let cy = 0.0
    let vxs = (0.0, 1.3, 2.6, 3.9)
    let cxs = (0.65, 2.6)
    let edges = ((0, 0), (1, 0), (2, 0), (1, 1), (2, 1), (3, 1))
    let mk_bl = (end: "stealth", fill: blue, size: 0.17)
    let mk_or = (end: "stealth", fill: orange, size: 0.17)

    // Flèches bidirectionnelles décalées
    for (vi, ci) in edges {
      let vx = vxs.at(vi)
      let cx = cxs.at(ci)
      // V → C (bleu, décalé à gauche)
      line((vx - 0.06, vy - 0.32), (cx - 0.06, cy + 0.38), mark: mk_bl, stroke: 1.4pt + blue.lighten(15%))
      // C → V (orange, décalé à droite)
      line((cx + 0.06, cy + 0.38), (vx + 0.06, vy - 0.32), mark: mk_or, stroke: 1.4pt + orange.lighten(10%))
    }

    // Nœuds de variable
    let vnames = ([$v_0$], [$v_1$], [$v_2$], [$v_3$])
    for i in range(4) {
      let vx = vxs.at(i)
      let r = 0.30
      circle((vx, vy), radius: r, fill: blue.lighten(72%), stroke: 1.6pt + blue)
      content((vx, vy + r + 0.20), anchor: "south", text(size: 0.70em, fill: blue, weight: "bold")[#vnames.at(i)])
    }

    // Nœuds de contrôle
    let cnames = ([$c_0$], [$c_1$])
    for j in range(2) {
      let cx = cxs.at(j)
      let s = 0.33
      rect((cx - s, cy - s), (cx + s, cy + s), fill: orange.lighten(72%), stroke: 1.6pt + orange)
      content((cx, cy - s - 0.22), anchor: "north", text(size: 0.70em, fill: orange, weight: "bold")[#cnames.at(j)])
    }

    // Légendes des flèches
    content((4.45, vy - 0.5), anchor: "west", text(size: 0.65em, fill: blue, weight: "bold")[$mu_(j arrow i)$])
    content((4.45, cy + 0.4), anchor: "west", text(size: 0.65em, fill: orange, weight: "bold")[$nu_(i arrow j)$])
  })
}

#let icon_var = box(baseline: 20%)[
  #cetz.canvas({
    import cetz.draw: *
    circle((0, 0), radius: 0.35em, fill: blue.lighten(88%), stroke: 1.5pt + blue)
  })
]

#let icon_chk = box(baseline: 20%)[
  #cetz.canvas({
    import cetz.draw: *
    rect((-0.35em, -0.35em), (0.35em, 0.35em), fill: orange.lighten(88%), stroke: 1.5pt + orange)
  })
]

// Calcule la parité d'un Check Node i en fonction des Variable Nodes connectés
#let calcule_parite(v_values, index_chk) = {
  let neighbors = _h_pts.filter(p => p.at(1) == index_chk).map(p => p.at(0))
  let somme = 0
  for n in neighbors {
    somme = somme + v_values.at(n)
  }
  calc.rem(somme, 2)
}

#let tanner_status(
  scale: 0.43cm,
  v_values: none,
  hl_v: (),
  highlight_edges: false,
) = {
  let pts = _h_pts
  cetz.canvas(length: scale, {
    import cetz.draw: *
    let n_var = 30
    let n_chk = 15
    let vy = 5.5
    let cy = 0.0
    let gap_v = 1.0
    let gap_c = 1.8
    let width_v = (n_var - 1) * gap_v
    let width_c = (n_chk - 1) * gap_c
    let offset_c = (width_v - width_c) / 2

    let col_ok = green
    let col_err = red

    for (vj, ci) in pts {
      let vx = vj * gap_v
      let ccx = offset_c + (ci * gap_c)
      let p_ci = calcule_parite(v_values, ci)
      let is_err_edge = highlight_edges and hl_v.contains(vj) and p_ci == 1

      if not is_err_edge {
        line((vx, vy), (ccx, cy), stroke: 0.4pt + gray.lighten(60%))
      }
    }

    for (vj, ci) in pts {
      let vx = vj * gap_v
      let ccx = offset_c + (ci * gap_c)
      let p_ci = calcule_parite(v_values, ci)
      let is_err_edge = highlight_edges and hl_v.contains(vj) and p_ci == 1

      if is_err_edge {
        line((vx, vy), (ccx, cy), stroke: 1.5pt + col_err)
      }
    }

    for j in range(n_chk) {
      let ccx = offset_c + (j * gap_c)
      let sz = 0.40
      let p = calcule_parite(v_values, j)
      let is_ok = (p == 0)

      rect(
        (ccx - sz, cy - sz),
        (ccx + sz, cy + sz),
        fill: if is_ok { col_ok.lighten(92%) } else { col_err.lighten(92%) },
        stroke: if is_ok { 1.2pt + col_ok } else { 1.5pt + col_err },
      )
      content((ccx, cy), text(size: 0.65em, weight: "bold", fill: if is_ok { col_ok } else { col_err })[#p])
    }

    for i in range(n_var) {
      let vx = i * gap_v
      let r = 0.33
      let val = v_values.at(i)
      let is_err = hl_v.contains(i)

      circle(
        (vx, vy),
        radius: r,
        fill: if is_err { col_err.lighten(92%) } else { blue.lighten(92%) },
        stroke: if is_err { 1.5pt + col_err } else { 1.2pt + blue },
      )
      content((vx, vy), text(size: 0.55em, weight: "bold", fill: if is_err { col_err } else { blue })[#val])
    }
  })
}

// Composant de zoom d'une seule contrainte (CN) et ses VN voisins
#let zoom_contrainte(is_ok: true) = {
  cetz.canvas(length: 1cm, {
    import cetz.draw: *
    let col = if is_ok { green } else { red }

    rect((-2.2, -1.2), (2.2, 2.0), stroke: none, fill: none)

    let blue_fill = blue.lighten(90%)
    let blue_stroke = 1.2pt + blue
    let red_fill = red.lighten(90%)
    let red_stroke = 2pt + red
    let gray_stroke = 0.8pt + gray

    for i in (0, 1, 2) {
      let x = i * 1.5 - 1.5
      let is_failed_vn = not is_ok and i == 1
      line((x, 1.5), (0, 0), stroke: if is_failed_vn { red_stroke } else { gray_stroke })
    }

    for i in (0, 1, 2) {
      let x = i * 1.5 - 1.5
      let is_failed_vn = not is_ok and i == 1
      circle(
        (x, 1.5),
        radius: 0.3,
        stroke: if is_failed_vn { red_stroke } else { blue_stroke },
        fill: if is_failed_vn { red_fill } else { blue_fill },
      )

      let val = if is_failed_vn { "1" } else { "0" }
      content((x, 1.5), text(size: 12pt, fill: if is_failed_vn { col } else { blue }, weight: "bold")[#val])
    }

    rect(
      (-0.3, -0.3),
      (0.3, 0.3),
      stroke: if not is_ok { red_stroke } else { 2pt + col },
      fill: if not is_ok { red_fill } else { col.lighten(90%) },
    )
    content((0, 0), text(fill: col, weight: "bold", size: 15pt)[#(if is_ok { "0" } else { "1" })])

    let eq_size = 14pt
    if is_ok {
      content(
        (0, -0.7),
        text(fill: col, size: eq_size)[$0 plus.o 0 plus.o 0 = bold(0)$],
        anchor: "north",
      )
    } else {
      content(
        (0, -0.71),
        text(fill: col, size: eq_size)[$0 plus.o 1 plus.o 0 = bold(1)$],
        anchor: "north",
      )
    }
  })
}

// --- Schémas pour la Topologie et la Construction ---
// Visualisation du Girth et d'un cycle de longueur 4
#let schema_girth_4(highlight: false) = {
  cetz.canvas(length: 0.8cm, {
    import cetz.draw: *
    let s_dash = (paint: red, thickness: 2pt, dash: "dashed")

    // Positionnement
    let v0 = (-1.5, 2)
    let v1 = (1.5, 2)
    let c0 = (-1.5, 0)
    let c1 = (1.5, 0)

    // Arêtes
    line(v0, c0, v1, c1, v0, stroke: if highlight { s_dash } else { 1pt + gray })
    // line(v0, c1, stroke: 1pt + gray.lighten(60%))

    // Noeuds
    circle(v0, radius: 0.3, fill: blue.lighten(90%), stroke: 1.2pt + blue)
    circle(v1, radius: 0.3, fill: blue.lighten(90%), stroke: 1.2pt + blue)
    rect(
      (c0.at(0) - 0.3, c0.at(1) - 0.3),
      (c0.at(0) + 0.3, c0.at(1) + 0.3),
      fill: orange.lighten(90%),
      stroke: 1.2pt + orange,
    )
    rect(
      (c1.at(0) - 0.3, c1.at(1) - 0.3),
      (c1.at(0) + 0.3, c1.at(1) + 0.3),
      fill: orange.lighten(90%),
      stroke: 1.2pt + orange,
    )

    // if highlight {
    //   content(
    //     (0, 1),
    //     text(fill: red, weight: "bold", size: 0.8em)[Cycle $L=4$],
    //     frame: "rect",
    //     stroke: none,
    //     fill: rgb("ffffffcc"),
    //   )
    // }
  })
}

#let echo_chamber() = {
  cetz.canvas(length: 1cm, {
    import cetz.draw: *
    let v0 = (0, 2)
    let c0 = (0, 0)

    line(v0, c0, mark: (end: "stealth", fill: red), stroke: 2pt + red)
    arc((0, 0), start: -45deg, stop: 225deg, radius: 0.5, mark: (end: "stealth", fill: red), stroke: 1.5pt + red)

    circle(v0, radius: 0.3, fill: blue.lighten(90%), stroke: 1.5pt + blue)
    rect((-0.3, -0.3), (0.3, 0.3), fill: orange.lighten(90%), stroke: 1.5pt + orange)

    content((1, 1), text(fill: red, size: 0.8em, style: "italic")[Auto-renforcement \ d'une erreur], anchor: "west")
  })
}

#let peg_concept() = {
  cetz.canvas(length: 0.6cm, {
    import cetz.draw: *
    circle((0, 4), radius: 0.4, fill: blue.lighten(80%), stroke: 1.5pt + blue)
    let targets = ((-3, 0), (0, 0), (3, 0))
    for t in targets {
      line((0, 4), t, stroke: 1.5pt + green)
      rect(
        (t.at(0) - 0.3, t.at(1) - 0.3),
        (t.at(0) + 0.3, t.at(1) + 0.3),
        fill: orange.lighten(90%),
        stroke: 1.2pt + orange,
      )
    }
    content((0, -1.2), text(size: 0.8em, fill: green, weight: "bold")[Distance maximale], anchor: "north")
  })
}

// --- VRAIES MATRICES GÉNÉRÉES ---

#let _h_pts = (
  (10, 0),
  (14, 0),
  (15, 0),
  (16, 0),
  (17, 0),
  (2, 1),
  (5, 1),
  (7, 1),
  (13, 1),
  (16, 1),
  (18, 1),
  (21, 1),
  (28, 1),
  (4, 2),
  (9, 2),
  (15, 2),
  (17, 2),
  (18, 2),
  (3, 3),
  (13, 3),
  (17, 3),
  (25, 3),
  (29, 3),
  (1, 4),
  (2, 4),
  (9, 4),
  (29, 4),
  (5, 5),
  (7, 5),
  (11, 5),
  (14, 5),
  (16, 5),
  (22, 5),
  (27, 5),
  (6, 6),
  (14, 6),
  (22, 6),
  (28, 6),
  (1, 7),
  (11, 7),
  (12, 7),
  (21, 7),
  (23, 7),
  (0, 8),
  (5, 8),
  (23, 8),
  (26, 8),
  (27, 8),
  (1, 9),
  (3, 9),
  (4, 9),
  (6, 9),
  (7, 9),
  (8, 9),
  (11, 9),
  (19, 9),
  (21, 9),
  (26, 9),
  (12, 10),
  (15, 10),
  (20, 10),
  (29, 10),
  (2, 11),
  (12, 11),
  (20, 11),
  (24, 11),
  (28, 11),
  (0, 12),
  (3, 12),
  (8, 12),
  (10, 12),
  (18, 12),
  (19, 12),
  (24, 12),
  (25, 12),
  (4, 13),
  (8, 13),
  (20, 13),
  (22, 13),
  (23, 13),
  (26, 13),
  (0, 14),
  (6, 14),
  (9, 14),
  (10, 14),
  (13, 14),
  (19, 14),
  (24, 14),
  (25, 14),
  (27, 14),
)

#let _p_pts = (
  (0, 0),
  (1, 0),
  (3, 0),
  (4, 0),
  (7, 0),
  (8, 0),
  (10, 0),
  (13, 0),
  (14, 0),
  (2, 1),
  (3, 1),
  (9, 1),
  (10, 1),
  (11, 1),
  (12, 1),
  (13, 1),
  (0, 2),
  (1, 2),
  (2, 2),
  (4, 2),
  (7, 2),
  (8, 2),
  (9, 2),
  (10, 2),
  (11, 2),
  (12, 2),
  (2, 3),
  (3, 3),
  (4, 3),
  (11, 3),
  (12, 3),
  (13, 3),
  (14, 3),
  (0, 4),
  (2, 4),
  (3, 4),
  (5, 4),
  (7, 4),
  (9, 4),
  (11, 4),
  (14, 4),
  (0, 5),
  (2, 5),
  (3, 5),
  (4, 5),
  (7, 5),
  (8, 5),
  (9, 5),
  (10, 5),
  (12, 5),
  (13, 5),
  (14, 5),
  (2, 6),
  (3, 6),
  (8, 6),
  (9, 6),
  (11, 6),
  (3, 7),
  (4, 7),
  (5, 7),
  (6, 7),
  (7, 7),
  (8, 7),
  (10, 7),
  (11, 7),
  (13, 7),
  (1, 8),
  (2, 8),
  (3, 8),
  (8, 8),
  (9, 8),
  (12, 8),
  (0, 9),
  (5, 9),
  (9, 9),
  (11, 9),
  (0, 10),
  (3, 10),
  (4, 10),
  (7, 10),
  (8, 10),
  (10, 10),
  (11, 10),
  (12, 10),
  (13, 10),
  (0, 11),
  (1, 11),
  (3, 11),
  (4, 11),
  (5, 11),
  (6, 11),
  (11, 11),
  (14, 11),
  (2, 12),
  (4, 12),
  (6, 12),
  (8, 12),
  (9, 12),
  (11, 12),
  (12, 12),
  (14, 12),
  (3, 13),
  (4, 13),
  (5, 13),
  (7, 13),
  (8, 13),
  (10, 13),
  (11, 13),
  (13, 13),
  (14, 13),
  (1, 14),
  (2, 14),
  (9, 14),
)

// --- Composant d'illustration des vraies densités ---

#let paradoxe_densite_reel() = {
  cetz.canvas(length: 0.18cm, {
    // Réglage de l'échelle pour que ça tienne
    import cetz.draw: *

    // Fonction pour dessiner une matrice à partir de ses points
    let draw_real_matrix(offset_x, cols, rows, pts, title, col, dens_text) = {
      // Cadre
      rect((offset_x, 0), (offset_x + cols, -rows), stroke: 1.5pt + black)

      // Titre au dessus
      content((offset_x + cols / 2, 2.5), text(weight: "bold", size: 0.8em)[#title])
      // Densité en dessous
      content((offset_x + cols / 2, -rows - 2.2), text(
        fill: gray,
        weight: "bold",
        size: 0.65em,
      )[_Densité_ : #dens_text])

      // Dessin des vrais points (les '1')
      for (x, y) in pts {
        circle((offset_x + x + 0.5, -y - 0.5), radius: 0.35, fill: col, stroke: none)
      }
    }

    // --- 1. Dessin de la matrice H (0 à 30) ---
    // draw_real_matrix(0, 30, 15, _h_pts, "H (Clairsemée)", orange, "20%")

    draw_real_matrix(0, 30, 15, _h_pts, "H", orange, "20%")

    // --- 2. Zone centrale élargie pour le Pivot de Gauss ---
    let arrow_start = 32
    let arrow_end = 50
    let center_x = (arrow_start + arrow_end) / 2

    line((arrow_start, -7.5), (arrow_end, -7.5), mark: (end: "stealth", fill: black), stroke: 1.5pt + black)
    content((center_x, -5), text(size: 0.70em, style: "italic", weight: "bold")[Pivot de Gauss])

    // --- 3. Dessin de la matrice P décalée ---
    // On commence P à 52 (au lieu de 41)
    // draw_real_matrix(52, 15, 15, _p_pts, "Bloc P (Dense)", blue, "50%")
    draw_real_matrix(52, 15, 15, _p_pts, "Bloc P", blue, "50%")
  })
}

#let schema_systematique() = {
  cetz.canvas(length: 1cm, {
    import cetz.draw: *
    let w_p = 2.2
    let w_i = 2.4
    let h = 1.5

    // --- MATRICE H = [P^T | I] ---
    group({
      translate((-w_p - w_i - 1.5, 0))
      content((-0.2, h / 2), $bold(H) =$, anchor: "east")

      // 1. Les fonds colorés (sans bordure)
      rect((0, 0), (w_p, h), fill: blue.lighten(80%), stroke: none)
      rect((w_p, 0), (w_p + w_i, h), fill: gray.lighten(90%), stroke: none)

      // 2. Les lignes (une seule par intersection)
      rect((0, 0), (w_p + w_i, h), stroke: 1.5pt + black) // Contour global
      line((w_p, 0), (w_p, h), stroke: 1pt + black) // Séparation propre

      content((w_p / 2, h / 2), $bold(P)^top$, text: (fill: blue.darken(20%), weight: "bold"))
      content((w_p + w_i / 2, h / 2), $bold(I)_(n-k)$)
    })

    // Flèche centrale
    line((-1.0, h / 2), (0.7, h / 2), mark: (end: "stealth", fill: black), stroke: 1.5pt + black)

    // --- MATRICE G = [I | P] ---
    group({
      translate((2.5, 0))
      content((-0.2, h / 2), $bold(G) =$, anchor: "east")

      // 1. Les fonds colorés (sans bordure)
      rect((0, 0), (w_i, h), fill: gray.lighten(90%), stroke: none)
      rect((w_i, 0), (w_i + w_p, h), fill: blue.lighten(80%), stroke: none)

      // 2. Les lignes
      rect((0, 0), (w_i + w_p, h), stroke: 1.5pt + black) // Contour global
      line((w_i, 0), (w_i, h), stroke: 1pt + black) // Séparation propre

      content((w_i / 2, h / 2), $bold(I)_k$)
      content((w_i + w_p / 2, h / 2), $bold(P)$, text: (fill: blue.darken(20%), weight: "bold"))
    })
  })
}

#let dessiner_matrice(nom, blocs, hauteur: 1.5) = {
  cetz.canvas(length: 1cm, {
    import cetz.draw: *

    group({
      // 1. Nom de la matrice à gauche (ex: "H =")
      if nom != none {
        content((-0.2, hauteur / 2), nom, anchor: "east")
      }

      let x = 0 // Curseur pour avancer de gauche à droite

      // 2. Étape des fonds et du texte (sans aucune bordure)
      for bloc in blocs {
        let w = bloc.at("largeur", default: 2)
        let bg = bloc.at("fond", default: none)
        let txt = bloc.at("texte", default: none)

        // Dessine le fond coloré
        rect((x, 0), (x + w, hauteur), fill: bg, stroke: none)

        // Place le texte exactement au milieu du bloc
        content((x + w / 2, hauteur / 2), txt)

        x += w // On décale le curseur pour le prochain bloc
      }

      let largeur_totale = x

      // 3. Étape des lignes (pour des intersections parfaites)
      x = 0
      for i in range(blocs.len() - 1) {
        x += blocs.at(i).at("largeur", default: 2)
        // Lignes de séparation internes (plus fines)
        line((x, 0), (x, hauteur), stroke: 1pt + black)
      }

      // 4. Contour global par-dessus tout (plus épais)
      rect((0, 0), (largeur_totale, hauteur), stroke: 1.5pt + black, fill: none)
    })
  })
}

//Comp 1
#let vvals = (1, 0, 1, 0)
#let edges = ((0, 0), (1, 0), (3, 0), (0, 1), (2, 1), (3, 1), (1, 2), (2, 2))
#let graph_layout(step: 1) = {
  cetz.canvas(length: 0.85cm, {
    import cetz.draw: *
    let (vy, cy) = (4.5, 0.0)
    let vxs = (0.0, 2.5, 5.0, 7.5)
    let cxs = (1.25, 3.75, 6.25)
    let r_v = 0.45
    let s_c = 0.4
    // --- 1. LES ARÊTES ---
    for (k, (vi, ci)) in edges.enumerate() {
      let v_pos = (vxs.at(vi), vy)
      let c_pos = (cxs.at(ci), cy)
      if step == 1 {
        line(
          v_pos,
          c_pos,
          shorten-start: r_v,
          shorten-end: s_c,
          mark: (end: ">", fill: blue, size: 0.3),
          stroke: 2pt + blue,
        )
      } else if step == 2 {
        let is_err = (ci == 0 or ci == 2)
        let col = if is_err { red } else { green.darken(20%) }
        line(
          c_pos,
          v_pos,
          shorten-start: s_c,
          shorten-end: r_v,
          mark: (end: ">", fill: col, size: 0.3),
          stroke: 2pt + col,
        )
      } else {
        line(v_pos, c_pos, shorten-start: r_v, shorten-end: s_c, stroke: 1pt + gray.lighten(70%))
      }
    }
    // --- 2. LES VARIABLE NODES (VN) ---
    for i in range(4) {
      let pos = (vxs.at(i), vy)
      let is_flip = (step == 3 and i == 1)
      let col = if is_flip { red } else { blue }
      let bg = if is_flip { red.lighten(92%) } else { blue.lighten(90%) }
      let val = if is_flip { "1" } else { str(vvals.at(i)) }
      circle(pos, radius: r_v, fill: bg, stroke: (if is_flip { 2.5pt } else { 1.5pt }) + col)
      content(pos, text(weight: "bold", fill: col, size: 1.1em, val))
      content((pos.at(0), pos.at(1) + 0.8), text(size: 0.8em, fill: col.darken(0%))[$v_#i$])
      if is_flip {
        content((pos.at(0) + 0.1, pos.at(1) + 1.3), text(fill: red, weight: "bold", size: 0.7em)[FLIP !])
      }
    }
    // --- 3. LES CHECK NODES (CN) ---
    for j in range(3) {
      let pos = (cxs.at(j), cy)
      let val_base = if j == 1 { 0 } else { 1 }
      let val_num = if step == 3 and j != 1 { 0 } else { val_base }
      let val_str = str(val_num)
      let col = if step == 1 {
        orange
      } else if step == 2 {
        if val_base == 0 { green.darken(20%) } else { red }
      } else {
        if val_num == 0 { green.darken(20%) } else { red }
      }
      let bg = if step == 1 { orange.lighten(90%) } else { col.lighten(92%) }
      let is_modified = step == 3 and j != 1
      let x0 = pos.at(0) - s_c
      let x1 = pos.at(0) + s_c
      let y0 = pos.at(1) - s_c
      let y1 = pos.at(1) + s_c
      rect((x0, y0), (x1, y1), fill: bg, stroke: none)
      if is_modified {
        let ds = (paint: col, thickness: 1.3pt, dash: (array: (2pt, 3pt), phase: -0pt))
        line((x0, y0), (x1, y0), stroke: ds)
        line((x1, y0), (x1, y1), stroke: ds)
        line((x1, y1), (x0, y1), stroke: ds)
        line((x0, y1), (x0, y0), stroke: ds)
      } else {
        rect((x0, y0), (x1, y1), fill: none, stroke: (paint: col, thickness: 1.8pt))
      }
      content(pos, text(weight: "bold", fill: col, size: 1.1em, val_str))
      content((pos.at(0), pos.at(1) - 0.8), text(size: 0.8em, fill: col)[$c_#j$])
    }
  })
}

#let bf_step1_sending() = graph_layout(step: 1)
#let bf_step2_verdict() = graph_layout(step: 2)
#let bf_step3_flip() = graph_layout(step: 3)

#let schema_vote_majoritaire(n_erreurs: 3, flipped: true) = {
  cetz.canvas(length: 1.0cm, {
    import cetz.draw: *

    // Positions des CN autour du VN central
    let v_pos = (0.0, 0.0)
    let c_positions = (
      (-2.2, 1.8),
      (0.0, 2.2),
      (2.2, 1.8),
      (-2.2, -1.8),
    )
    // Statuts des CN (1=Erreur)
    let states = (1, 1, 1, 0)

    let col_err = red
    let col_ok = green.darken(10%)

    // Arêtes avec flèche CN→VN
    for (i, p) in c_positions.enumerate() {
      let col = if states.at(i) == 1 { col_err } else { col_ok }
      // VN→CN (bleu)
      line(
        v_pos,
        p,
        mark: (end: "stealth", fill: blue.lighten(20%), size: 0.16),
        stroke: 1.2pt + blue.lighten(30%),
      )
      // CN→VN (coloré)
      line(
        p,
        (v_pos.at(0) + 0.06, v_pos.at(1) + 0.06),
        mark: (end: "stealth", fill: col, size: 0.18),
        stroke: 1.8pt + col,
      )
      // Badge message
      let mx = (v_pos.at(0) + p.at(0)) / 2
      let my = (v_pos.at(1) + p.at(1)) / 2
      content(
        (mx, my),
        box(
          fill: if states.at(i) == 1 { red.lighten(88%) } else { green.lighten(88%) },
          stroke: 0.5pt + col,
          radius: 2pt,
          inset: (x: 3pt, y: 1.5pt),
        )[#text(size: 0.52em, fill: col, weight: "bold")[#if states.at(i) == 1 { [✗] } else { [✓] }]],
      )
    }

    // CN (carrés)
    for (i, p) in c_positions.enumerate() {
      let col = if states.at(i) == 1 { col_err } else { col_ok }
      let sz = 0.35
      rect(
        (p.at(0) - sz, p.at(1) - sz),
        (p.at(0) + sz, p.at(1) + sz),
        fill: col.lighten(90%),
        stroke: 1.8pt + col,
      )
      content(p, text(size: 0.72em, fill: col, weight: "bold")[#states.at(i)])
    }

    // VN central
    let r_vn = 0.52
    let col_vn = if flipped { red } else { blue }
    circle(
      v_pos,
      radius: r_vn,
      fill: col_vn.lighten(82%),
      stroke: 2.5pt + col_vn,
    )
    let val_txt = if flipped { [0] } else { [1] }
    content(v_pos, text(size: 0.95em, fill: col_vn, weight: "bold")[#val_txt])

    // Décision sous le nœud
    if flipped {
      content(
        (0, -1.0),
        box(
          fill: red.lighten(88%),
          stroke: 1.2pt + red,
          radius: 4pt,
          inset: (x: 7pt, y: 4pt),
        )[#text(fill: red, weight: "bold", size: 0.78em)[FLIP : 1 → 0]],
      )
    } else {
      content(
        (0, -1.0),
        box(
          fill: rgb("#f8f9fa"),
          stroke: 0.8pt + gray,
          radius: 4pt,
          inset: (x: 7pt, y: 4pt),
        )[#text(fill: gray.darken(40%), size: 0.72em, style: "italic")[Majorité = 3/4 ✗]],
      )
    }
  })
}

// --- Définitions de style globales ---
#let iterative_decoder_colors = (
  top: rgb("000000"), // Noir
  cn_update: rgb("ff9933"), // Orange
  vn_update: rgb("3366cc"), // Bleu
  success: rgb("339900"), // Vert
  failure: rgb("cc3333"), // Rouge
  edge: black,
)

// Définir des styles de nœuds avec de grands insets pour un espacement généreux
#let node_style = (
  cn_update: (
    stroke: iterative_decoder_colors.cn_update,
    fill: iterative_decoder_colors.cn_update.lighten(90%),
    radius: 4pt,
    inset: (x: 20pt, y: 15pt), // Grand inset horizontal et vertical
    label: (align: center, gutter: 8pt, size: 1em),
  ),
  vn_update: (
    stroke: iterative_decoder_colors.vn_update,
    fill: iterative_decoder_colors.vn_update.lighten(90%),
    radius: 4pt,
    inset: (x: 20pt, y: 15pt),
    label: (align: center, gutter: 8pt, size: 1em),
  ),
  diamond_node: (
    radius: 1pt, // Coins légèrement arrondis pour le diamant
    label: (align: center, size: 1em, gutter: 0pt), // Font légèrement plus petit pour la lisibilité
  ),
  output_node: (
    radius: 4pt,
    inset: (x: 10pt, y: 8pt), // Espacement légèrement plus compact pour les sorties
    label: (align: center, size: 1em, gutter: 4pt),
  ),
)

// Style d'arête
#let edge_style = (
  stroke: iterative_decoder_colors.edge,
  mark: (end: ">"),
)

#let schema_boucle_bf() = {
  cetz.canvas(length: 0.9cm, {
    import cetz.draw: *

    let s_fleche = (stroke: 1.5pt + black, mark: (end: "stealth", fill: black, size: 0.22))
    let col_1 = orange
    let col_2 = blue
    let col_3 = green.darken(10%)
    let col_stop = red

    // --- 1. PARAMÈTRES ---
    let w_boite = 9.2
    let h_boite = 3.2
    let espacement_titre = 8pt // Un peu réduit pour l'équilibre

    let y_init = 12.0
    let y_cn = 8.0
    let y_vn = 3.5
    let y_check = -1.0
    let y_fail = -5.5

    let w_l = 2.4
    let h_l = 1.3

    // --- 2. ANCRES (Rectangles invisibles) ---
    rect((-2.5, y_init - 0.6), (2.5, y_init + 0.6), stroke: none, name: "init")
    rect((-w_boite / 2, y_cn - h_boite / 2), (w_boite / 2, y_cn + h_boite / 2), stroke: none, name: "cn")
    rect((-w_boite / 2, y_vn - h_boite / 2), (w_boite / 2, y_vn + h_boite / 2), stroke: none, name: "vn")
    rect((-w_l, y_check - h_l), (w_l, y_check + h_l), stroke: none, name: "check")
    // Bloc succès bien large
    rect((5.8, y_check - 1.0), (12.5, y_check + 1.0), stroke: none, name: "ok")
    rect((-w_boite / 2, y_fail - 0.7), (w_boite / 2, y_fail + 0.7), stroke: none, name: "fail")

    // --- 3. DESSIN DES FLÈCHES ---
    line("init.south", "cn.north", ..s_fleche)
    line("cn.south", "vn.north", ..s_fleche)
    line("vn.south", "check.north", ..s_fleche)
    line("check.south", "fail.north", ..s_fleche)
    line("check.east", "ok.west", ..s_fleche)
    line("check.west", (-6.5, y_check), (-6.5, y_cn), "cn.west", ..s_fleche)

    // --- 4. DESSIN VISUEL ---

    // Mot reçu
    rect("init.north-west", "init.south-east", fill: black, radius: 25pt)
    content("init", text(white, weight: "bold", size: 1.1em)[Mot reçu $bold(r)$])

    // ① CN Update - Correction de l'équilibre
    rect("cn.north-west", "cn.south-east", fill: col_1.lighten(97%), stroke: 2.2pt + col_1, radius: 5pt)
    content("cn")[
      #set align(center + horizon)
      #block(breakable: false)[
        #set align(left)
        #stack(dir: ttb, spacing: espacement_titre, text(1.1em, fill: col_1, weight: "bold")[① CN Update], text(
          0.85em,
        )[Calcul du syndrome $bold(s) = bold(H)bold(r)^T$ \ Chaque $c_i$ évalue sa parité])
      ]
    ]

    // ② VN Update - Correction de l'équilibre
    rect("vn.north-west", "vn.south-east", fill: col_2.lighten(97%), stroke: 2.2pt + col_2, radius: 5pt)
    content("vn")[
      #set align(center + horizon)
      #block(breakable: false)[
        #set align(left)
        #stack(dir: ttb, spacing: espacement_titre, text(1.1em, fill: col_2, weight: "bold")[② VN Update], text(
          0.85em,
        )[Chaque $v_j$ compte ses erreurs\ Si majorité → FLIP])
      ]
    ]

    // Losange
    line(
      "check.north",
      "check.east",
      "check.south",
      "check.west",
      close: true,
      fill: green.lighten(97%),
      stroke: 2.2pt + col_3,
    )
    content("check", text(1.1em, weight: "bold")[$bold(s) = bold(0)$])

    // Succès : Largeur corrigée et texte sur une ligne
    rect("ok.north-west", "ok.south-east", fill: col_3.lighten(97%), stroke: 2.2pt + col_3, radius: 5pt)
    content("ok", text(col_3, weight: "bold", size: 1.1em)[Succès : $hat(bold(c))=bold(r)$])

    // Échec - Correction de l'équilibre
    rect("fail.north-west", "fail.south-east", fill: col_stop.lighten(97%), stroke: 2.2pt + col_stop, radius: 5pt)
    content("fail")[
      #set align(center + horizon)
      #text(col_stop, weight: "bold", size: 1.1em)[iter $>$ M $arrow.r$ Échec]
    ]

    // --- 5. ÉTIQUETTES ---
    // Oui centré sur sa flèche
    content((4.1, y_check + 0.6), text(col_3, weight: "bold", size: 1.1em)[Oui])

    content((1.2, (y_check + y_fail) / 2), text(col_stop, weight: "bold", size: 1.1em)[Non])
    content((-7.3, (y_cn + y_check) / 2), angle: 90deg, text(size: 1.1em, style: "italic", weight: "bold")[iter $+1$])
  })
}

/////
//
//
//

#let schema_llr_droite() = {
  cetz.canvas(length: 1cm, {
    import cetz.draw: *

    // Couleurs ajustées selon la logique mathématique
    let col_bit1 = red.darken(10%) // Négatif -> Bit 1
    let col_bit0 = blue.darken(10%) // Positif -> Bit 0
    let col_ax = black
    let col_mid = gray.darken(30%)

    let L = 7.0 // demi-longueur de l'axe
    let y0 = 0.0
    let y_top = 1.8

    // ── 1. Zones colorées (Corrigées) ──

    // Zone gauche (négative) -> Bit 1
    rect((-L, y0 + 0.02), (-0.05, y0 + 0.32), fill: col_bit1.lighten(92%), stroke: none)
    content((-L / 2, y0 + 0.65), text(fill: col_bit1, weight: "bold", size: 0.9em)[Bit = 1])

    // Zone droite (positive) -> Bit 0
    rect((0.05, y0 + 0.02), (L, y0 + 0.32), fill: col_bit0.lighten(92%), stroke: none)
    content((L / 2, y0 + 0.65), text(fill: col_bit0, weight: "bold", size: 0.9em)[Bit = 0])

    // ── 2. Axe horizontal ──
    line((-L, y0), (L, y0), stroke: 1.8pt + col_ax, mark: (end: "stealth", fill: col_ax, size: 0.22))
    content((L + 0.5, y0), text(size: 0.85em)[$L(v_i)$], anchor: "west")

    // ── 3. Zéro ──
    line((0, -0.2), (0, 0.2), stroke: 1.5pt + col_mid)
    content((0, -0.5), text(size: 0.8em, fill: col_mid)[0])

    // ── 4. Points exemples (Couleurs mises à jour) ──

    // -6 (Bit 1 à gauche désormais)
    let px_left = -5.0
    circle((px_left, y0), radius: 0.15, fill: col_bit1, stroke: none)
    line((px_left, y0), (px_left, y_top), stroke: (paint: col_bit1, thickness: 1pt, dash: "dashed"))
    content(
      (px_left, y_top + 0.1),
      box(fill: col_bit1.lighten(92%), stroke: 0.5pt + col_bit1, radius: 3pt, inset: 4pt)[
        #text(fill: col_bit1, weight: "bold", size: 0.75em)[$-6$]
      ],
      anchor: "south",
    )

    // +0.3 (Proche de zéro, Bit 0)
    let px_near = 0.6
    circle((px_near, y0), radius: 0.15, fill: col_bit0.lighten(40%), stroke: none)
    line((px_near, y0), (px_near, y_top - 0.7), stroke: (paint: col_bit0.lighten(20%), thickness: 1pt, dash: "dashed"))
    content(
      (px_near, y_top - 0.65),
      box(fill: white, stroke: 0.5pt + col_bit0.lighten(30%), radius: 3pt, inset: 3pt)[
        #text(fill: col_bit0.darken(20%), size: 0.7em)[$+0.3$]
      ],
      anchor: "south-west",
    )

    // +10 (Bit 0 à droite)
    let px_right = 5.8
    circle((px_right, y0), radius: 0.15, fill: col_bit0, stroke: none)
    line((px_right, y0), (px_right, y_top), stroke: (paint: col_bit0, thickness: 1pt, dash: "dashed"))
    content(
      (px_right, y_top + 0.1),
      box(fill: col_bit0.lighten(92%), stroke: 0.5pt + col_bit0, radius: 3pt, inset: 4pt)[
        #text(fill: col_bit0, weight: "bold", size: 0.75em)[$+10$]
      ],
      anchor: "south",
    )
  })
}

///2
#let schema_sum_product_dual() = {
  cetz.canvas(length: 1cm, {
    import cetz.draw: *

    let col_cn = orange.darken(0%)
    let col_vn = blue.darken(0%)
    let col_msg = gray.darken(35%)

    // --- PARTIE HAUTE : MISE À JOUR CN (PRODUIT DES TANH) ---
    group({
      let base_y = 4.0
      let cn_pos = (0.0, base_y)
      let vn_in_pos = ((-3.2, base_y + 1.3), (-3.2, base_y - 1.3))
      let vn_out_pos = (3.2, base_y)

      // 1. Check Node Central (cj)
      rect(
        (cn_pos.at(0) - 0.45, cn_pos.at(1) - 0.45),
        (cn_pos.at(0) + 0.45, cn_pos.at(1) + 0.45),
        fill: col_cn.lighten(90%),
        stroke: 2pt + col_cn,
        name: "cj_top",
      )
      content("cj_top", text(size: 0.75em, fill: col_cn, weight: "bold")[$c_j$])

      // 2. Variable Node de sortie (v0)
      circle(vn_out_pos, radius: 0.5, fill: white, stroke: 2pt + col_vn, name: "v0_top")
      content("v0_top", text(size: 0.7em, fill: col_vn, weight: "bold")[$v_0$])

      // 3. Arête de sortie : cj -> v0
      line("cj_top", "v0_top", stroke: 2.2pt + orange, mark: (end: "stealth", fill: orange, size: 0.25))
      content((1.8, base_y + 0.6), text(size: 0.8em, fill: orange.darken(0%), weight: "bold")[$m_(c_j arrow v_0)$])

      // 4. Voisins entrants (vi)
      for (i, p) in vn_in_pos.enumerate() {
        let n_name = "vin_" + str(i)
        circle(p, radius: 0.45, fill: col_vn.lighten(92%), stroke: 1.2pt + col_vn, name: n_name)
        content(n_name, text(size: 0.65em, fill: col_vn, weight: "bold")[$v_#(i + 1)$])

        line(n_name, "cj_top", stroke: 1.1pt + col_msg, mark: (end: "stealth", fill: col_msg, size: 0.2))
        content((p.at(0) + 1.4, p.at(1)), text(size: 0.75em, fill: col_msg)[$m_(v_#(i + 1) arrow c_j)$])
      }
    })

    // Séparateur discret
    line((-4.5, 1.8), (4.5, 1.8), stroke: (paint: gray.lighten(85%), dash: "dashed"))

    // --- PARTIE BASSE : MISE À JOUR VN (SOMME DES LLR) ---
    group({
      let base_y = -1.5
      let vn_pos = (0.0, base_y)
      let cn_in_pos = ((-3.2, base_y + 1.2), (-3.2, base_y - 1.2))
      let cn_out_pos = (3.2, base_y)

      // 1. Variable Node Central (vj)
      circle(vn_pos, radius: 0.55, fill: col_vn.lighten(90%), stroke: 2pt + col_vn, name: "vj_bot")
      content("vj_bot", text(weight: "bold", fill: col_vn)[$v_j$])

      // 2. Check Node de sortie (c0) - Toujours ORANGE
      rect(
        (cn_out_pos.at(0) - 0.4, cn_out_pos.at(1) - 0.4),
        (cn_out_pos.at(0) + 0.4, cn_out_pos.at(1) + 0.4),
        fill: white,
        stroke: 1.8pt + orange,
        name: "c0_bot",
      )
      content("c0_bot", text(size: 0.65em, fill: orange.darken(0%), weight: "bold")[$c_0$])

      // 3. Arête de sortie : vj -> c0
      line("vj_bot", "c0_bot", stroke: 2.2pt + blue, mark: (end: "stealth", fill: blue, size: 0.25))
      content((1.8, base_y - 0.7), text(size: 0.8em, fill: blue.darken(0%), weight: "bold")[$m_(v_j arrow c_0)$])

      // 4. Entrée Canal LLR
      let canal_top = (0.0, base_y + 2.2)
      line(canal_top, "vj_bot", stroke: 1.8pt + blue, mark: (end: "stealth", fill: blue, size: 0.22))
      content((canal_top.at(0), canal_top.at(1) + 0.3), text(
        size: 0.85em,
        fill: blue,
        weight: "bold",
      )[$L_"canal" (y_j)$])

      // 5. Check Nodes entrants
      for (i, p) in cn_in_pos.enumerate() {
        let n_name = "cin_" + str(i)
        rect(
          (p.at(0) - 0.35, p.at(1) - 0.35),
          (p.at(0) + 0.35, p.at(1) + 0.35),
          fill: orange.lighten(95%),
          stroke: 1pt + orange,
          name: n_name,
        )
        content(n_name, text(size: 0.6em, fill: orange.darken(0%))[$c_#(i + 1)$])

        line(n_name, "vj_bot", stroke: 1.1pt + col_msg, mark: (end: "stealth", fill: col_msg, size: 0.2))
        content((p.at(0) + 1.4, p.at(1)), text(size: 0.75em, fill: col_msg)[$m_(c_#(i + 1) arrow v_j)$])
      }
    })
  })
}

#let col_cn = orange
#let col_vn = blue
#let col_msg = gray.darken(50%)
#let s_len = 1.1cm

// 1. INITIALISATION
#let schema_detailed_init() = cetz.canvas(length: s_len, {
  import cetz.draw: *

  // Couleurs pures
  let col_vn_pure = blue
  let col_cn_pure = orange
  let col_msg_dark = gray.darken(50%)

  // Noeud Variable
  circle((0, 0), radius: 0.6, fill: col_vn_pure.lighten(90%), stroke: 2pt + col_vn_pure, name: "vj")
  content("vj", text(weight: "bold")[$v_j$])

  // Entrée canal
  line((0, 1.8), (0, 0.6), stroke: 2pt + blue, mark: (end: "stealth", fill: blue, size: 0.2))
  content((0, 2.1), text(fill: blue, size: 0.9em, weight: "bold")[$L_("canal")$])

  // Targets plus éloignées sur la droite (x: 4.0)
  let targets = ((4.0, 1.2), (4.0, -1.2))
  for (i, p) in targets.enumerate() {
    let name = "c" + str(i + 1)

    // Check Nodes
    rect(
      (p.at(0) - 0.4, p.at(1) - 0.4),
      (p.at(0) + 0.4, p.at(1) + 0.4),
      fill: col_cn_pure.lighten(95%),
      stroke: 1.5pt + col_cn_pure,
      name: name,
    )
    content(name, text(size: 0.7em, fill: col_cn_pure, weight: "bold")[$c_#(i + 1)$])

    // Arête
    line("vj.east", name, stroke: 1.2pt + col_msg_dark, mark: (end: "stealth", fill: col_msg_dark))

    // Position du message calculée pour être "collée" à l'arête
    // On place le texte à mi-chemin (x=2.0) avec un léger offset y
    let y_offset = if p.at(1) > 0 { 0.4 } else { -0.25 }
    content((2.0, p.at(1) * 0.5 + y_offset), text(size: 0.75em, fill: col_msg_dark)[$m_(v_j arrow c_#(i + 1))$])
  }
})

// 2. CN UPDATE
#let schema_detailed_cn() = cetz.canvas(length: s_len, {
  import cetz.draw: *
  rect((-0.5, -0.5), (0.5, 0.5), fill: col_cn.lighten(90%), stroke: 2.2pt + col_cn, name: "cj")
  content("cj", text(weight: "bold", fill: col_cn)[$c_j$])

  let ins = ((-3.0, 1.3), (-3.0, -1.3))
  for (i, p) in ins.enumerate() {
    let v_name = "vi" + str(i + 1)
    circle(p, radius: 0.45, fill: col_vn.lighten(92%), stroke: 1.2pt + col_vn, name: v_name)
    line(v_name, "cj", stroke: 1.1pt + col_msg, mark: (end: "stealth", fill: col_msg))
    content(v_name, text(size: 0.65em, fill: col_vn, weight: "bold")[$v_#(i + 1)$])
    content((p.at(0) + 1.5, p.at(1)), text(size: 0.75em, fill: col_msg)[$m_(v_#(i + 1) arrow c_j)$])
  }

  circle((3.0, 0), radius: 0.6, fill: white, stroke: 2pt + col_vn, name: "v0")
  content("v0", text(size: 0.75em, fill: col_vn, weight: "bold")[$v_0$])
  line("cj", "v0", stroke: 2.3pt + orange, mark: (end: "stealth", fill: orange, size: 0.25))
  content((1.8, 0.6), text(size: 0.85em, fill: orange, weight: "bold")[$m_(c_j arrow v_0)$])
})

// 3. VN UPDATE
#let schema_detailed_vn() = cetz.canvas(length: s_len, {
  import cetz.draw: *
  circle((0, 0), radius: 0.7, fill: col_vn.lighten(90%), stroke: 2.5pt + col_vn, name: "vj")
  content("vj", text(weight: "bold", fill: col_vn)[$v_j$])

  line((0, 2.0), (0, 0.7), stroke: 2pt + blue, mark: (end: "stealth", fill: blue, size: 0.2))
  content((0, 2.3), text(size: 0.85em, fill: blue, weight: "bold")[$L_("canal")$])

  let ins = ((-3.0, 1.2), (-3.0, -1.2))
  for (i, p) in ins.enumerate() {
    let c_name = "ci" + str(i + 1)
    rect(
      (p.at(0) - 0.4, p.at(1) - 0.4),
      (p.at(0) + 0.4, p.at(1) + 0.4),
      fill: col_cn.lighten(95%),
      stroke: 1.2pt + col_cn,
      name: c_name,
    )
    line(c_name, "vj", stroke: 1.1pt + col_msg, mark: (end: "stealth", fill: col_msg))
    content(c_name, text(size: 0.65em, fill: col_cn, weight: "bold")[$c_#(i + 1)$])
    content((p.at(0) + 1.5, p.at(1)), text(size: 0.75em, fill: col_msg)[$m_(c_#(i + 1) arrow v_j)$])
  }

  rect((2.6, -0.45), (3.4, 0.45), fill: white, stroke: 2pt + orange, name: "c0")
  content("c0", text(size: 0.7em, fill: col_cn, weight: "bold")[$c_0$])
  line("vj", "c0", stroke: 2.3pt + blue, mark: (end: "stealth", fill: blue, size: 0.25))
  content((1.7, -0.8), text(size: 0.85em, fill: blue, weight: "bold")[$m_(v_j arrow c_0)$])
})

// 4. DÉCISION FINALE
#let schema_detailed_decision() = cetz.canvas(length: s_len, {
  import cetz.draw: *
  let col_vn_dec = green

  // 1. Variable Node Central (Le bit j)
  circle((0, 0), radius: 0.75, fill: green.lighten(92%), stroke: 2.5pt + col_vn_dec, name: "vn")
  content("vn", text(weight: "bold", size: 1.1em)[$Lambda_j$])

  // 2. Entrée LLR Canal
  line((0, 1.7), (0, 0.75), stroke: 1.8pt + blue, mark: (end: "stealth", fill: blue, size: 0.15))
  content((0, 2.0), text(fill: blue, size: 0.8em, weight: "bold")[$L_"canal"$])

  // 3. Arrivées des Check Nodes (i)
  let ins = ((-2.3, 1.0), (-2.3, -1.0), (2.3, 1.0), (2.3, -1.0))
  for (i, p) in ins.enumerate() {
    let name = "call" + str(i)
    rect(
      (p.at(0) - 0.3, p.at(1) - 0.3),
      (p.at(0) + 0.3, p.at(1) + 0.3),
      fill: orange.lighten(95%),
      stroke: 1.1pt + orange,
      name: name,
    )
    line(name, "vn", stroke: 1.1pt + orange, mark: (end: "stealth", fill: orange, size: 0.12))
    content(name, text(size: 0.5em, fill: orange, weight: "bold")[$c_#i$])
  }

  // 4. ÉQUATION LAMBDA : On somme sur les voisins du bit j -> N(v_j)
  content((0, -2), text(size: 0.8em, fill: col_vn_dec)[
    $display(Lambda_j = L_("canal") + sum_(i in cal(N)(v_j)) m_(c_i arrow v_j))$
  ])

  // 5. Bloc Hard Decision
  content((3.5, 0), name: "verdict", box(fill: white, stroke: 1.5pt + black, inset: 6pt, radius: 2pt)[
    #set text(size: 0.6em)
    #set math.cases(gap: 0.3em)
    $hat(v)_j = cases(0 "si" Lambda_j > 0, 1 "si" Lambda_j < 0)$
  ])

  // 6. Liaison pointillés
  line((0.8, 0), "verdict.west", stroke: (paint: black, thickness: 1.3pt, dash: "dotted"))
})

// Schéma Min-Sum : comparaison SP vs MS + highlight du terme minimum
// #let schema_min_sum_vis() = {
//   cetz.canvas(length: 1cm, {
//     import cetz.draw: *
//
//     let col_ms   = blue.darken(15%)
//     let col_min  = red.darken(5%)
//     let col_sgn  = orange.darken(5%)
//
//     // Messages entrants (4 voisins u ≠ v)
//     let msgs = (2.1, 0.4, 3.7, 1.2)
//     let sgns = (1, -1, 1, 1)   // signes
//
//     let y_base = 0.0
//     let xs = (-4.5, -1.5, 1.5, 4.5)
//
//     // Titre colonnes
//     content((-3.0, 4.8), text(size: 0.82em, fill: col_sgn, weight: "bold")[Signes])
//     content((3.0, 4.8), text(size: 0.82em, fill: col_ms, weight: "bold")[|Amplitudes|])
//
//     for (i, x) in xs.enumerate() {
//       let m   = msgs.at(i)
//       let s   = sgns.at(i)
//       let is_min = (m == 0.4)
//       let col_box = if is_min { col_min } else { col_ms }
//
//       // Barre amplitude
//       let bar_h = m * 0.55
//       rect((x - 0.5, y_base), (x + 0.5, y_base + bar_h),
//            fill: col_box.lighten(if is_min { 70% } else { 85% }),
//            stroke: 1.5pt + col_box)
//
//       // Valeur amplitude
//       content((x, y_base + bar_h + 0.35),
//         text(size: 0.82em, fill: col_box, weight: "bold")[#m])
//
//       // Signe (XOR logic)
//       let s_txt = if s > 0 { [+] } else { [−] }
//       let s_col = col_sgn
//       content((x, -0.65),
//         box(fill: s_col.lighten(88%), stroke: 0.5pt + s_col,
//             radius: 2pt, inset: (x:6pt, y:3pt))[
//           #text(size: 0.88em, fill: s_col, weight: "bold")[#s_txt]
//         ])
//
//       // Label u_i
//       content((x, -1.3), text(size: 0.74em, fill: gray.darken(30%))[
//         $m_(#(i+1)→c)$
//       ])
//     }
//
//     // Flèche vers résultat
//     let y_res = -2.4
//     line((0, -1.6), (0, y_res + 0.3),
//          mark: (end: "stealth", fill: black, size: 0.2),
//          stroke: 1.4pt + black)
//
//     // Encadré résultat
//     content((0, y_res - 0.1),
//       box(fill: col_min.lighten(88%), stroke: 1.5pt + col_min,
//           radius: 5pt, inset: (x: 14pt, y: 8pt))[
//         #set text(size: 0.82em)
//         Signe produit : #text(fill: col_sgn, weight: "bold")[$(-1)^1 = -$]
//         #h(8pt)
//         Min : #text(fill: col_min, weight: "bold")[$0.4$]
//         #h(8pt)
//         $=>$ #text(fill: col_min, weight: "bold")[$m_(c→v) = -0.4$]
//       ])
//
//     // Étiquette "minimum"
//     content((-1.5, y_base + 0.4 * 0.55 + 0.95),
//       text(size: 0.72em, fill: col_min, style: "italic",
//            weight: "bold")[← minimum !])
//   })
// }

#let schema_min_sum_complet() = cetz.canvas(length: 1cm, {
  import cetz.draw: *

  let msgs = (2.8, 0.7, 4.2, 1.8)
  let sgns = (1, -1, 1, 1)
  let vals = ("+2.8", "-0.7", "+4.2", "+1.8")

  let col_ms = blue.darken(15%)
  let col_min = red.darken(10%)
  let col_vn_bg = blue.lighten(94%)

  // --- 1. GRAPHE DE TANNER ---
  group(name: "tanner", {
    for i in (0, 1, 2, 3) {
      let y_pos = 2.25 - i * 1.5
      line((-6.5, y_pos), (-2.0, 0), stroke: 1.2pt + gray.lighten(30%))
    }

    rect((-2.5, -0.5), (-1.5, 0.5), name: "cn", stroke: 2pt + orange, fill: orange.lighten(92%))
    content((-2.01, 0.07), text(size: 1.2em, fill: orange, weight: "bold")[$c$])

    for i in (0, 1, 2, 3) {
      let y_pos = 2.25 - i * 1.5
      circle((-6.5, y_pos), radius: 0.4, name: "v" + str(i), fill: col_vn_bg, stroke: 1.5pt + blue)
      content("v" + str(i), text(size: 0.75em, fill: blue.darken(20%), weight: "bold")[$v_#i$])

      let val = vals.at(i)
      content(((-6.5, y_pos), 42%, (-2.0, 0)), box(
        fill: col_vn_bg,
        stroke: 0.5pt + blue.lighten(60%),
        radius: 2pt,
        inset: 4pt,
      )[
        #text(size: 0.65em, fill: black.lighten(10%), weight: "bold")[#val]
      ])
    }
  })

  // --- 2. FLÈCHE GRISE ---
  line((-1.0, 0), (0.5, 0), mark: (end: "stealth", fill: gray), stroke: 1.5pt + gray)

  // --- 3. GRAPHIQUE DES BARRES ---
  group(name: "bars", {
    let base_x = 1.4
    for (i, m) in msgs.enumerate() {
      let x = base_x + i * 1.4
      let is_min = (m == 0.7)
      let col = if is_min { col_min } else { col_ms }

      rect((x - 0.4, 0), (x + 0.4, m * 0.45), fill: col.lighten(if is_min { 75% } else { 88% }), stroke: 1.5pt + col)
      content((x, m * 0.45 + 0.35), text(size: 0.7em, fill: col, weight: "bold")[#m])
      content((x, -0.7), box(fill: orange.lighten(92%), stroke: 0.5pt + orange, inset: 5pt, radius: 2pt)[
        #text(size: 0.8em, fill: orange, weight: "bold")[#(if sgns.at(i) > 0 [+] else [−])]
      ])
      if is_min { content((x, m * 0.45 + 0.8), text(size: 0.55em, fill: col, weight: "bold")[MIN]) }
    }
  })

  // --- 4. FLÈCHE ROUGE ---
  line((6.5, 0), (8.0, 0), mark: (end: "stealth", fill: col_min), stroke: 2pt + col_min)

  // --- 5. RÉSULTAT FINAL (Décalé plus à droite à x = 11.5) ---
  content((10.5, 0), box(stroke: 1.5pt + col_min, fill: col_min.lighten(96%), radius: 5pt, inset: 10pt)[
    #text(size: 0.8em, weight: "bold")[$m_(c arrow v_"cible") = -0.7$]
  ])
})

#let convergence_plot(iters, syndrms) = {
  let n = iters.len()
  let max_s = 160.0
  let W = 20.0
  let H = 15.0
  let bw = W / n

  cetz.canvas({
    import cetz.draw

    for tick in (0, 50, 100, 150) {
      let y = (tick / max_s) * H
      draw.line((0, y), (W, y), stroke: 0.5pt + gray.lighten(60%))
      draw.content((-0.8, y), text(size: 13pt)[#tick])
    }

    for i in range(n) {
      let s = syndrms.at(i)
      let bh = (s / max_s) * H
      let x0 = i * bw
      let col = if s == 0 { green.darken(20%) } else { blue }

      draw.rect(
        (x0, 0),
        (x0 + bw * 0.8, bh),
        fill: col.lighten(70%),
        stroke: 1.2pt + col,
      )

      if calc.rem(i, 2) == 0 or i == n - 1 {
        draw.content((x0 + bw * 0.4, -0.4), text(size: 13pt)[#i])
      }
    }

    draw.line((0, 0), (W + 0.8, 0), stroke: 2pt, mark: (end: "stealth"))
    draw.line((0, 0), (0, H + 0.8), stroke: 2pt, mark: (end: "stealth"))

    draw.content((W / 2, -1), text(size: 19pt, weight: "bold")[Itérations])
    draw.content((-2.2, H / 2), angle: 90deg, text(size: 19pt, weight: "bold")[Syndromes])
  })
}

#let waterfall_plot(pts_uncoded, pts_bf) = {
  cetz.canvas({
    import cetz.draw: *
    plot.plot(
      size: (22, 15),
      x-label: pad(bottom: 30pt)[#move(dy: 90pt)[#text(size: 1.3em)[$E_b/N_0$ (dB)]]],
      y-label: move(dx: -20pt, dy: -10pt)[#text(size: 1.3em)[BER]],
      x-min: 0,
      x-max: 10,
      y-min: -9,
      y-max: 0.2,
      x-tick-step: 1,
      y-tick-step: 1,
      x-grid: true,
      y-grid: true,
      y-format: y => move(dx: -5pt)[$10^(#int(y))$],
      x-format: x => move(dy: 14pt)[#text(size: 16pt)[#int(x)]],
      legend: (14.4, 12.9),
      legend-style: (padding: 0.3, stroke: gray.lighten(50%), fill: white),
      {
        plot.annotate({
          // content((4, -9.9), text(size: 1.3em)[$E_b/N_0$ (dB)])
          // content((-1, -4.5), angle: 90deg, text(size: 1.3em)[BER])
        })
        plot.add(
          pts_uncoded,
          label: [Signal non codé],
          style: (stroke: (paint: orange, thickness: 2pt, dash: "dashed")),
          mark: "+",
          mark-style: (stroke: orange, size: 0.5),
        )
        plot.add(
          pts_bf,
          label: [Bit-Flipping LDPC],
          style: (stroke: (paint: blue, thickness: 2.5pt)),
          mark: "square",
          mark-style: (stroke: blue, fill: white, size: 0.25),
        )
      },
    )
  })
}

// #let waterfall_plot(pts_uncoded, pts_bf) = {
//   cetz.canvas({
//     import cetz.draw: *
//     plot.plot(
//       size: (22, 15),
//       x-label: none,
//       y-label: none,
//       x-min: 0,
//       x-max: 10,
//       y-min: -9,
//       y-max: 0.2,
//       x-tick-step: 1,
//       y-tick-step: 1,
//       x-grid: true,
//       y-grid: true,
//       style: (
//         axes: (
//           bottom: (tick: (label: (offset: 0.2))), // Chiffres plus proches de l'axe
//           left: (tick: (label: (offset: 0.2))),
//         ),
//       ),
//       y-format: y => $10^(#int(y))$,
//       x-format: x => [#x],
//       legend: (14.4, 12.9),
//       legend-style: (padding: 0.3, stroke: gray.lighten(50%), fill: white),
//       {
//         plot.annotate({
//           // for decade in range(-9, 0) {
//           //   for minor in (2, 3, 4, 5, 6, 7, 8, 9) {
//           //     let y_pos = decade + (calc.log(minor) / calc.log(10))
//           //     if y_pos >= -9 and y_pos <= 0 {
//           //       line((0, y_pos), (8, y_pos), stroke: (paint: gray.lighten(60%), thickness: 0.3pt))
//           //     }
//           //   }
//           // }
//           content((4, -9.9), text(size: 1.3em)[$E_b/N_0$ (dB)])
//           content((-1, -4.5), angle: 90deg, text(size: 1.3em)[BER])
//         })
//         plot.add(
//           pts_uncoded,
//           label: [Signal non codé],
//           style: (stroke: (paint: orange, thickness: 2pt, dash: "dashed")),
//           mark: "+",
//           mark-style: (stroke: orange, size: 0.5),
//         )
//         plot.add(
//           pts_bf,
//           label: [Bit-Flipping LDPC],
//           style: (stroke: (paint: blue, thickness: 2.5pt)),
//           mark: "square",
//           mark-style: (stroke: blue, fill: white, size: 0.25),
//         )
//       },
//     )
//   })
// }

#let simulation_image_flow(img_orig, img_noisy, img_r05, img_r66, img_r75, img_w: 150pt, gap_top: 3.0, gap_bot: 4.0) = {
  cetz.canvas({
    import cetz.draw: *

    let w_unit = img_w / 1cm
    let y_top = 5.0
    let y_bot = -6.0

    let x_side = (w_unit + gap_top) / 2
    let x_res = w_unit + gap_bot

    // --- LIGNE 1 : ÉMISSION ET BRUIT (Texte collé en haut) ---
    content(
      (-x_side, y_top),
      [
        #set align(center)
        #set par(leading: 0pt)
        #set text(bottom-edge: "baseline") // Supprime l'espace sous les lettres
        #text(weight: "bold", size: 1.3em)[Originale]
        #v(-0.6cm)
        #box(width: img_w, stroke: 2pt + black, radius: 4pt, inset: 0pt, img_orig)
      ],
      name: "orig",
    )

    content(
      (x_side, y_top),
      [
        #set align(center)
        #set par(leading: 0pt)
        #set text(bottom-edge: "baseline")
        #text(weight: "bold", size: 1.3em, fill: red)[Reçue (Bruité)]
        #v(-0.6cm)
        #box(width: img_w, stroke: 2pt + red, radius: 4pt, inset: 0pt, img_noisy)
      ],
      name: "noisy",
    )

    // --- FLÈCHE CANAL (Ajustée pour le nouveau centre de gravité) ---
    let y_img_center = y_top - 1.4

    line(
      (-x_side + w_unit / 2, y_img_center),
      (x_side - w_unit / 2, y_img_center),
      stroke: 4pt + gray,
      mark: (end: "stealth", fill: gray, size: 0.5),
    )

    content((0, y_img_center + 0.8), text(size: 1.1em, fill: gray.darken(40%), style: "italic")[AWGN (2.3 dB)])

    // --- LIGNE 2 : COMPARAISON DES RENDEMENTS (Texte collé en bas) ---
    let targets = (
      (pos: (-x_res, y_bot), img: img_r05, rate: [$R = 1/2$], col: green.darken(20%), name: "r1"),
      (pos: (0, y_bot), img: img_r66, rate: [$R = 2/3$], col: orange, name: "r2"),
      (pos: (x_res, y_bot), img: img_r75, rate: [$R = 3/4$], col: red, name: "r3"),
    )

    for t in targets {
      content(
        t.pos,
        [
          #set align(center)
          #set par(leading: 0pt)
          #set text(top-edge: "baseline") // Supprime l'espace au-dessus des lettres
          #box(width: img_w, stroke: 2.5pt + t.col, radius: 4pt, inset: 0pt, t.img)
          #v(-0.6cm)
          #text(weight: "bold", size: 1.5em, fill: t.col)[#t.rate]
        ],
        name: t.name,
      )
    }
  })
}

#let comparison_waterfall_plot(pts_unc, pts_bf, pts_sp, pts_ms) = {
  cetz.canvas({
    import cetz.draw: *
    plot.plot(
      size: (21, 15),
      x-label: pad(bottom: 30pt)[#move(dy: 80pt)[#text(size: 16pt)[$E_b/N_0$ (dB)]]],
      y-label: move(dx: -20pt, dy: -10pt)[#text(size: 16pt)[BER]],
      x-min: 0,
      x-max: 11,
      y-min: -8,
      y-max: 0.2,
      x-tick-step: 1,
      y-tick-step: 1,
      x-grid: true,
      y-grid: true,
      y-format: y => move(dx: -5pt)[$10^(#int(y))$],
      x-format: x => move(dy: 14pt)[#text(size: 16pt)[#int(x)]],
      legend: (13.2, 11.4),
      legend-style: (padding: 0.3, stroke: gray.lighten(50%), fill: white),
      {
        plot.add(
          pts_unc,
          label: [Signal non codé],
          style: (stroke: (paint: orange, thickness: 2pt, dash: "dashed")),
          mark: "+",
          mark-style: (stroke: orange, size: 1),
        )
        plot.add(
          pts_bf,
          label: [Bit-Flipping (Hard)],
          style: (stroke: (paint: red, thickness: 2pt)),
          mark: "x",
          mark-style: (stroke: red, size: 1),
        )
        plot.add(
          pts_ms,
          label: [Min-Sum (Soft)],
          style: (stroke: (paint: green.darken(20%), thickness: 2pt)),
          mark: "triangle",
          mark-style: (stroke: green.darken(20%), fill: white, size: 0.3),
        )
        plot.add(
          pts_sp,
          label: [Sum-Product (Soft)],
          style: (stroke: (paint: blue, thickness: 2pt)),
          mark: "square",
          mark-style: (stroke: blue, fill: white, size: 1),
        )
      },
    )
  })
}
