diff --git a/.isort.cfg b/.isort.cfg
index 24b9a93dc4f5076e8fdf86f0067797f9d97cc640..98158d882ac68662082219d224697d52f24c8512 100644
--- a/.isort.cfg
+++ b/.isort.cfg
@@ -4,4 +4,4 @@ line_length = 88
 multi_line_output = 3
 default_section = "THIRDPARTY"
 include_trailing_comma = true
-known_third_party = arrow,captcha,django,environ,faker,ics,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,wagtail,wagtailmetadata
+known_third_party = arrow,bleach,bs4,captcha,django,environ,faker,ics,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,wagtail,wagtailmetadata
diff --git a/elections2021/constants.py b/elections2021/constants.py
index b056fc832d493decf59cc17f6377125ab80b3d5b..065b7194cd287472f59ee2eab0cd6e8a4bf0feb6 100644
--- a/elections2021/constants.py
+++ b/elections2021/constants.py
@@ -146,8 +146,3 @@ TARGET_CHOICES = (
     (HOUSING, "bydlení"),
     (EDUCATION, "vzdělávání"),
 )
-
-YEARS = "years"
-MONTHS = "months"
-
-TIME_HORIZON_CHOICES = ((YEARS, "roky"), (MONTHS, "měsíce"))
diff --git a/elections2021/forms.py b/elections2021/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..39381f81874f39a2e0cd8a7fd87a78cd2523a3e5
--- /dev/null
+++ b/elections2021/forms.py
@@ -0,0 +1,62 @@
+import zipfile
+
+from django import forms
+from django.utils.text import slugify
+from wagtail.admin.forms import WagtailAdminPageForm
+
+from .parser import (
+    parse_program_html,
+    prepare_benefits,
+    prepare_faq,
+    prepare_horizon,
+    prepare_point,
+)
+
+
+class ProgramPointPageForm(WagtailAdminPageForm):
+    import_file = forms.FileField(label="soubor s programem", required=False)
+
+    def clean(self):
+        cleaned_data = super().clean()
+
+        # extract data from ZIP file with HTML export
+        import_file = cleaned_data["import_file"]
+        if import_file:
+            try:
+                with zipfile.ZipFile(import_file) as archive:
+                    name = archive.namelist()[0]
+                    data = parse_program_html(archive.read(name))
+            except zipfile.BadZipFile:
+                self.add_error("import_file", "Vadný ZIP soubor. Nelze rozbalit.")
+            cleaned_data["_import_data"] = data
+        else:
+            cleaned_data["_import_data"] = None
+
+        return cleaned_data
+
+    def save(self, commit=True):
+        page = super().save(commit=False)
+
+        if self.cleaned_data["_import_data"]:
+            point = prepare_point(self.cleaned_data["_import_data"]["sekce"])
+            benefits = prepare_benefits(self.cleaned_data["_import_data"]["benefity"])
+
+            page.title = point["nadpis"]
+            page.slug = slugify(page.title)
+            page.annotation = point["anotace"]
+            page.problem = point["problem"]
+            page.context = point["kontext-problemu"]
+            page.ideal = point["ideal"]
+            page.proposal = point["navrhovana-opatreni"]
+            page.already_done = point["co-jsme-uz-udelali"]
+            page.sources = point["zdroje"]
+            text, number, unit = prepare_horizon(point["casovy-horizont"])
+            page.time_horizon_text = text
+            page.time_horizon_number = number
+            page.time_horizon_unit = unit
+            page.faq = prepare_faq(point["faq"])
+
+        if commit:
+            page.save()
+
+        return page
diff --git a/elections2021/migrations/0006_auto_20210511_0048.py b/elections2021/migrations/0006_auto_20210511_0048.py
new file mode 100644
index 0000000000000000000000000000000000000000..da6f5e1dd0941d7f9692f61b921a14f7a06a5145
--- /dev/null
+++ b/elections2021/migrations/0006_auto_20210511_0048.py
@@ -0,0 +1,73 @@
+# Generated by Django 3.1.7 on 2021-05-10 22:48
+
+import wagtail.core.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("elections2021", "0005_auto_20210505_0945"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="already_done",
+            field=wagtail.core.fields.RichTextField(
+                blank=True, null=True, verbose_name="co jsme už udělali"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="annotation",
+            field=wagtail.core.fields.RichTextField(
+                blank=True, null=True, verbose_name="anotace"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="context",
+            field=wagtail.core.fields.RichTextField(
+                blank=True, null=True, verbose_name="kontext problému"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="ideal",
+            field=wagtail.core.fields.RichTextField(
+                blank=True, null=True, verbose_name="ideál"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="problem",
+            field=wagtail.core.fields.RichTextField(
+                blank=True, null=True, verbose_name="problém"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="proposal",
+            field=wagtail.core.fields.RichTextField(
+                blank=True, null=True, verbose_name="navrhovaná opatření"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="sources",
+            field=wagtail.core.fields.RichTextField(
+                blank=True, null=True, verbose_name="zdroje"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="elections2021programpointpage",
+            name="time_horizon_unit",
+            field=models.CharField(
+                blank=True,
+                max_length=20,
+                null=True,
+                verbose_name="časový horizont jednotka",
+            ),
+        ),
+    ]
diff --git a/elections2021/models.py b/elections2021/models.py
index e5e1a22a5419d04ccd93c96940f3e25b5a4a8cde..194375146194eadd6a279129797c56e1f7acde6b 100644
--- a/elections2021/models.py
+++ b/elections2021/models.py
@@ -41,11 +41,10 @@ from .constants import (
     STANDARD_FEATURES,
     STYLE_CHOICES,
     STYLE_CSS,
-    TIME_HORIZON_CHOICES,
     TOP_CANDIDATES_NUM,
     WHITE,
-    YEARS,
 )
+from .forms import ProgramPointPageForm
 
 NO_SEARCH_IMAGE_USE_PHOTO = (
     "Pokud není zadán <strong>Search image</strong>, použije se <strong>hlavní "
@@ -520,11 +519,19 @@ class QuestionBlock(blocks.StructBlock):
 class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
     ### FIELDS
 
-    annotation = RichTextField("anotace", features=RESTRICTED_FEATURES)
-    problem = RichTextField("problém", features=RESTRICTED_FEATURES)
-    context = RichTextField("kontext problému", features=RESTRICTED_FEATURES)
-    ideal = RichTextField("ideál", features=RESTRICTED_FEATURES)
-    proposal = RichTextField("navrhovaná opatření", features=EXTRA_FEATURES)
+    annotation = RichTextField(
+        "anotace", blank=True, null=True, features=RESTRICTED_FEATURES
+    )
+    problem = RichTextField(
+        "problém", blank=True, null=True, features=RESTRICTED_FEATURES
+    )
+    context = RichTextField(
+        "kontext problému", blank=True, null=True, features=RESTRICTED_FEATURES
+    )
+    ideal = RichTextField("ideál", blank=True, null=True, features=RESTRICTED_FEATURES)
+    proposal = RichTextField(
+        "navrhovaná opatření", blank=True, null=True, features=EXTRA_FEATURES
+    )
     time_horizon_text = RichTextField(
         "časový horizont textově", blank=True, null=True, features=STANDARD_FEATURES
     )
@@ -532,13 +539,12 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
         "časový horizont číslo", blank=True, null=True
     )
     time_horizon_unit = models.CharField(
-        "časový horizont jednotka",
-        choices=TIME_HORIZON_CHOICES,
-        default=YEARS,
-        max_length=6,
+        "časový horizont jednotka", max_length=20, blank=True, null=True
     )
-    already_done = RichTextField("co jsme už udělali", features=EXTRA_FEATURES)
-    sources = RichTextField("zdroje", features=STANDARD_FEATURES)
+    already_done = RichTextField(
+        "co jsme už udělali", blank=True, null=True, features=EXTRA_FEATURES
+    )
+    sources = RichTextField("zdroje", blank=True, null=True, features=STANDARD_FEATURES)
 
     faq = StreamField(
         [("question", QuestionBlock())],
@@ -635,7 +641,7 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
         MultiFieldPanel(
             [
                 FieldPanel("time_horizon_number"),
-                FieldPanel("time_horizon_unit", widget=forms.RadioSelect),
+                FieldPanel("time_horizon_unit"),
                 FieldPanel("time_horizon_text"),
             ],
             "časový horizont",
@@ -658,7 +664,9 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
     ]
 
     faq_panels = [StreamFieldPanel("faq")]
+
     related_panels = [StreamFieldPanel("related_points")]
+
     weights_panels = [
         MultiFieldPanel(
             [
@@ -722,6 +730,19 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
         ),
     ]
 
+    import_panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel("import_file"),
+                HelpPanel(
+                    'Soubor z Goodle Docs stáhněte jako "Webová stránka (komprimovaný '
+                    ' HTML soubor)" a ten nahrajte do formuláře.'
+                ),
+            ],
+            "import programového bodu",
+        ),
+    ]
+
     edit_handler = TabbedInterface(
         [
             ObjectList(content_panels, heading=gettext_lazy("Content")),
@@ -729,6 +750,7 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
             ObjectList(faq_panels, heading="FAQ"),
             ObjectList(related_panels, heading="související"),
             ObjectList(weights_panels, heading="váhy"),
+            ObjectList(import_panels, heading="import"),
         ]
     )
 
@@ -739,5 +761,7 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
 
     ### OTHERS
 
+    base_form_class = ProgramPointPageForm
+
     class Meta:
         verbose_name = "Programový bod"
diff --git a/elections2021/parser.py b/elections2021/parser.py
new file mode 100644
index 0000000000000000000000000000000000000000..5440f507a5e089419bcc757bd94cf57d27afc7c2
--- /dev/null
+++ b/elections2021/parser.py
@@ -0,0 +1,282 @@
+import json
+import re
+import sys
+import urllib
+from collections import defaultdict
+
+import bleach
+import bs4
+from django.utils.text import slugify
+
+KNOWN_KEYS = [
+    "nadpis",
+    "anotace",
+    "problem",
+    "kontext-problemu",
+    "ideal",
+    "navrhovana-opatreni",
+    "casovy-horizont",
+    "co-jsme-uz-udelali",
+    "faq",
+    "souvisejici-body",
+    "zdroje",
+]
+
+
+def parse_program_html(fp):
+    # Načteme celý dokument.
+    html = bs4.BeautifulSoup(fp, "html5lib")
+
+    # Vyházíme odkazy na komentáře.
+    for cmnt in html.select('*[id^="cmnt"]'):
+        cmnt.parent.extract()
+
+    # Bod má svůj pracovní název
+    nazev_bodu = html.select_one("h1, h2, h3").text.strip()
+
+    # Bod má své pojmenované sekce.
+    bod = {}
+
+    SEKCE = [
+        "Nadpis",
+        "Anotace",
+        "Problém",
+        "Kontext problému",
+        "Ideál",
+        "Navrhovaná opatření",
+        "Časový horizont",
+        "FAQ",
+        "Související body",
+        "Zdroje",
+        "Co jsme už udělali",
+    ]
+
+    # Tabulka benefitů má své pojmenované cílové skupiny.
+    benefity = {}
+
+    # Chceme očesat HTML na výstupu a nechat jenom tyto atributy.
+    ATRIBUTY = set(["id", "href"])
+
+    # Dokument má právě dvě tabulky. První je s bodem a druhá s jeho benefity.
+    bod_html, bene_html = html.select("table")
+
+    # Z CSS je potřeba vyvodit, které třídy odpovídají tučnému textu a
+    # které superskriptu. Protože Google.
+
+    strong = []
+    sup = []
+
+    for style in html.select("style"):
+        strong = re.findall(r"(\.c[0-9]+)\{[^{]*font-weight:700", style.text)
+        sup = re.findall(r"(\.c[0-9]+)\{[^{]*vertical-align:super", style.text)
+
+    assert strong, "Nenašel jsem styl pro tučný text"
+    assert sup, "Nenašel jsem styl pro superskript"
+
+    def vycisti(sekce):
+        sekce.name = "div"
+        sekce.attrs.clear()
+
+        # Nahradíme třídy nativními HTML prvky.
+        for tag in sekce.select(", ".join(sup)):
+            tag.name = "sup"
+
+        for tag in sekce.select(", ".join(strong)):
+            tag.name = "strong"
+
+        # Zbavíme se <span>ů.
+        for tag in sekce.select("span"):
+            tag.unwrap()
+
+        # Ořízneme nežádoucí atributy.
+        for tag in sekce.find_all():
+            for attr in list(tag.attrs):
+                if attr not in ATRIBUTY:
+                    del tag.attrs[attr]
+
+                if tag.name != "a" and attr == "id":
+                    del tag.attrs[attr]
+
+        # Ořízneme prázdné tagy.
+        for tag in sekce.find_all():
+            if tag.text == "" and tag.name not in ["br", "hr"]:
+                tag.extract()
+
+        # Opravíme odkazy, které se nakazily Googlem.
+        for tag in sekce.select("*[href]"):
+            _proto, _loc, _path, query, _frag = urllib.parse.urlsplit(tag.attrs["href"])
+            qs = urllib.parse.parse_qs(query)
+
+            if "q" in qs:
+                tag.attrs["href"] = qs["q"][0]
+
+        # Opravíme odkazy, které se nakazily Facebookem.
+        for tag in sekce.select("*[href]"):
+            proto, loc, path, query, frag = urllib.parse.urlsplit(tag.attrs["href"])
+            qs = urllib.parse.parse_qs(query)
+
+            if "fbclid" in qs:
+                del qs["fbclid"]
+                query = urllib.parse.urlencode(qs, doseq=True)
+                tag.attrs["href"] = urllib.parse.urlunsplit(
+                    (proto, loc, path, query, frag)
+                )
+
+        # Spojíme po sobě následující prvky některých typů.
+        for fst in sekce.select("ul, sup, strong"):
+            if fst.parent is None:
+                continue
+
+            snd = fst.next_sibling
+
+            while snd is not None:
+                if snd.name == fst.name:
+                    snd.extract()
+                    for child in snd:
+                        fst.append(child)
+                else:
+                    break
+
+                snd = fst.next_sibling
+
+    # Nejprve zpracujeme bod.
+    radky = list(bod_html.select("tr"))
+
+    for radek in radky[1:]:
+        nazev, sekce = radek.select("td")
+
+        nazev = nazev.text
+        nazev = nazev.strip(" \u00a0\t\r\n:")
+        nazev = re.sub("[ \u00a0]+", " ", nazev)
+
+        if nazev not in SEKCE:
+            print("Přebývá neznámá sekce: {!r}".format(nazev), file=sys.stderr)
+
+        vycisti(sekce)
+        bod[nazev] = sekce
+
+    for nazev in SEKCE:
+        if nazev not in bod:
+            print("Chybí povinná sekce {!r}".format(nazev), file=sys.stderr)
+
+    # Benefity
+
+    for radek in bene_html.select("tr")[1:]:
+        cilovka, benefit, _info = radek.select("td")
+
+        cilovka = cilovka.text
+        cilovka = re.sub(r"\(.*\)", "", cilovka)
+        cilovka = cilovka.strip(" \u00a0\t\r\n:")
+        cilovka = re.sub("[ \u00a0]+", " ", cilovka)
+
+        vycisti(benefit)
+
+        if benefit.text.strip() != "":
+            benefity[cilovka] = benefit
+
+    # Pro případnou zběžnou kontrolu:
+
+    # print("<h1>" + nazev_bodu + "</h1>")
+
+    # for nazev, sekce in bod.items():
+    #     print("<hr/>")
+    #     print("<h2>" + nazev + "</h2>")
+    #     print(sekce)
+
+    # print("<hr/>")
+    # print("<h2>Benefity</h2>")
+    # for cilovka, benefit in benefity.items():
+    #     print("<h3>" + cilovka + "</h3>")
+    #     print(benefit)
+
+    return {
+        "nazev": nazev_bodu,
+        "sekce": {nazev: str(sekce) for nazev, sekce in bod.items()},
+        "benefity": {cilovka: str(benefit) for cilovka, benefit in benefity.items()},
+    }
+
+
+def strip_html(value):
+    return bleach.clean(value, tags=[], attributes={}, styles=[], strip=True)
+
+
+def replace_tags(value):
+    value = value.replace("<div>", "")
+    value = value.replace("</div>", "")
+    if not value.startswith("<p>"):
+        value = f"<p>{value}</p>"
+    return value
+
+
+def set_fancy_lists(value):
+    value = value.replace("<ul>", '<ul class="unordered-list unordered-list-checks">')
+    value = value.replace("<li>", '<li class="mb-4">')
+    return value
+
+
+def clean_point(point):
+    out = {}
+
+    for old_key, val in point.items():
+        key = slugify(old_key)
+
+        if key not in KNOWN_KEYS:
+            raise ValueError(f"Unknown key: {old_key}")
+
+        if key in ["nadpis"]:
+            out[key] = strip_html(val)
+        else:
+            out[key] = replace_tags(val)
+
+    return out
+
+
+def prepare_faq(value):
+    soup = bs4.BeautifulSoup(value, "html.parser")
+    questions = defaultdict(list)
+    for tag in soup.children:
+        if tag.strong:
+            key = tag.strong.string
+        else:
+            questions[key].append(str(tag))
+
+    data = []
+    for key, val in questions.items():
+        data.append(
+            {"type": "question", "value": {"question": key, "answer": "".join(val)}}
+        )
+
+    return json.dumps(data)
+
+
+def prepare_horizon(value):
+    raw = strip_html(value)
+    m = re.match(r"^(\d+)\s(\w+)$", raw)
+    if m:
+        return None, m.group(1), m.group(2)
+    return value, None, None
+
+
+def print_preview(point):
+    print("")
+    for key, val in point.items():
+        print(key, ":", val[:120])
+
+
+def print_full(point):
+    for key, val in point.items():
+        print("")
+        print(key)
+        print("-" * 100)
+        print(val)
+        print("-" * 100)
+
+
+def prepare_point(source):
+    point = clean_point(source)
+    # print_full(point)
+    return point
+
+
+def prepare_benefits(benefits):
+    return None
diff --git a/elections2021/templates/elections2021/_question_block.html b/elections2021/templates/elections2021/_question_block.html
index 0be0c8a2536d1c32c9e415becb89df647ddf6281..0aa83c73aa42d0f87976c154a771011c72b432bf 100644
--- a/elections2021/templates/elections2021/_question_block.html
+++ b/elections2021/templates/elections2021/_question_block.html
@@ -1,4 +1,4 @@
-{% load wagtailcore_tags %}
+{% load wagtailcore_tags elections2021_extras %}
 <div class="accordeon-row">
   <div class="accordeon-row-head" onclick="if(this.parentElement.classList.contains('accordeon-row--open')) this.parentElement.classList.remove('accordeon-row--open'); else this.parentElement.classList.add('accordeon-row--open');">
     <h3 class="accordeon-row-heading head-alt-xs">{{ block.value.question }}</h3>
@@ -6,7 +6,7 @@
   </div>
   <div class="accordeon-row-body" style="max-height: 216px;">
     <div>
-      <p>{{ block.value.answer|richtext }}</p>
+      <p>{{ block.value.answer|richtext|format_sources }}</p>
     </div>
   </div>
 </div>
diff --git a/elections2021/templates/elections2021/elections2021_program_point_page.html b/elections2021/templates/elections2021/elections2021_program_point_page.html
index de2878295ac3b356a024d80574720517adcaa7c9..34158efa0aebe17c68edd97192a5e22ee0cf454e 100644
--- a/elections2021/templates/elections2021/elections2021_program_point_page.html
+++ b/elections2021/templates/elections2021/elections2021_program_point_page.html
@@ -1,5 +1,5 @@
 {% extends "elections2021/base.html" %}
-{% load wagtailcore_tags wagtailimages_tags %}
+{% load wagtailcore_tags wagtailimages_tags elections2021_extras %}
 
 {% block content %}
 <div class="container container-default container-collapsible pt-4 pb-20 container-collapsible-open">
@@ -27,7 +27,7 @@
       <div class="mt-10 md:mt-12">
         <h1 class="head-alt-md md:head-alt-lg head-alt-highlighted">{{ page.title }}</h1>
         <h2 class="head-alt-sm md:head-alt-md mb-20 mt-9">
-          <div class="leading-tight">{{ page.annotation|richtext }}</div>
+          <div class="leading-tight">{{ page.annotation|richtext|format_sources }}</div>
         </h2>
       </div>
     </header>
@@ -38,18 +38,18 @@
         <div class="problem-inner content-block">
           <h2 class="head-alt-sm md:head-alt-md mb-9">Problém</h2>
           <p class="text-base mb-8 para">
-            {{ page.problem|richtext }}
+            {{ page.problem|richtext|format_sources }}
           </p>
           <h3 class="head-alt-xs md:head-alt-sm mb-9">Kontext problému</h3>
           <p class="text-base mb-8 para">
-            {{ page.context|richtext }}
+            {{ page.context|richtext|format_sources }}
           </p>
         </div>
         <div class="ideal-check"><i class="ico--check text-xs sm:text-xl text-black"></i></div>
         <div class="ideal-inner bg-lemon content-block">
           <h2 class="head-alt-sm md:head-alt-md mb-9">Ideál</h2>
           <p class="text-base">
-            {{ page.ideal|richtext }}
+            {{ page.ideal|richtext|format_sources }}
           </p>
         </div>
       </section>
@@ -57,7 +57,7 @@
 
     <h3 class="head-alt-base mb-8">Navrhovaná opatření:</h3>
     <div class="content-block">
-      {{ page.proposal|richtext }}
+      {{ page.proposal|richtext|format_sources }}
     </div>
 
     <div class="grid grid-cols-1 gap-8 my-20">
@@ -75,7 +75,7 @@
                     <b data-value="{{ page.time_horizon_number }}" class="flip-card__back"></b>
                     <b data-value="{{ page.time_horizon_number }}" class="flip-card__back-bottom"></b>
                   </span>
-                  <span class="flip-clock__slot font-alt text-2xl">{{ page.get_time_horizon_unit_display }}</span>
+                  <span class="flip-clock__slot font-alt text-2xl">{{ page.time_horizon_unit }}</span>
                 </span>
               </div>
             </div>
@@ -87,7 +87,7 @@
         <div class="card shadow-none bg-grey-125">
           <div class="card__body content-block">
             <h3 class="card-headline mb-8">Časový horizont</h3>
-            <p class="card-body-text para">{{ page.time_horizon_text|richtext }}</p>
+            <p class="card-body-text para">{{ page.time_horizon_text|richtext|format_sources }}</p>
           </div>
         </div>
       {% endif %}
@@ -176,7 +176,7 @@
 
     <h3 class="head-alt-base mb-10">Co už jsme udělali:</h3>
     <div class="content-block">
-      {{ page.already_done|richtext }}
+      {{ page.already_done|richtext|format_sources }}
     </div>
 
     <h3 class="head-alt-base mb-8 mt-20">FAQ</h3>
@@ -185,6 +185,7 @@
     {% for block in page.faq %}
       {% include_block block %}
     {% endfor %}
+    </div>
 
     <script>
       document.addEventListener("DOMContentLoaded", setMaxHeights);
@@ -203,111 +204,61 @@
     </script>
 
     {% comment %}
-    <h3 id="relatedpoints" class="head-alt-base mb-8 mt-20">Související body:</h3>
-    <div class="">
-      <div class="__js-root"><div><div class="VueCarousel article-card-list pb-8"><div class="VueCarousel-wrapper"><div class="VueCarousel-inner" style="transform: translate(0px, 0px); transition: transform 0.5s ease 0s; flex-basis: 868px; visibility: visible; height: auto;"><div tabindex="-1" role="tabpanel" class="VueCarousel-slide VueCarousel-slide-active VueCarousel-slide-center"><div class="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-8"><div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. </p></div></div></div></div> <div tabindex="-1" role="tabpanel" class="VueCarousel-slide" aria-hidden="true"><div class="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-8"><div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. </p></div></div></div></div></div></div> <div data-v-453ad8cd="" class="VueCarousel-navigation"><button data-v-453ad8cd="" type="button" aria-label="Previous page" tabindex="-1" class="VueCarousel-navigation-button VueCarousel-navigation-prev VueCarousel-navigation--disabled" style="padding: 8px; margin-right: -8px;">◀</button> <button data-v-453ad8cd="" type="button" aria-label="Next page" tabindex="0" class="VueCarousel-navigation-button VueCarousel-navigation-next" style="padding: 8px; margin-left: -8px;">▶</button></div> <div data-v-438fd353="" class="VueCarousel-pagination" style=""><div data-v-438fd353="" role="tablist" class="VueCarousel-dot-container" style="margin-top: 20px;"><button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 0" value="Item 0" aria-label="Item 0" aria-selected="true" class="VueCarousel-dot VueCarousel-dot--active" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);"></button><button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 1" value="Item 1" aria-label="Item 1" aria-selected="false" class="VueCarousel-dot" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);"></button></div></div></div></div></div>
-    </div>
-    <h3 class="head-alt-base mb-3 mt-20">Které části jsou závazné</h3>
-    <p><a href="#" class="text-acidgreen underline">Zjistit více</a></p>
-
-    <h3 class="head-alt-base mb-8 mt-8">Zdroje:</h3>
-
-    <div id="source1" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference1">[1]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source2" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference2">[2]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <p>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</p>
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source3" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference3">[3]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <p>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</p>
-        <blockquote class="quote quote-pirati-stan">
-          „Curabitizzle fo shizzle diam quizzle nisi nizzle mollizzle. Suspendisse boofron. Morbi odio. Sure pizzle. Crazy orci. Shut the shizzle up maurizzle get down get down, check out this a, go to hizzle sit amizzle, malesuada izzle, pede. Pellentesque gravida. Vestibulizzle check it out mi, volutpat izzle, shiz sed, shiznit sempizzle, da bomb. Funky fresh in ipsum. Da bomb volutpat felis vizzle daahng dawg. Crizzle quis dope izzle fo shizzle my ni.“
-        </blockquote>
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source4" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference4">[4]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source5" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference5">[5]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source6" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference6">[6]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source7" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference7">[7]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source8" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference8">[8]</a></div>
-
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
+    {% if page.related_points %}
+      <h3 id="relatedpoints" class="head-alt-base mb-8 mt-20">Související body:</h3>
+      <div class="">
+        <div class="__js-root">
+          <div>
+            <div class="VueCarousel article-card-list pb-8">
+              <div class="VueCarousel-wrapper">
+                <div class="VueCarousel-inner" style="transform: translate(0px, 0px); transition: transform 0.5s ease 0s; flex-basis: 868px; visibility: visible; height: auto;">
+                  <div tabindex="-1" role="tabpanel" class="VueCarousel-slide VueCarousel-slide-active VueCarousel-slide-center">
+                    <div class="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-8">
+
+                      <div class="card shadow-none bg-grey-125 card--hoveractive">
+                        <div class="card__body">
+                          <h1 class="card-headline mb-8">
+                            <a href="#">Otevřenost a zapojení občanů</a>
+                          </h1>
+                          <p class="card-body-text">Short description</p>
+                        </div>
+                      </div>
+
+                      <div class="card shadow-none bg-grey-125 card--hoveractive">
+                        <div class="card__body">
+                          <h1 class="card-headline mb-8">
+                            <a href="#">Otevřenost a zapojení občanů</a>
+                          </h1>
+                          <p class="card-body-text">Short description</p>
+                        </div>
+                      </div>
+
+                    </div>
+                  </div>
+                </div>
+              </div>
+              <div data-v-453ad8cd="" class="VueCarousel-navigation">
+                <button data-v-453ad8cd="" type="button" aria-label="Previous page" tabindex="-1" class="VueCarousel-navigation-button VueCarousel-navigation-prev VueCarousel-navigation--disabled" style="padding: 8px; margin-right: -8px;">◀</button>
+                <button data-v-453ad8cd="" type="button" aria-label="Next page" tabindex="0" class="VueCarousel-navigation-button VueCarousel-navigation-next" style="padding: 8px; margin-left: -8px;">▶</button>
+              </div>
+              <div data-v-438fd353="" class="VueCarousel-pagination" style="">
+                <div data-v-438fd353="" role="tablist" class="VueCarousel-dot-container" style="margin-top: 20px;">
+                  <button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 0" value="Item 0" aria-label="Item 0" aria-selected="true" class="VueCarousel-dot VueCarousel-dot--active" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);"></button>
+                  <button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 1" value="Item 1" aria-label="Item 1" aria-selected="false" class="VueCarousel-dot" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);"></button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
       </div>
-    </div>
-    <div id="source9" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference9">[9]</a></div>
+    {% endif %}
+    {% endcomment %}
 
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    <div id="source10" class="source leading-loose text-sm">
-      <div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference10">[10]</a></div>
+    <h3 class="head-alt-base mb-3 mt-20">Které části jsou závazné</h3>
+    <p><a href="#" class="text-acidgreen underline">Zjistit více</a></p>
 
-      <div class="inline-block align-top max-w-2xl">
-        <a href="mailto:example@example.com" class="text-fxactivecolor underline ">
-          <span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
-        </a>
-      </div>
-    </div>
-    {% endcomment %}
+    <h3 id="zdroje" class="head-alt-base mb-8 mt-8">Zdroje:</h3>
+    {{ page.sources|richtext|format_sources_block }}
 
   </div>
 
diff --git a/elections2021/templatetags/__init__.py b/elections2021/templatetags/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/elections2021/templatetags/elections2021_extras.py b/elections2021/templatetags/elections2021_extras.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c257f2e6e6fe1b15f1c0a255e160f495ae14049
--- /dev/null
+++ b/elections2021/templatetags/elections2021_extras.py
@@ -0,0 +1,46 @@
+import re
+
+import bs4
+from django import template
+from django.template.defaultfilters import stringfilter
+
+register = template.Library()
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def format_sources_block(value):
+    soup = bs4.BeautifulSoup(value, "html.parser")
+    out = []
+
+    for item in soup.children:
+        for a in item.find_all("a"):
+            a["class"] = "text-fxactivecolor underline"
+
+        text = "".join(str(c) for c in item.contents)
+        number = ""
+
+        m = re.match(r"^\[(\d+)\]\s(.+)", text)
+        if m:
+            number = m.group(1)
+            text = m.group(2)
+
+        out.append(
+            f"""
+            <div id="zdroj{number}" class="source leading-loose text-sm">
+              <div class="inline-block align-top w-9 text-fxactivecolor">[{number}]</div>
+              <div class="inline-block align-top max-w-2xl">{text}</div>
+            </div>
+            """
+        )
+
+    return "".join(out)
+
+
+@register.filter(is_safe=True)
+@stringfilter
+def format_sources(value):
+    soup = bs4.BeautifulSoup(value, "html.parser")
+    for sup in soup.find_all("sup"):
+        sup.wrap(soup.new_tag("a", id="reference", href="#zdroje"))
+    return str(soup)
diff --git a/requirements/base.in b/requirements/base.in
index 76b21d69f435b64fc1ca293e3d035fe00ee95e3b..ce93dbec40607f2d97103a454f5ab3c3265b0e59 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -15,3 +15,5 @@ ics
 arrow
 sentry-sdk
 Markdown
+beautifulsoup4
+bleach
diff --git a/requirements/base.txt b/requirements/base.txt
index 06638dde30abb4548973a000190e27c732ded0f9..6e42c7c8f9fdf141ccb2195d5e44f77e5d520845 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -4,63 +4,169 @@
 #
 #    pip-compile base.in
 #
-anyascii==0.1.7           # via wagtail
-arrow==0.14.7             # via -r base.in, ics
-asgiref==3.3.1            # via django
-beautifulsoup4==4.8.2     # via wagtail
-certifi==2020.12.5        # via requests, sentry-sdk
-cffi==1.14.5              # via cryptography
-chardet==4.0.0            # via requests
-cryptography==3.4.7       # via josepy, mozilla-django-oidc, pyopenssl
-django-environ==0.4.5     # via -r base.in
-django-extensions==3.1.1  # via -r base.in
-django-filter==2.4.0      # via wagtail
-django-modelcluster==5.1  # via wagtail
-django-ranged-response==0.2.0  # via django-simple-captcha
-django-redis==4.12.1      # via -r base.in
-django-settings-export==1.2.1  # via -r base.in
-django-simple-captcha==0.5.14  # via -r base.in
-django-taggit==1.3.0      # via wagtail
-django-treebeard==4.5.1   # via wagtail
-django-widget-tweaks==1.4.8  # via -r base.in
-django==3.1.7             # via django-filter, django-ranged-response, django-redis, django-settings-export, django-simple-captcha, django-taggit, django-treebeard, djangorestframework, mozilla-django-oidc, wagtail
-djangorestframework==3.12.4  # via wagtail
-draftjs-exporter==2.1.7   # via wagtail
-et-xmlfile==1.0.1         # via openpyxl
-html5lib==1.1             # via wagtail
-ics==0.7                  # via -r base.in
-idna==2.10                # via requests
-josepy==1.8.0             # via mozilla-django-oidc
-l18n==2020.6.1            # via wagtail
-markdown==3.3.4           # via -r base.in
-mozilla-django-oidc==1.2.4  # via pirates
-numpy==1.20.2             # via opencv-python
-opencv-python==4.5.1.48   # via -r base.in
-openpyxl==3.0.7           # via tablib
-pillow==8.1.2             # via django-simple-captcha, wagtail
-pirates==0.5.0            # via -r base.in
-psycopg2-binary==2.8.6    # via -r base.in
-pycparser==2.20           # via cffi
-pyopenssl==20.0.1         # via josepy
-python-dateutil==2.8.1    # via arrow, ics
-pytz==2021.1              # via django, django-modelcluster, l18n
-redis==3.5.3              # via django-redis
-requests==2.25.1          # via -r base.in, mozilla-django-oidc, wagtail
-sentry-sdk==1.0.0         # via -r base.in
-six==1.15.0               # via django-simple-captcha, html5lib, ics, l18n, mozilla-django-oidc, pyopenssl, python-dateutil
-soupsieve==2.2.1          # via beautifulsoup4
-sqlparse==0.4.1           # via django
-tablib[xls,xlsx]==3.0.0   # via wagtail
-tatsu==5.6.1              # via ics
-urllib3==1.26.4           # via requests, sentry-sdk
-wagtail-metadata==3.4.0   # via -r base.in
-wagtail==2.12.3           # via -r base.in, wagtail-metadata
-webencodings==0.5.1       # via html5lib
-whitenoise==5.2.0         # via -r base.in
-willow==1.4               # via wagtail
-xlrd==2.0.1               # via tablib
-xlsxwriter==1.3.7         # via wagtail
-xlwt==1.3.0               # via tablib
+anyascii==0.1.7
+    # via wagtail
+arrow==0.14.7
+    # via
+    #   -r base.in
+    #   ics
+asgiref==3.3.1
+    # via django
+beautifulsoup4==4.8.2
+    # via
+    #   -r base.in
+    #   wagtail
+bleach==3.3.0
+    # via -r base.in
+certifi==2020.12.5
+    # via
+    #   requests
+    #   sentry-sdk
+cffi==1.14.5
+    # via cryptography
+chardet==4.0.0
+    # via requests
+cryptography==3.4.7
+    # via
+    #   josepy
+    #   mozilla-django-oidc
+    #   pyopenssl
+django-environ==0.4.5
+    # via -r base.in
+django-extensions==3.1.1
+    # via -r base.in
+django-filter==2.4.0
+    # via wagtail
+django-modelcluster==5.1
+    # via wagtail
+django-ranged-response==0.2.0
+    # via django-simple-captcha
+django-redis==4.12.1
+    # via -r base.in
+django-settings-export==1.2.1
+    # via -r base.in
+django-simple-captcha==0.5.14
+    # via -r base.in
+django-taggit==1.3.0
+    # via wagtail
+django-treebeard==4.5.1
+    # via wagtail
+django-widget-tweaks==1.4.8
+    # via -r base.in
+django==3.1.7
+    # via
+    #   django-filter
+    #   django-ranged-response
+    #   django-redis
+    #   django-settings-export
+    #   django-simple-captcha
+    #   django-taggit
+    #   django-treebeard
+    #   djangorestframework
+    #   mozilla-django-oidc
+    #   wagtail
+djangorestframework==3.12.4
+    # via wagtail
+draftjs-exporter==2.1.7
+    # via wagtail
+et-xmlfile==1.0.1
+    # via openpyxl
+html5lib==1.1
+    # via wagtail
+ics==0.7
+    # via -r base.in
+idna==2.10
+    # via requests
+josepy==1.8.0
+    # via mozilla-django-oidc
+l18n==2020.6.1
+    # via wagtail
+markdown==3.3.4
+    # via -r base.in
+mozilla-django-oidc==1.2.4
+    # via pirates
+numpy==1.20.2
+    # via opencv-python
+opencv-python==4.5.1.48
+    # via -r base.in
+openpyxl==3.0.7
+    # via tablib
+packaging==20.9
+    # via bleach
+pillow==8.1.2
+    # via
+    #   django-simple-captcha
+    #   wagtail
+pirates==0.5.0
+    # via -r base.in
+psycopg2-binary==2.8.6
+    # via -r base.in
+pycparser==2.20
+    # via cffi
+pyopenssl==20.0.1
+    # via josepy
+pyparsing==2.4.7
+    # via packaging
+python-dateutil==2.8.1
+    # via
+    #   arrow
+    #   ics
+pytz==2021.1
+    # via
+    #   django
+    #   django-modelcluster
+    #   l18n
+redis==3.5.3
+    # via django-redis
+requests==2.25.1
+    # via
+    #   -r base.in
+    #   mozilla-django-oidc
+    #   wagtail
+sentry-sdk==1.0.0
+    # via -r base.in
+six==1.15.0
+    # via
+    #   bleach
+    #   django-simple-captcha
+    #   html5lib
+    #   ics
+    #   l18n
+    #   mozilla-django-oidc
+    #   pyopenssl
+    #   python-dateutil
+soupsieve==2.2.1
+    # via beautifulsoup4
+sqlparse==0.4.1
+    # via django
+tablib[xls,xlsx]==3.0.0
+    # via wagtail
+tatsu==5.6.1
+    # via ics
+urllib3==1.26.4
+    # via
+    #   requests
+    #   sentry-sdk
+wagtail-metadata==3.4.0
+    # via -r base.in
+wagtail==2.12.3
+    # via
+    #   -r base.in
+    #   wagtail-metadata
+webencodings==0.5.1
+    # via
+    #   bleach
+    #   html5lib
+whitenoise==5.2.0
+    # via -r base.in
+willow==1.4
+    # via wagtail
+xlrd==2.0.1
+    # via tablib
+xlsxwriter==1.3.7
+    # via wagtail
+xlwt==1.3.0
+    # via tablib
 
 # The following packages are considered to be unsafe in a requirements file:
 # setuptools