Ajout du guide PDF HestiaCP réutilisable (HestiaCP 1.10.4)

- 14 chapitres + annexe en Markdown (chapitres/), 99 étapes
- Générateur WeasyPrint (build/build.py) : PDF générique ou
  personnalisé via --vars fichier.conf
- PDF générique + PDF personnalisé srv1.skglobalinstitute.com
- Commandes v-* vérifiées contre le code source HestiaCP 1.10.4
- 03-bis-hestiacp.md pointe vers le nouveau guide

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018J2PsVQ28oW4xEkWJpcU9G
This commit is contained in:
Zana
2026-09-10 20:01:59 +02:00
parent acbe11489c
commit 2e18724685
31 changed files with 2419 additions and 44 deletions

245
hestiacp/build/build.py Normal file
View File

@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Génère le guide PDF HestiaCP à partir des chapitres Markdown.
Usage :
python3 build/build.py # PDF générique (variables <IP>, <DOMAINE>...)
python3 build/build.py --vars mon-vps.conf # PDF personnalisé avec les valeurs du VPS
python3 build/build.py --html # garde aussi le HTML intermédiaire (debug)
Dépendances : pip install weasyprint markdown pygments
"""
import argparse
import datetime
import html
import re
import sys
from pathlib import Path
import markdown
from weasyprint import HTML
ROOT = Path(__file__).resolve().parent.parent
BUILD = ROOT / "build"
CHAPTERS = ROOT / "chapitres"
# Variables du guide : clé -> (description, exemple)
VARIABLES = {
"IP": ("Adresse IPv4 publique du VPS", "203.0.113.10"),
"IPV6": ("Adresse IPv6 publique du VPS", "2001:db8::10"),
"DOMAINE": ("Domaine principal du serveur", "exemple.com"),
"FQDN": ("Nom complet du serveur (hostname)", "srv1.exemple.com"),
"HOST": ("Nom court du serveur", "srv1"),
"USER": ("Utilisateur Linux sudo (SSH)", "jean"),
"ADMIN": ("Administrateur du panel HestiaCP", "hadmin"),
"EMAIL": ("E-mail de l'administrateur", "admin@exemple.com"),
"SSH_PORT": ("Port SSH personnalisé", "50022"),
"PANEL_PORT": ("Port du panel HestiaCP", "8083"),
"CLIENT": ("Utilisateur Hestia qui héberge les projets", "client1"),
"SITE": ("Domaine d'un projet hébergé", "monprojet.com"),
"BACKUP_HOST": ("Serveur de sauvegarde distant (SFTP)", "backup.exemple.net"),
"BACKUP_USER": ("Utilisateur SFTP de la sauvegarde", "sauvegarde"),
"BACKUP_PORT": ("Port SSH du serveur de sauvegarde", "22"),
}
TOKEN = "ZZV{}ZZ"
TOKEN_RE = re.compile(r"ZZV([A-Z0-9_]+?)ZZ")
PLACEHOLDER_RE = re.compile(r"\{\{([A-Z0-9_]+)\}\}")
CALLOUTS = {
"NOTE": ("note", "À savoir"),
"TIP": ("tip", "Astuce"),
"WARNING": ("warning", "Attention"),
"DANGER": ("danger", "Danger"),
"CHECK": ("check", "Vérification"),
}
def load_vars(path):
values = {}
for line in Path(path).read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, val = line.split("=", 1)
values[key.strip().upper()] = val.strip().strip('"').strip("'")
return values
def protect_placeholders(text):
def repl(m):
key = m.group(1)
if key not in VARIABLES:
sys.exit(f"Variable inconnue dans le guide : {{{{{key}}}}}")
return TOKEN.format(key)
return PLACEHOLDER_RE.sub(repl, text)
def render_placeholders(html_text, values):
def repl(m):
key = m.group(1)
if key in values and values[key]:
return f'<span class="var filled">{html.escape(values[key])}</span>'
return f'<span class="var">&lt;{key}&gt;</span>'
return TOKEN_RE.sub(repl, html_text)
def convert_callouts(text):
"""> [!WARNING] Titre optionnel -> bloc <div class="callout warning">."""
out, lines, i = [], text.split("\n"), 0
while i < len(lines):
m = re.match(r"^>\s*\[!(\w+)\]\s*(.*)$", lines[i])
if m and m.group(1).upper() in CALLOUTS:
kind, label = CALLOUTS[m.group(1).upper()]
title = m.group(2).strip() or label
body = []
i += 1
while i < len(lines) and lines[i].startswith(">"):
body.append(re.sub(r"^>\s?", "", lines[i]))
i += 1
out.append(f'<div class="callout {kind}" markdown="1">')
out.append(f'<p class="callout-title">{html.escape(title)}</p>\n')
out.extend(body)
out.append("</div>\n")
continue
out.append(lines[i])
i += 1
return "\n".join(out)
def slug(text, prefix):
s = re.sub(r"<[^>]+>", "", text)
s = re.sub(r"[^\w]+", "-", s.lower(), flags=re.UNICODE).strip("-")
return f"{prefix}-{s}"[:80]
def build_chapter(md_text, chap_no, label):
md_text = convert_callouts(protect_placeholders(md_text))
body = markdown.markdown(
md_text,
extensions=["fenced_code", "tables", "attr_list", "md_in_html", "codehilite", "sane_lists"],
extension_configs={"codehilite": {"guess_lang": False, "css_class": "code"}},
)
toc = []
step_counter = {"n": 0}
def h1(m):
title = m.group(1)
cid = f"chap-{chap_no}"
toc.append(("chap", cid, title, "A" if label == "Annexe" else chap_no))
return (f'<h1 id="{cid}" class="chapter-title"><span class="chap-no">{label}</span>'
f'<span class="chap-name">{title}</span></h1>')
def h2(m):
title = m.group(1)
step_counter["n"] += 1
sid = slug(title, f"s{chap_no}-{step_counter['n']}")
toc.append(("step", sid, title, chap_no))
return f'<h2 id="{sid}" class="step"><span class="box"></span>{title}</h2>'
body = re.sub(r"<h1>(.*?)</h1>", h1, body, count=1)
body = re.sub(r"<h2>(.*?)</h2>", h2, body)
return f'<section class="chapter">{body}</section>', toc
def cover(values, version, date):
personal = bool(values)
subtitle = values.get("FQDN") if personal else "Guide réutilisable pour tout VPS Ubuntu 24.04"
return f"""
<section class="cover">
<div class="cover-top">
<p class="kicker">Guide d'installation pas à pas</p>
<h1 class="cover-title">Serveur VPS<br>avec <span>HestiaCP</span></h1>
<p class="cover-sub">{html.escape(subtitle)}</p>
</div>
<div class="cover-meta">
<div><span>Panel</span><strong>HestiaCP {version}</strong></div>
<div><span>Système</span><strong>Ubuntu 24.04 LTS</strong></div>
<div><span>Pile</span><strong>Nginx · Apache · PHP-FPM · MariaDB · Exim · Dovecot · Bind</strong></div>
<div><span>Commandes vérifiées le</span><strong>{date}</strong></div>
</div>
<p class="cover-foot">Web · DNS · Mail · SSL · Sécurité · Docker · Supervision · Sauvegardes</p>
</section>"""
def vars_page(values):
rows = []
for key, (desc, example) in VARIABLES.items():
val = values.get(key, "")
cell = f'<td class="val filled">{html.escape(val)}</td>' if val else '<td class="val"></td>'
rows.append(f"<tr><td><code>&lt;{key}&gt;</code></td><td>{desc}</td>"
f"<td class=\"ex\">{example}</td>{cell}</tr>")
intro = ("Ce PDF a été généré avec les valeurs de ce serveur."
if values else
"Remplissez la dernière colonne avant de commencer. Dans tout le guide, remplacez chaque "
"<span class=\"var\">&lt;VARIABLE&gt;</span> par votre valeur. Pour obtenir un PDF déjà "
"personnalisé, voir l'annexe « Régénérer ce guide ».")
return f"""
<section class="vars-page">
<h1 class="plain">Fiche du serveur</h1>
<p>{intro}</p>
<table class="vars"><thead><tr><th>Variable</th><th>Signification</th><th>Exemple</th><th>Votre valeur</th></tr></thead>
<tbody>{''.join(rows)}</tbody></table>
<div class="legend">
<p><strong>Conventions</strong></p>
<ul>
<li>Les commandes se copient telles quelles (pas d'invite <code>$</code>). Les lignes grises qui commencent par <code>#</code> sont des commentaires.</li>
<li>Chapitres 1 à 3 : connecté en <strong>root</strong>. À partir du chapitre 4 : avec votre utilisateur <span class="var">&lt;USER&gt;</span> et <code>sudo</code>.</li>
<li>Chaque <strong>étape</strong> a une case à cocher : avancez dans l'ordre, une étape à la fois.</li>
<li>Les encadrés <em>Vérification</em> indiquent le résultat attendu avant de passer à la suite.</li>
</ul>
</div>
</section>"""
def toc_page(entries):
items = []
for kind, anchor, title, chap in entries:
title_txt = re.sub(r"<[^>]+>", "", title)
if kind == "chap":
items.append(f'<li class="toc-chap"><a href="#{anchor}"><span class="n">{chap}</span>{title_txt}</a></li>')
else:
items.append(f'<li class="toc-step"><a href="#{anchor}">{title_txt}</a></li>')
return f'<section class="toc"><h1 class="plain">Sommaire</h1><ol>{"".join(items)}</ol></section>'
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--vars", help="fichier de variables (voir vps.conf.exemple)")
ap.add_argument("--out", help="chemin du PDF de sortie")
ap.add_argument("--html", action="store_true", help="écrire aussi le HTML intermédiaire")
args = ap.parse_args()
values = load_vars(args.vars) if args.vars else {}
meta = load_vars(BUILD / "meta.conf")
version, date = meta.get("HESTIA_VERSION", "?"), meta.get("VERIFIED_ON", datetime.date.today().isoformat())
chapters, toc = [], []
for no, path in enumerate(sorted(CHAPTERS.glob("*.md")), start=1):
label = "Annexe" if path.name.startswith("99") else f"Chapitre {no}"
html_chap, entries = build_chapter(path.read_text(encoding="utf-8"), no, label)
chapters.append(html_chap)
toc.extend(entries)
body = cover(values, version, date) + vars_page(values) + toc_page(toc) + "".join(chapters)
body = render_placeholders(body, values)
doc_title = "Guide VPS HestiaCP" + (f" — {values['FQDN']}" if values.get("FQDN") else "")
page = f"""<!doctype html><html lang="fr"><head><meta charset="utf-8">
<title>{html.escape(doc_title)}</title><meta name="author" content="scriptshadow">
<link rel="stylesheet" href="style.css"></head><body>{body}</body></html>"""
if args.out:
out = Path(args.out)
elif values.get("FQDN"):
out = ROOT / f"Guide-VPS-HestiaCP-{values['FQDN']}.pdf"
else:
out = ROOT / "Guide-VPS-HestiaCP.pdf"
if args.html:
(BUILD / "guide.html").write_text(page, encoding="utf-8")
HTML(string=page, base_url=str(BUILD)).write_pdf(out)
print(f"PDF généré : {out}")
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2
hestiacp/build/meta.conf Normal file
View File

@@ -0,0 +1,2 @@
HESTIA_VERSION=1.10.4
VERIFIED_ON=10 septembre 2026

254
hestiacp/build/style.css Normal file
View File

@@ -0,0 +1,254 @@
/* ---------- Polices (embarquées, licence OFL) ---------- */
@font-face { font-family: "Inter"; font-weight: 400; src: url(fonts/Inter-400.ttf); }
@font-face { font-family: "Inter"; font-weight: 500; src: url(fonts/Inter-500.ttf); }
@font-face { font-family: "Inter"; font-weight: 600; src: url(fonts/Inter-600.ttf); }
@font-face { font-family: "Inter"; font-weight: 700; src: url(fonts/Inter-700.ttf); }
@font-face { font-family: "Inter"; font-weight: 800; src: url(fonts/Inter-800.ttf); }
@font-face { font-family: "JetBrains Mono"; font-weight: 400; src: url(fonts/JetBrainsMono-400.ttf); }
@font-face { font-family: "JetBrains Mono"; font-weight: 600; src: url(fonts/JetBrainsMono-600.ttf); }
:root {
--ink: #0f172a;
--text: #1e293b;
--muted: #64748b;
--line: #e2e8f0;
--surface: #f8fafc;
--accent: #0f766e;
--accent-2: #14b8a6;
--accent-soft: #ccfbf1;
--code-bg: #0d1117;
--var: #b45309;
--var-bg: #fef3c7;
}
/* ---------- Pages ---------- */
@page {
size: A4;
margin: 22mm 18mm 20mm 18mm;
@top-left {
content: string(chapter);
font: 500 7.5pt "Inter", sans-serif;
color: #94a3b8;
vertical-align: bottom;
padding-bottom: 4mm;
}
@top-right {
content: "HestiaCP · Ubuntu 24.04";
font: 500 7.5pt "Inter", sans-serif;
color: #94a3b8;
vertical-align: bottom;
padding-bottom: 4mm;
}
@bottom-right {
content: counter(page);
font: 700 8pt "Inter", sans-serif;
color: var(--accent);
}
@bottom-left {
content: "Guide VPS HestiaCP";
font: 400 7.5pt "Inter", sans-serif;
color: #94a3b8;
}
}
@page cover { margin: 0; @top-left { content: none; } @top-right { content: none; } @bottom-right { content: none; } @bottom-left { content: none; } }
@page front { @top-left { content: none; } @top-right { content: none; } }
html { font-family: "Inter", "DejaVu Sans", sans-serif; font-size: 9.4pt; line-height: 1.55; color: var(--text); }
body { margin: 0; }
/* ---------- Couverture ---------- */
.cover {
page: cover;
height: 297mm;
box-sizing: border-box;
padding: 34mm 22mm 20mm;
background: var(--ink);
background-image:
radial-gradient(circle at 88% 12%, rgba(20,184,166,.35) 0, rgba(20,184,166,0) 38%),
radial-gradient(circle at 5% 95%, rgba(20,184,166,.18) 0, rgba(20,184,166,0) 40%);
color: #e2e8f0;
position: relative;
}
.kicker { text-transform: uppercase; letter-spacing: .18em; font-size: 8.5pt; font-weight: 600; color: var(--accent-2); margin: 0 0 8mm; }
.cover-title { font-size: 40pt; line-height: 1.05; font-weight: 800; color: #fff; margin: 0; letter-spacing: -.02em; }
.cover-title span { color: var(--accent-2); }
.cover-sub { font-size: 13pt; color: #94a3b8; margin: 7mm 0 0; font-weight: 500; }
.cover-meta { position: absolute; left: 22mm; right: 22mm; bottom: 42mm; border-top: 1px solid #334155; padding-top: 7mm; }
.cover-meta div { display: flex; justify-content: space-between; padding: 2.2mm 0; border-bottom: 1px solid #1e293b; font-size: 9.5pt; }
.cover-meta span { color: #94a3b8; }
.cover-meta strong { color: #f1f5f9; font-weight: 600; text-align: right; max-width: 60%; }
.cover-foot { position: absolute; left: 22mm; bottom: 20mm; margin: 0; font-size: 8.5pt; letter-spacing: .06em; color: var(--accent-2); }
/* ---------- Fiche serveur & sommaire ---------- */
.vars-page, .toc { page: front; break-before: page; }
h1.plain { font-size: 22pt; font-weight: 800; color: var(--ink); margin: 0 0 5mm; letter-spacing: -.01em; }
table.vars td.val { border-bottom: 1px dashed #94a3b8; min-width: 45mm; width: 32%; }
table.vars td.val.filled { font-family: "JetBrains Mono", monospace; font-size: 8pt; color: var(--var); border-bottom: 1px solid var(--line); }
table.vars td.ex { color: var(--muted); font-size: 8pt; }
td:first-child { white-space: nowrap; }
td:first-child code { white-space: nowrap; overflow-wrap: normal; }
.legend { margin-top: 8mm; background: var(--surface); border: 1px solid var(--line); border-radius: 3mm; padding: 3mm 5mm; }
.legend p { margin: 1mm 0; }
.legend ul { margin: 1mm 0 1mm; padding-left: 5mm; }
.toc ol { list-style: none; padding: 0; margin: 0; }
.toc a { color: var(--text); text-decoration: none; }
.toc a::after { content: leader(dotted) target-counter(attr(href), page); color: var(--muted); }
.toc-chap { margin-top: 3.2mm; font-weight: 700; font-size: 10pt; color: var(--ink); break-after: avoid; }
.toc-chap a { color: var(--ink); }
.toc-chap .n { display: inline-block; width: 8mm; color: var(--accent); }
.toc-step { padding-left: 8mm; font-size: 8.6pt; line-height: 1.7; }
/* ---------- Chapitres ---------- */
.chapter { break-before: page; }
h1.chapter-title { margin: 0 0 7mm; padding-bottom: 5mm; border-bottom: 2px solid var(--ink); }
.chap-no { display: block; text-transform: uppercase; letter-spacing: .16em; font-size: 8pt; font-weight: 700; color: var(--accent); margin-bottom: 1.5mm; }
.chap-name { display: block; font-size: 21pt; font-weight: 800; color: var(--ink); line-height: 1.15; letter-spacing: -.01em; string-set: chapter content(); }
body { counter-reset: step; }
h2.step {
counter-increment: step;
font-size: 12.5pt; font-weight: 700; color: var(--ink);
margin: 8mm 0 2.5mm; padding-top: 1mm;
break-after: avoid;
}
h2.step::before {
content: "Étape " counter(step);
display: inline-block;
font-size: 7.5pt; font-weight: 700; letter-spacing: .04em;
color: #fff; background: var(--accent);
border-radius: 1.2mm; padding: .6mm 2mm; margin-right: 2.5mm;
vertical-align: 1.2pt;
}
h2.step .box { float: right; width: 3.6mm; height: 3.6mm; border: 1.3px solid #94a3b8; border-radius: .8mm; margin-top: 1mm; }
.toc-step a::before { content: "Étape " target-counter(attr(href), step) " · "; color: var(--muted); font-weight: 500; }
h3 { font-size: 10.2pt; font-weight: 700; color: var(--ink); margin: 5mm 0 1.5mm; break-after: avoid; }
h4 { font-size: 9.4pt; font-weight: 600; color: var(--accent); margin: 4mm 0 1mm; break-after: avoid; }
p { margin: 1.6mm 0 2.4mm; orphans: 3; widows: 3; }
ul, ol { margin: 1.5mm 0 2.5mm; padding-left: 5.5mm; }
li { margin: .6mm 0; }
a { color: var(--accent); text-decoration: none; }
strong { color: var(--ink); font-weight: 600; }
hr { border: 0; border-top: 1px solid var(--line); margin: 6mm 0; }
/* ---------- Code ---------- */
code, pre, .var { font-variant-ligatures: none; font-feature-settings: "liga" 0, "calt" 0; }
code { font-family: "JetBrains Mono", "DejaVu Sans Mono", monospace; font-size: 8pt; }
p code, li code, td code {
background: #eef2f7; color: #0f172a; border-radius: 1mm; padding: .2mm 1.1mm;
overflow-wrap: anywhere;
}
.code, pre {
background: var(--code-bg); border-radius: 2.2mm;
margin: 2mm 0 3.5mm; padding: 3mm 4mm;
break-inside: avoid;
}
.code pre { margin: 0; padding: 0; background: none; }
pre, .code pre {
color: #e6edf3; font-family: "JetBrains Mono", "DejaVu Sans Mono", monospace; font-size: 7.9pt; line-height: 1.55;
white-space: pre-wrap; overflow-wrap: anywhere;
}
pre code { font-size: inherit; background: none; padding: 0; color: inherit; }
/* ---------- Variables ---------- */
.var { font-family: "JetBrains Mono", monospace; font-size: .92em; color: var(--var); background: var(--var-bg); border-radius: .8mm; padding: 0 .6mm; }
pre .var, .code .var { background: rgba(251,191,36,.14); color: #fbbf24; }
.var.filled { background: none; padding: 0; }
pre .var.filled, .code .var.filled { background: none; color: #fcd34d; }
/* ---------- Tableaux ---------- */
table { width: 100%; border-collapse: collapse; margin: 2.5mm 0 4mm; font-size: 8.6pt; break-inside: auto; }
th { text-align: left; font-weight: 700; color: var(--ink); background: var(--surface); border-bottom: 1.5px solid var(--ink); padding: 1.8mm 2.2mm; }
td { border-bottom: 1px solid var(--line); padding: 1.6mm 2.2mm; vertical-align: top; }
tr { break-inside: avoid; }
/* ---------- Encadrés ---------- */
.callout { border-left: 3px solid; border-radius: 0 2mm 2mm 0; padding: 2.2mm 4mm 1.5mm; margin: 3mm 0 4mm; break-inside: avoid; }
.callout p { margin: .8mm 0 1.6mm; }
.callout .callout-title { font-weight: 700; font-size: 8pt; text-transform: uppercase; letter-spacing: .08em; margin: 0 0 1mm; }
.callout .code { margin: 1.5mm 0 2mm; }
.callout.note { background: #eff6ff; border-color: #3b82f6; } .callout.note .callout-title { color: #1d4ed8; }
.callout.tip { background: #f0fdfa; border-color: var(--accent-2); } .callout.tip .callout-title { color: var(--accent); }
.callout.warning { background: #fffbeb; border-color: #f59e0b; } .callout.warning .callout-title { color: #b45309; }
.callout.danger { background: #fef2f2; border-color: #ef4444; } .callout.danger .callout-title { color: #b91c1c; }
.callout.check { background: #f0fdf4; border-color: #22c55e; } .callout.check .callout-title { color: #15803d; }
/* ---------- Coloration syntaxique (Pygments github-dark) ---------- */
.code .c { color: #8B949E; font-style: italic } /* Comment */
.code .err { color: #F85149 } /* Error */
.code .esc { color: #E6EDF3 } /* Escape */
.code .g { color: #E6EDF3 } /* Generic */
.code .k { color: #FF7B72 } /* Keyword */
.code .l { color: #A5D6FF } /* Literal */
.code .n { color: #E6EDF3 } /* Name */
.code .o { color: #FF7B72; font-weight: bold } /* Operator */
.code .x { color: #E6EDF3 } /* Other */
.code .p { color: #E6EDF3 } /* Punctuation */
.code .ch { color: #8B949E; font-style: italic } /* Comment.Hashbang */
.code .cm { color: #8B949E; font-style: italic } /* Comment.Multiline */
.code .cp { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Preproc */
.code .cpf { color: #8B949E; font-style: italic } /* Comment.PreprocFile */
.code .c1 { color: #8B949E; font-style: italic } /* Comment.Single */
.code .cs { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Special */
.code .ge { color: #E6EDF3; font-style: italic } /* Generic.Emph */
.code .ges { color: #E6EDF3; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.code .gr { color: #FFA198 } /* Generic.Error */
.code .gh { color: #79C0FF; font-weight: bold } /* Generic.Heading */
.code .go { color: #8B949E } /* Generic.Output */
.code .gp { color: #8B949E } /* Generic.Prompt */
.code .gs { color: #E6EDF3; font-weight: bold } /* Generic.Strong */
.code .gu { color: #79C0FF } /* Generic.Subheading */
.code .gt { color: #FF7B72 } /* Generic.Traceback */
.code .g-Underline { color: #E6EDF3; text-decoration: underline } /* Generic.Underline */
.code .kc { color: #79C0FF } /* Keyword.Constant */
.code .kd { color: #FF7B72 } /* Keyword.Declaration */
.code .kn { color: #FF7B72 } /* Keyword.Namespace */
.code .kp { color: #79C0FF } /* Keyword.Pseudo */
.code .kr { color: #FF7B72 } /* Keyword.Reserved */
.code .kt { color: #FF7B72 } /* Keyword.Type */
.code .ld { color: #79C0FF } /* Literal.Date */
.code .m { color: #A5D6FF } /* Literal.Number */
.code .s { color: #A5D6FF } /* Literal.String */
.code .na { color: #E6EDF3 } /* Name.Attribute */
.code .nb { color: #E6EDF3 } /* Name.Builtin */
.code .nc { color: #F0883E; font-weight: bold } /* Name.Class */
.code .no { color: #79C0FF; font-weight: bold } /* Name.Constant */
.code .nd { color: #D2A8FF; font-weight: bold } /* Name.Decorator */
.code .ni { color: #FFA657 } /* Name.Entity */
.code .ne { color: #F0883E; font-weight: bold } /* Name.Exception */
.code .nf { color: #D2A8FF; font-weight: bold } /* Name.Function */
.code .nl { color: #79C0FF; font-weight: bold } /* Name.Label */
.code .nn { color: #FF7B72 } /* Name.Namespace */
.code .nx { color: #E6EDF3 } /* Name.Other */
.code .py { color: #79C0FF } /* Name.Property */
.code .nt { color: #7EE787 } /* Name.Tag */
.code .nv { color: #79C0FF } /* Name.Variable */
.code .ow { color: #FF7B72; font-weight: bold } /* Operator.Word */
.code .pm { color: #E6EDF3 } /* Punctuation.Marker */
.code .w { color: #6E7681 } /* Text.Whitespace */
.code .mb { color: #A5D6FF } /* Literal.Number.Bin */
.code .mf { color: #A5D6FF } /* Literal.Number.Float */
.code .mh { color: #A5D6FF } /* Literal.Number.Hex */
.code .mi { color: #A5D6FF } /* Literal.Number.Integer */
.code .mo { color: #A5D6FF } /* Literal.Number.Oct */
.code .sa { color: #79C0FF } /* Literal.String.Affix */
.code .sb { color: #A5D6FF } /* Literal.String.Backtick */
.code .sc { color: #A5D6FF } /* Literal.String.Char */
.code .dl { color: #79C0FF } /* Literal.String.Delimiter */
.code .sd { color: #A5D6FF } /* Literal.String.Doc */
.code .s2 { color: #A5D6FF } /* Literal.String.Double */
.code .se { color: #79C0FF } /* Literal.String.Escape */
.code .sh { color: #79C0FF } /* Literal.String.Heredoc */
.code .si { color: #A5D6FF } /* Literal.String.Interpol */
.code .sx { color: #A5D6FF } /* Literal.String.Other */
.code .sr { color: #79C0FF } /* Literal.String.Regex */
.code .s1 { color: #A5D6FF } /* Literal.String.Single */
.code .ss { color: #A5D6FF } /* Literal.String.Symbol */
.code .bp { color: #E6EDF3 } /* Name.Builtin.Pseudo */
.code .fm { color: #D2A8FF; font-weight: bold } /* Name.Function.Magic */
.code .vc { color: #79C0FF } /* Name.Variable.Class */
.code .vg { color: #79C0FF } /* Name.Variable.Global */
.code .vi { color: #79C0FF } /* Name.Variable.Instance */
.code .vm { color: #79C0FF } /* Name.Variable.Magic */
.code .il { color: #A5D6FF } /* Literal.Number.Integer.Long */