about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/docutils/writers
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/docutils/writers
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils/writers')
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/__init__.py159
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py1887
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py187
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py955
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css350
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt8
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py393
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css26
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css332
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css293
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css307
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css486
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt8
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css566
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py3323
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex14
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty223
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex19
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex18
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex21
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/manpage.py1214
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/null.py25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py3461
-rwxr-xr-x.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py78
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py109
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odtbin0 -> 16500 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py101
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css344
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py40
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py353
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt6
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__2
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css109
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css107
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css8
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css16
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css120
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css11
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css10
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js558
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__2
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css115
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css113
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__2
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css116
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css114
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py147
54 files changed, 17027 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/__init__.py
new file mode 100644
index 00000000..eb6d3d27
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/__init__.py
@@ -0,0 +1,159 @@
+# $Id: __init__.py 9368 2023-04-28 21:26:36Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains Docutils Writer modules.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from importlib import import_module
+
+import docutils
+from docutils import languages, Component
+from docutils.transforms import universal
+
+
+class Writer(Component):
+
+    """
+    Abstract base class for docutils Writers.
+
+    Each writer module or package must export a subclass also called 'Writer'.
+    Each writer must support all standard node types listed in
+    `docutils.nodes.node_class_names`.
+
+    The `write()` method is the main entry point.
+    """
+
+    component_type = 'writer'
+    config_section = 'writers'
+
+    def get_transforms(self):
+        return super().get_transforms() + [universal.Messages,
+                                           universal.FilterMessages,
+                                           universal.StripClassesAndElements]
+
+    document = None
+    """The document to write (Docutils doctree); set by `write()`."""
+
+    output = None
+    """Final translated form of `document`
+
+    (`str` for text, `bytes` for binary formats); set by `translate()`.
+    """
+
+    language = None
+    """Language module for the document; set by `write()`."""
+
+    destination = None
+    """`docutils.io` Output object; where to write the document.
+
+    Set by `write()`.
+    """
+
+    def __init__(self):
+
+        self.parts = {}
+        """Mapping of document part names to fragments of `self.output`.
+
+        See `Writer.assemble_parts()` below and
+        <https://docutils.sourceforge.io/docs/api/publisher.html>.
+        """
+
+    def write(self, document, destination):
+        """
+        Process a document into its final form.
+
+        Translate `document` (a Docutils document tree) into the Writer's
+        native format, and write it out to its `destination` (a
+        `docutils.io.Output` subclass object).
+
+        Normally not overridden or extended in subclasses.
+        """
+        self.document = document
+        self.language = languages.get_language(
+            document.settings.language_code,
+            document.reporter)
+        self.destination = destination
+        self.translate()
+        return self.destination.write(self.output)
+
+    def translate(self):
+        """
+        Do final translation of `self.document` into `self.output`.  Called
+        from `write`.  Override in subclasses.
+
+        Usually done with a `docutils.nodes.NodeVisitor` subclass, in
+        combination with a call to `docutils.nodes.Node.walk()` or
+        `docutils.nodes.Node.walkabout()`.  The ``NodeVisitor`` subclass must
+        support all standard elements (listed in
+        `docutils.nodes.node_class_names`) and possibly non-standard elements
+        used by the current Reader as well.
+        """
+        raise NotImplementedError('subclass must override this method')
+
+    def assemble_parts(self):
+        """Assemble the `self.parts` dictionary.  Extend in subclasses.
+
+        See <https://docutils.sourceforge.io/docs/api/publisher.html>.
+        """
+        self.parts['whole'] = self.output
+        self.parts['encoding'] = self.document.settings.output_encoding
+        self.parts['errors'] = (
+            self.document.settings.output_encoding_error_handler)
+        self.parts['version'] = docutils.__version__
+
+
+class UnfilteredWriter(Writer):
+
+    """
+    A writer that passes the document tree on unchanged (e.g. a
+    serializer.)
+
+    Documents written by UnfilteredWriters are typically reused at a
+    later date using a subclass of `readers.ReReader`.
+    """
+
+    def get_transforms(self):
+        # Do not add any transforms.  When the document is reused
+        # later, the then-used writer will add the appropriate
+        # transforms.
+        return Component.get_transforms(self)
+
+
+_writer_aliases = {
+      'html': 'html4css1',  # may change to html5 some day
+      'html4': 'html4css1',
+      'xhtml10': 'html4css1',
+      'html5': 'html5_polyglot',
+      'xhtml': 'html5_polyglot',
+      's5': 's5_html',
+      'latex': 'latex2e',
+      'xelatex': 'xetex',
+      'luatex': 'xetex',
+      'lualatex': 'xetex',
+      'odf': 'odf_odt',
+      'odt': 'odf_odt',
+      'ooffice': 'odf_odt',
+      'openoffice': 'odf_odt',
+      'libreoffice': 'odf_odt',
+      'pprint': 'pseudoxml',
+      'pformat': 'pseudoxml',
+      'pdf': 'rlpdf',
+      'xml': 'docutils_xml'}
+
+
+def get_writer_class(writer_name):
+    """Return the Writer class from the `writer_name` module."""
+    name = writer_name.lower()
+    name = _writer_aliases.get(name, name)
+    try:
+        module = import_module('docutils.writers.'+name)
+    except ImportError:
+        try:
+            module = import_module(name)
+        except ImportError as err:
+            raise ImportError(f'Writer "{writer_name}" not found. {err}')
+    return module.Writer
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py b/.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py
new file mode 100644
index 00000000..8122f4b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py
@@ -0,0 +1,1887 @@
+#!/usr/bin/env python3
+# :Author: David Goodger, Günter Milde
+#          Based on the html4css1 writer by David Goodger.
+# :Maintainer: docutils-develop@lists.sourceforge.net
+# :Revision: $Revision: 9614 $
+# :Date: $Date: 2005-06-28$
+# :Copyright: © 2016 David Goodger, Günter Milde
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+"""common definitions for Docutils HTML writers"""
+
+import base64
+import mimetypes
+import os
+import os.path
+from pathlib import Path
+import re
+import urllib
+import warnings
+import xml.etree.ElementTree as ET  # TODO: lazy import in prepare_svg()?
+
+import docutils
+from docutils import frontend, languages, nodes, utils, writers
+from docutils.parsers.rst.directives import length_or_percentage_or_unitless
+from docutils.parsers.rst.directives.images import PIL
+from docutils.transforms import writer_aux
+from docutils.utils.math import (latex2mathml, math2html, tex2mathml_extern,
+                                 unichar2tex, wrap_math_code, MathError)
+
+
+class Writer(writers.Writer):
+
+    supported = ('html', 'xhtml')  # update in subclass
+    """Formats this writer supports."""
+
+    settings_spec = (
+        'HTML Writer Options',
+        None,
+        (('Specify the template file (UTF-8 encoded). '
+          '(default: writer dependent)',
+          ['--template'],
+          {'metavar': '<file>'}),
+         ('Comma separated list of stylesheet URLs. '
+          'Overrides previous --stylesheet and --stylesheet-path settings.',
+          ['--stylesheet'],
+          {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
+           'validator': frontend.validate_comma_separated_list}),
+         ('Comma separated list of stylesheet paths. '
+          'Relative paths are expanded if a matching file is found in '
+          'the --stylesheet-dirs. With --link-stylesheet, '
+          'the path is rewritten relative to the output HTML file. '
+          '(default: writer dependent)',
+          ['--stylesheet-path'],
+          {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+           'validator': frontend.validate_comma_separated_list}),
+         ('Comma-separated list of directories where stylesheets are found. '
+          'Used by --stylesheet-path when expanding relative path arguments. '
+          '(default: writer dependent)',
+          ['--stylesheet-dirs'],
+          {'metavar': '<dir[,dir,...]>',
+           'validator': frontend.validate_comma_separated_list}),
+         ('Embed the stylesheet(s) in the output HTML file.  The stylesheet '
+          'files must be accessible during processing. (default)',
+          ['--embed-stylesheet'],
+          {'default': True, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Link to the stylesheet(s) in the output HTML file. ',
+          ['--link-stylesheet'],
+          {'dest': 'embed_stylesheet', 'action': 'store_false'}),
+         ('Specify the initial header level. '
+          'Does not affect document title & subtitle (see --no-doc-title).'
+          '(default: writer dependent).',
+          ['--initial-header-level'],
+          {'choices': '1 2 3 4 5 6'.split(), 'default': '2',
+           'metavar': '<level>'}),
+         ('Format for footnote references: one of "superscript" or '
+          '"brackets". (default: "brackets")',
+          ['--footnote-references'],
+          {'choices': ['superscript', 'brackets'], 'default': 'brackets',
+           'metavar': '<format>',
+           'overrides': 'trim_footnote_reference_space'}),
+         ('Format for block quote attributions: '
+          'one of "dash" (em-dash prefix), "parentheses"/"parens", or "none". '
+          '(default: "dash")',
+          ['--attribution'],
+          {'choices': ['dash', 'parentheses', 'parens', 'none'],
+           'default': 'dash', 'metavar': '<format>'}),
+         ('Remove extra vertical whitespace between items of "simple" bullet '
+          'lists and enumerated lists. (default)',
+          ['--compact-lists'],
+          {'default': True, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Disable compact simple bullet and enumerated lists.',
+          ['--no-compact-lists'],
+          {'dest': 'compact_lists', 'action': 'store_false'}),
+         ('Remove extra vertical whitespace between items of simple field '
+          'lists. (default)',
+          ['--compact-field-lists'],
+          {'default': True, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Disable compact simple field lists.',
+          ['--no-compact-field-lists'],
+          {'dest': 'compact_field_lists', 'action': 'store_false'}),
+         ('Added to standard table classes. '
+          'Defined styles: borderless, booktabs, '
+          'align-left, align-center, align-right, '
+          'colwidths-auto, colwidths-grid.',
+          ['--table-style'],
+          {'default': ''}),
+         ('Math output format (one of "MathML", "HTML", "MathJax", '
+          'or "LaTeX") and option(s). '
+          '(default: "HTML math.css")',
+          ['--math-output'],
+          {'default': 'HTML math.css',
+           'validator': frontend.validate_math_output}),
+         ('Prepend an XML declaration. ',
+          ['--xml-declaration'],
+          {'default': False, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Omit the XML declaration.',
+          ['--no-xml-declaration'],
+          {'dest': 'xml_declaration', 'action': 'store_false'}),
+         ('Obfuscate email addresses to confuse harvesters while still '
+          'keeping email links usable with standards-compliant browsers.',
+          ['--cloak-email-addresses'],
+          {'action': 'store_true', 'validator': frontend.validate_boolean}),
+         )
+        )
+
+    settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
+
+    relative_path_settings = ('template',)
+
+    config_section = 'html base writer'  # overwrite in subclass
+    config_section_dependencies = ('writers', 'html writers')
+
+    visitor_attributes = (
+        'head_prefix', 'head', 'stylesheet', 'body_prefix',
+        'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
+        'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
+        'html_prolog', 'html_head', 'html_title', 'html_subtitle',
+        'html_body')
+
+    def get_transforms(self):
+        return super().get_transforms() + [writer_aux.Admonitions]
+
+    def translate(self):
+        self.visitor = visitor = self.translator_class(self.document)
+        self.document.walkabout(visitor)
+        for attr in self.visitor_attributes:
+            setattr(self, attr, getattr(visitor, attr))
+        self.output = self.apply_template()
+
+    def apply_template(self):
+        with open(self.document.settings.template, encoding='utf-8') as fp:
+            template = fp.read()
+        subs = self.interpolation_dict()
+        return template % subs
+
+    def interpolation_dict(self):
+        subs = {}
+        settings = self.document.settings
+        for attr in self.visitor_attributes:
+            subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
+        subs['encoding'] = settings.output_encoding
+        subs['version'] = docutils.__version__
+        return subs
+
+    def assemble_parts(self):
+        writers.Writer.assemble_parts(self)
+        for part in self.visitor_attributes:
+            self.parts[part] = ''.join(getattr(self, part))
+
+
+class HTMLTranslator(nodes.NodeVisitor):
+
+    """
+    Generic Docutils to HTML translator.
+
+    See the `html4css1` and `html5_polyglot` writers for full featured
+    HTML writers.
+
+    .. IMPORTANT::
+      The `visit_*` and `depart_*` methods use a
+      heterogeneous stack, `self.context`.
+      When subclassing, make sure to be consistent in its use!
+
+      Examples for robust coding:
+
+      a) Override both `visit_*` and `depart_*` methods, don't call the
+         parent functions.
+
+      b) Extend both and unconditionally call the parent functions::
+
+           def visit_example(self, node):
+               if foo:
+                   self.body.append('<div class="foo">')
+               html4css1.HTMLTranslator.visit_example(self, node)
+
+           def depart_example(self, node):
+               html4css1.HTMLTranslator.depart_example(self, node)
+               if foo:
+                   self.body.append('</div>')
+
+      c) Extend both, calling the parent functions under the same
+         conditions::
+
+           def visit_example(self, node):
+               if foo:
+                   self.body.append('<div class="foo">\n')
+               else: # call the parent method
+                   _html_base.HTMLTranslator.visit_example(self, node)
+
+           def depart_example(self, node):
+               if foo:
+                   self.body.append('</div>\n')
+               else: # call the parent method
+                   _html_base.HTMLTranslator.depart_example(self, node)
+
+      d) Extend one method (call the parent), but don't otherwise use the
+         `self.context` stack::
+
+           def depart_example(self, node):
+               _html_base.HTMLTranslator.depart_example(self, node)
+               if foo:
+                   # implementation-specific code
+                   # that does not use `self.context`
+                   self.body.append('</div>\n')
+
+      This way, changes in stack use will not bite you.
+    """
+
+    doctype = '<!DOCTYPE html>\n'
+    doctype_mathml = doctype
+
+    head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
+                            ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
+    content_type = '<meta charset="%s" />\n'
+    generator = (
+        f'<meta name="generator" content="Docutils {docutils.__version__}: '
+        'https://docutils.sourceforge.io/" />\n')
+    # `starttag()` arguments for the main document (HTML5 uses <main>)
+    documenttag_args = {'tagname': 'div', 'CLASS': 'document'}
+
+    # Template for the MathJax script in the header:
+    mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
+
+    mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js'
+    """
+    URL of the MathJax javascript library.
+
+    The MathJax library ought to be installed on the same
+    server as the rest of the deployed site files and specified
+    in the `math-output` setting appended to "mathjax".
+    See `Docutils Configuration`__.
+
+    __ https://docutils.sourceforge.io/docs/user/config.html#math-output
+
+    The fallback tries a local MathJax installation at
+    ``/usr/share/javascript/mathjax/MathJax.js``.
+    """
+
+    stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
+    embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
+    words_and_spaces = re.compile(r'[^ \n]+| +|\n')
+    # wrap point inside word:
+    in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+')
+    lang_attribute = 'lang'  # name changes to 'xml:lang' in XHTML 1.1
+
+    special_characters = {ord('&'): '&amp;',
+                          ord('<'): '&lt;',
+                          ord('"'): '&quot;',
+                          ord('>'): '&gt;',
+                          ord('@'): '&#64;',  # may thwart address harvesters
+                          }
+    """Character references for characters with a special meaning in HTML."""
+
+    videotypes = ('video/mp4', 'video/webm', 'video/ogg')
+    """MIME types supported by the HTML5 <video> element."""
+
+    def __init__(self, document):
+        nodes.NodeVisitor.__init__(self, document)
+        # process settings
+        self.settings = settings = document.settings
+        self.language = languages.get_language(
+                            settings.language_code, document.reporter)
+        self.initial_header_level = int(settings.initial_header_level)
+        # image_loading (only defined for HTML5 writer)
+        _image_loading_default = 'link'
+        # convert legacy setting embed_images:
+        if getattr(settings, 'embed_images', None) is not None:
+            if settings.embed_images:
+                _image_loading_default = 'embed'
+            warnings.warn('The configuration setting "embed_images"\n'
+                          '  will be removed in Docutils 2.0. '
+                          f'Use "image_loading: {_image_loading_default}".',
+                          FutureWarning, stacklevel=8)
+        self.image_loading = getattr(settings,
+                                     'image_loading', _image_loading_default)
+        # backwards compatibiltiy: validate/convert programatically set strings
+        if isinstance(self.settings.math_output, str):
+            self.settings.math_output = frontend.validate_math_output(
+                                            self.settings.math_output)
+        (self.math_output,
+         self.math_options) = self.settings.math_output
+
+        # set up "parts" (cf. docs/api/publisher.html#publish-parts-details)
+        #
+        self.body = []  # equivalent to `fragment`, ≠ `html_body`
+        self.body_prefix = ['</head>\n<body>\n']  # + optional header
+        self.body_pre_docinfo = []  # document heading (title and subtitle)
+        self.body_suffix = ['</body>\n</html>\n']  # + optional footer
+        self.docinfo = []
+        self.footer = []
+        self.fragment = []  # main content of the document ("naked" body)
+        self.head = []
+        self.head_prefix = []  # everything up to and including <head>
+        self.header = []
+        self.html_body = []
+        self.html_head = [self.content_type]  # charset not interpolated
+        self.html_prolog = []
+        self.html_subtitle = []
+        self.html_title = []
+        self.meta = [self.generator]
+        self.stylesheet = [self.stylesheet_call(path)
+                           for path in utils.get_stylesheet_list(settings)]
+        self.title = []
+        self.subtitle = []
+        if settings.xml_declaration:
+            self.head_prefix.append(
+                utils.xml_declaration(settings.output_encoding))
+            self.html_prolog.append(
+                utils.xml_declaration('%s'))  # encoding not interpolated
+        if (settings.output_encoding
+            and settings.output_encoding.lower() != 'unicode'):
+            self.meta.insert(0, self.content_type % settings.output_encoding)
+
+        # bookkeeping attributes; reflect state of translator
+        #
+        self.context = []
+        """Heterogeneous stack.
+
+        Used by visit_* and depart_* functions in conjunction with the tree
+        traversal. Make sure that the pops correspond to the pushes.
+        """
+        self.section_level = 0
+        self.colspecs = []
+        self.compact_p = True
+        self.compact_simple = False
+        self.compact_field_list = False
+        self.in_docinfo = False
+        self.in_sidebar = False
+        self.in_document_title = 0  # len(self.body) or 0
+        self.in_mailto = False
+        self.author_in_authors = False  # for html4css1
+        self.math_header = []
+        self.messages = []
+        """Queue of system_message nodes (writing issues).
+
+        Call `report_messages()` in `depart_*_block()` methods to clean up!
+        """
+
+    def astext(self):
+        return ''.join(self.head_prefix + self.head
+                       + self.stylesheet + self.body_prefix
+                       + self.body_pre_docinfo + self.docinfo
+                       + self.body + self.body_suffix)
+
+    def attval(self, text,
+               whitespace=re.compile('[\n\r\t\v\f]')):
+        """Cleanse, HTML encode, and return attribute value text."""
+        encoded = self.encode(whitespace.sub(' ', text))
+        if self.in_mailto and self.settings.cloak_email_addresses:
+            # Cloak at-signs ("%40") and periods with HTML entities.
+            encoded = encoded.replace('%40', '&#37;&#52;&#48;')
+            encoded = encoded.replace('.', '&#46;')
+        return encoded
+
+    def cloak_email(self, addr):
+        """Try to hide the link text of a email link from harversters."""
+        # Surround at-signs and periods with <span> tags.  ("@" has
+        # already been encoded to "&#64;" by the `encode` method.)
+        addr = addr.replace('&#64;', '<span>&#64;</span>')
+        return addr.replace('.', '<span>&#46;</span>')
+
+    def cloak_mailto(self, uri):
+        """Try to hide a mailto: URL from harvesters."""
+        # Encode "@" using a URL octet reference (see RFC 1738).
+        # Further cloaking with HTML entities will be done in the
+        # `attval` function.
+        return uri.replace('@', '%40')
+
+    def encode(self, text):
+        """Encode special characters in `text` & return."""
+        # Use only named entities known in both XML and HTML
+        # other characters are automatically encoded "by number" if required.
+        # @@@ A codec to do these and all other HTML entities would be nice.
+        text = str(text)
+        return text.translate(self.special_characters)
+
+    def image_size(self, node):
+        # Determine the image size from the node arguments or the image file.
+        # Return a size declaration suitable as "style" argument value,
+        # e.g., ``'width: 4px; height: 2em;'``.
+        # TODO: consider feature-request #102?
+        size = [node.get('width', None), node.get('height', None)]
+        if 'scale' in node:
+            if 'width' not in node or 'height' not in node:
+                # try reading size from image file
+                reading_problems = []
+                uri = node['uri']
+                if not PIL:
+                    reading_problems.append('Requires Python Imaging Library.')
+                if mimetypes.guess_type(uri)[0] in self.videotypes:
+                    reading_problems.append('PIL cannot read video images.')
+                if not self.settings.file_insertion_enabled:
+                    reading_problems.append('Reading external files disabled.')
+                if not reading_problems:
+                    try:
+                        imagepath = self.uri2imagepath(uri)
+                        with PIL.Image.open(imagepath) as img:
+                            imgsize = img.size
+                    except (ValueError, OSError, UnicodeEncodeError) as err:
+                        reading_problems.append(str(err))
+                    else:
+                        self.settings.record_dependencies.add(
+                            imagepath.replace('\\', '/'))
+                if reading_problems:
+                    msg = ['Cannot scale image!',
+                           f'Could not get size from "{uri}":',
+                           *reading_problems]
+                    self.messages.append(self.document.reporter.warning(
+                        '\n  '.join(msg), base_node=node))
+                else:
+                    for i in range(2):
+                        size[i] = size[i] or '%dpx' % imgsize[i]
+            # scale provided/determined size values:
+            factor = float(node['scale']) / 100
+            for i in range(2):
+                if size[i]:
+                    match = re.match(r'([0-9.]+)(\S*)$', size[i])
+                    size[i] = '%s%s' % (factor * float(match.group(1)),
+                                        match.group(2))
+        size_declarations = []
+        for i, dimension in enumerate(('width', 'height')):
+            if size[i]:
+                # Interpret unitless values as pixels:
+                if re.match(r'^[0-9.]+$', size[i]):
+                    size[i] += 'px'
+                size_declarations.append(f'{dimension}: {size[i]};')
+        return ' '.join(size_declarations)
+
+    def prepare_svg(self, node, imagedata, size_declaration):
+        # Edit `imagedata` for embedding as SVG image.
+        # Use ElementTree to add node attributes.
+        # ET also removes comments and preamble code.
+        #
+        # Provisional:
+        # interface and behaviour may change without notice.
+
+        # SVG namespace
+        svg_ns = {'': 'http://www.w3.org/2000/svg',
+                  'xlink': 'http://www.w3.org/1999/xlink'}
+        # don't add SVG namespace to all elements
+        ET.register_namespace('', svg_ns[''])
+        ET.register_namespace('xlink', svg_ns['xlink'])
+        try:
+            svg = ET.fromstring(imagedata.decode('utf-8'))
+        except ET.ParseError as err:
+            self.messages.append(self.document.reporter.error(
+                f'Cannot parse SVG image "{node["uri"]}":\n  {err}',
+                base_node=node))
+            return imagedata.decode('utf-8')
+        # apply image node attributes:
+        if size_declaration:  # append to style, replacing width & height
+            declarations = [d.strip() for d in svg.get('style', '').split(';')]
+            declarations = [d for d in declarations
+                            if d
+                            and not d.startswith('width')
+                            and not d.startswith('height')]
+            svg.set('style', '; '.join(declarations+[size_declaration]))
+        if node['classes'] or 'align' in node:
+            classes = svg.get('class', '').split()
+            classes += node.get('classes', [])
+            if 'align' in node:
+                classes.append(f'align-{node["align"]}')
+            svg.set('class', ' '.join(classes))
+        if 'alt' in node and svg.find('title', svg_ns) is None:
+            svg_title = ET.Element('title')
+            svg_title.text = node['alt']
+            svg.insert(0, svg_title)
+        return ET.tostring(svg, encoding='unicode')
+
+    def stylesheet_call(self, path, adjust_path=None):
+        """Return code to reference or embed stylesheet file `path`"""
+        if adjust_path is None:
+            adjust_path = bool(self.settings.stylesheet_path)
+        if self.settings.embed_stylesheet:
+            try:
+                with open(path, encoding='utf-8') as f:
+                    content = f.read()
+            except OSError as err:
+                msg = f'Cannot embed stylesheet: {err}'
+                self.document.reporter.error(msg)
+                return '<--- %s --->\n' % msg
+            else:
+                self.settings.record_dependencies.add(path)
+            return self.embedded_stylesheet % content
+        # else link to style file:
+        if adjust_path:
+            # rewrite path relative to output (cf. config.html#stylesheet-path)
+            path = utils.relative_path(self.settings._destination, path)
+        return self.stylesheet_link % self.encode(path)
+
+    def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
+        """
+        Construct and return a start tag given a node (id & class attributes
+        are extracted), tag name, and optional attributes.
+        """
+        tagname = tagname.lower()
+        prefix = []
+        atts = {}
+        for (name, value) in attributes.items():
+            atts[name.lower()] = value
+        classes = atts.pop('classes', [])
+        languages = []
+        # unify class arguments and move language specification
+        for cls in node.get('classes', []) + atts.pop('class', '').split():
+            if cls.startswith('language-'):
+                languages.append(cls[9:])
+            elif cls.strip() and cls not in classes:
+                classes.append(cls)
+        if languages:
+            # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
+            atts[self.lang_attribute] = languages[0]
+        # filter classes that are processed by the writer:
+        internal = ('colwidths-auto', 'colwidths-given', 'colwidths-grid')
+        if isinstance(node, nodes.table):
+            classes = [cls for cls in classes if cls not in internal]
+        if classes:
+            atts['class'] = ' '.join(classes)
+        assert 'id' not in atts
+        ids = node.get('ids', [])
+        ids.extend(atts.pop('ids', []))
+        if ids:
+            atts['id'] = ids[0]
+            for id in ids[1:]:
+                # Add empty "span" elements for additional IDs.  Note
+                # that we cannot use empty "a" elements because there
+                # may be targets inside of references, but nested "a"
+                # elements aren't allowed in XHTML (even if they do
+                # not all have a "href" attribute).
+                if empty or isinstance(node, (nodes.Sequential,
+                                              nodes.docinfo,
+                                              nodes.table)):
+                    # Insert target right in front of element.
+                    prefix.append('<span id="%s"></span>' % id)
+                else:
+                    # Non-empty tag.  Place the auxiliary <span> tag
+                    # *inside* the element, as the first child.
+                    suffix += '<span id="%s"></span>' % id
+        attlist = sorted(atts.items())
+        parts = [tagname]
+        for name, value in attlist:
+            # value=None was used for boolean attributes without
+            # value, but this isn't supported by XHTML.
+            assert value is not None
+            if isinstance(value, list):
+                values = [str(v) for v in value]
+                parts.append('%s="%s"' % (name.lower(),
+                                          self.attval(' '.join(values))))
+            else:
+                parts.append('%s="%s"' % (name.lower(),
+                                          self.attval(str(value))))
+        if empty:
+            infix = ' /'
+        else:
+            infix = ''
+        return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
+
+    def emptytag(self, node, tagname, suffix='\n', **attributes):
+        """Construct and return an XML-compatible empty tag."""
+        return self.starttag(node, tagname, suffix, empty=True, **attributes)
+
+    def report_messages(self, node):
+        if isinstance(node.parent, (nodes.system_message, nodes.entry)):
+            return
+        while self.messages:
+            message = self.messages.pop(0)
+            if self.settings.report_level <= message['level']:
+                message.walkabout(self)
+
+    def set_class_on_child(self, node, class_, index=0):
+        """
+        Set class `class_` on the visible child no. index of `node`.
+        Do nothing if node has fewer children than `index`.
+        """
+        children = [n for n in node if not isinstance(n, nodes.Invisible)]
+        try:
+            child = children[index]
+        except IndexError:
+            return
+        child['classes'].append(class_)
+
+    def uri2imagepath(self, uri):
+        """Get filesystem path corresponding to an URI.
+
+        The image directive expects an image URI. Some writers require the
+        corresponding image path to read the image size from the file or to
+        embed the image in the output.
+
+        Absolute URIs consider the "root_prefix" setting.
+
+        In order to work in the output document, relative image URIs relate
+        to the output directory. For access by the writer, the corresponding
+        image path must be relative to the current working directory.
+
+        Provisional: the function's location, interface and behaviour
+        may change without advance warning.
+        """
+        destination = self.settings._destination or ''
+        uri_parts = urllib.parse.urlparse(uri)
+        if uri_parts.scheme not in ('', 'file'):
+            raise ValueError('Can only read local images.')
+        imagepath = urllib.request.url2pathname(uri_parts.path)
+        if imagepath.startswith('/'):
+            root_prefix = Path(self.settings.root_prefix)
+            imagepath = (root_prefix/imagepath[1:]).as_posix()
+        elif not os.path.isabs(imagepath):  # exclude absolute Windows paths
+            destdir = os.path.abspath(os.path.dirname(destination))
+            imagepath = utils.relative_path(None,
+                                            os.path.join(destdir, imagepath))
+        return imagepath
+
+    def visit_Text(self, node):
+        text = node.astext()
+        encoded = self.encode(text)
+        if self.in_mailto and self.settings.cloak_email_addresses:
+            encoded = self.cloak_email(encoded)
+        self.body.append(encoded)
+
+    def depart_Text(self, node):
+        pass
+
+    def visit_abbreviation(self, node):
+        # @@@ implementation incomplete ("title" attribute)
+        self.body.append(self.starttag(node, 'abbr', ''))
+
+    def depart_abbreviation(self, node):
+        self.body.append('</abbr>')
+
+    def visit_acronym(self, node):
+        # @@@ implementation incomplete ("title" attribute)
+        self.body.append(self.starttag(node, 'acronym', ''))
+
+    def depart_acronym(self, node):
+        self.body.append('</acronym>')
+
+    def visit_address(self, node):
+        self.visit_docinfo_item(node, 'address', meta=False)
+        self.body.append(self.starttag(node, 'pre',
+                                       suffix='', CLASS='address'))
+
+    def depart_address(self, node):
+        self.body.append('\n</pre>\n')
+        self.depart_docinfo_item()
+
+    def visit_admonition(self, node):
+        self.body.append(self.starttag(node, 'aside', classes=['admonition']))
+
+    def depart_admonition(self, node=None):
+        self.body.append('</aside>\n')
+
+    attribution_formats = {'dash': ('\u2014', ''),
+                           'parentheses': ('(', ')'),
+                           'parens': ('(', ')'),
+                           'none': ('', '')}
+
+    def visit_attribution(self, node):
+        prefix, suffix = self.attribution_formats[self.settings.attribution]
+        self.context.append(suffix)
+        self.body.append(
+            self.starttag(node, 'p', prefix, CLASS='attribution'))
+
+    def depart_attribution(self, node):
+        self.body.append(self.context.pop() + '</p>\n')
+
+    def visit_author(self, node):
+        if not isinstance(node.parent, nodes.authors):
+            self.visit_docinfo_item(node, 'author')
+        self.body.append('<p>')
+
+    def depart_author(self, node):
+        self.body.append('</p>')
+        if isinstance(node.parent, nodes.authors):
+            self.body.append('\n')
+        else:
+            self.depart_docinfo_item()
+
+    def visit_authors(self, node):
+        self.visit_docinfo_item(node, 'authors')
+
+    def depart_authors(self, node):
+        self.depart_docinfo_item()
+
+    def visit_block_quote(self, node):
+        self.body.append(self.starttag(node, 'blockquote'))
+
+    def depart_block_quote(self, node):
+        self.body.append('</blockquote>\n')
+
+    def check_simple_list(self, node):
+        """Check for a simple list that can be rendered compactly."""
+        visitor = SimpleListChecker(self.document)
+        try:
+            node.walk(visitor)
+        except nodes.NodeFound:
+            return False
+        else:
+            return True
+
+    # Compact lists
+    # ------------
+    # Include definition lists and field lists (in addition to ordered
+    # and unordered lists) in the test if a list is "simple"  (cf. the
+    # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
+    # the end of this file).
+
+    def is_compactable(self, node):
+        # explicit class arguments have precedence
+        if 'compact' in node['classes']:
+            return True
+        if 'open' in node['classes']:
+            return False
+        # check config setting:
+        if (isinstance(node, (nodes.field_list, nodes.definition_list))
+            and not self.settings.compact_field_lists):
+            return False
+        if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
+            and not self.settings.compact_lists):
+            return False
+        # Table of Contents:
+        if 'contents' in node.parent['classes']:
+            return True
+        # check the list items:
+        return self.check_simple_list(node)
+
+    def visit_bullet_list(self, node):
+        atts = {}
+        old_compact_simple = self.compact_simple
+        self.context.append((self.compact_simple, self.compact_p))
+        self.compact_p = None
+        self.compact_simple = self.is_compactable(node)
+        if self.compact_simple and not old_compact_simple:
+            atts['class'] = 'simple'
+        self.body.append(self.starttag(node, 'ul', **atts))
+
+    def depart_bullet_list(self, node):
+        self.compact_simple, self.compact_p = self.context.pop()
+        self.body.append('</ul>\n')
+
+    def visit_caption(self, node):
+        self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
+
+    def depart_caption(self, node):
+        self.body.append('</p>\n')
+
+    # Use semantic tag and DPub role (HTML4 uses a table)
+    def visit_citation(self, node):
+        # role 'doc-bibloentry' requires wrapping in an element with
+        # role 'list' and an element with role 'doc-bibliography'
+        # https://www.w3.org/TR/dpub-aria-1.0/#doc-biblioentry)
+        if not isinstance(node.previous_sibling(), type(node)):
+            self.body.append('<div role="list" class="citation-list">\n')
+        self.body.append(self.starttag(node, 'div', classes=[node.tagname],
+                                       role="doc-biblioentry"))
+
+    def depart_citation(self, node):
+        self.body.append('</div>\n')
+        if not isinstance(node.next_node(descend=False, siblings=True),
+                          type(node)):
+            self.body.append('</div>\n')
+
+    # Use DPub role (overwritten in HTML4)
+    def visit_citation_reference(self, node):
+        href = '#'
+        if 'refid' in node:
+            href += node['refid']
+        elif 'refname' in node:
+            href += self.document.nameids[node['refname']]
+        # else: # TODO system message (or already in the transform)?
+        # 'Citation reference missing.'
+        self.body.append(self.starttag(node, 'a', suffix='[', href=href,
+                                       classes=['citation-reference'],
+                                       role='doc-biblioref'))
+
+    def depart_citation_reference(self, node):
+        self.body.append(']</a>')
+
+    def visit_classifier(self, node):
+        self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
+
+    def depart_classifier(self, node):
+        self.body.append('</span>')
+        self.depart_term(node)  # close the term element after last classifier
+
+    def visit_colspec(self, node):
+        self.colspecs.append(node)
+        # "stubs" list is an attribute of the tgroup element:
+        node.parent.stubs.append(node.attributes.get('stub'))
+
+    def depart_colspec(self, node):
+        # write out <colgroup> when all colspecs are processed
+        if isinstance(node.next_node(descend=False, siblings=True),
+                      nodes.colspec):
+            return
+        if 'colwidths-auto' in node.parent.parent['classes'] or (
+            'colwidths-grid' not in self.settings.table_style
+            and 'colwidths-given' not in node.parent.parent['classes']):
+            return
+        self.body.append(self.starttag(node, 'colgroup'))
+        total_width = sum(node['colwidth'] for node in self.colspecs)
+        for node in self.colspecs:
+            colwidth = node['colwidth'] / total_width
+            self.body.append(self.emptytag(node, 'col',
+                                           style=f'width: {colwidth:.1%}'))
+        self.body.append('</colgroup>\n')
+
+    def visit_comment(self, node,
+                      sub=re.compile('-(?=-)').sub):
+        """Escape double-dashes in comment text."""
+        self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
+        # Content already processed:
+        raise nodes.SkipNode
+
+    def visit_compound(self, node):
+        self.body.append(self.starttag(node, 'div', CLASS='compound'))
+
+    def depart_compound(self, node):
+        self.body.append('</div>\n')
+
+    def visit_container(self, node):
+        self.body.append(self.starttag(node, 'div',
+                                       CLASS='docutils container'))
+
+    def depart_container(self, node):
+        self.body.append('</div>\n')
+
+    def visit_contact(self, node):
+        self.visit_docinfo_item(node, 'contact', meta=False)
+
+    def depart_contact(self, node):
+        self.depart_docinfo_item()
+
+    def visit_copyright(self, node):
+        self.visit_docinfo_item(node, 'copyright')
+
+    def depart_copyright(self, node):
+        self.depart_docinfo_item()
+
+    def visit_date(self, node):
+        self.visit_docinfo_item(node, 'date')
+
+    def depart_date(self, node):
+        self.depart_docinfo_item()
+
+    def visit_decoration(self, node):
+        pass
+
+    def depart_decoration(self, node):
+        pass
+
+    def visit_definition(self, node):
+        if 'details' not in node.parent.parent['classes']:
+            self.body.append(self.starttag(node, 'dd', ''))
+
+    def depart_definition(self, node):
+        if 'details' not in node.parent.parent['classes']:
+            self.body.append('</dd>\n')
+
+    def visit_definition_list(self, node):
+        if 'details' in node['classes']:
+            self.body.append(self.starttag(node, 'div'))
+        else:
+            classes = ['simple'] if self.is_compactable(node) else []
+            self.body.append(self.starttag(node, 'dl', classes=classes))
+
+    def depart_definition_list(self, node):
+        if 'details' in node['classes']:
+            self.body.append('</div>\n')
+        else:
+            self.body.append('</dl>\n')
+
+    # Use a "details" disclosure element if parent has "class" arg "details".
+    def visit_definition_list_item(self, node):
+        if 'details' in node.parent['classes']:
+            atts = {}
+            if "open" in node.parent['classes']:
+                atts['open'] = 'open'
+            self.body.append(self.starttag(node, 'details', **atts))
+
+    def depart_definition_list_item(self, node):
+        if 'details' in node.parent['classes']:
+            self.body.append('</details>\n')
+
+    def visit_description(self, node):
+        self.body.append(self.starttag(node, 'dd', ''))
+
+    def depart_description(self, node):
+        self.body.append('</dd>\n')
+
+    def visit_docinfo(self, node):
+        self.context.append(len(self.body))
+        classes = ['docinfo']
+        if self.is_compactable(node):
+            classes.append('simple')
+        self.body.append(self.starttag(node, 'dl', classes=classes))
+
+    def depart_docinfo(self, node):
+        self.body.append('</dl>\n')
+        start = self.context.pop()
+        self.docinfo = self.body[start:]
+        self.body = []
+
+    def visit_docinfo_item(self, node, name, meta=True):
+        if meta:
+            self.meta.append(f'<meta name="{name}" '
+                             f'content="{self.attval(node.astext())}" />\n')
+        self.body.append(f'<dt class="{name}">{self.language.labels[name]}'
+                         '<span class="colon">:</span></dt>\n')
+        self.body.append(self.starttag(node, 'dd', '', CLASS=name))
+
+    def depart_docinfo_item(self):
+        self.body.append('</dd>\n')
+
+    def visit_doctest_block(self, node):
+        self.body.append(self.starttag(node, 'pre', suffix='',
+                                       classes=['code', 'python', 'doctest']))
+
+    def depart_doctest_block(self, node):
+        self.body.append('\n</pre>\n')
+
+    def visit_document(self, node):
+        title = (node.get('title') or os.path.basename(node['source'])
+                 or 'untitled Docutils document')
+        self.head.append(f'<title>{self.encode(title)}</title>\n')
+
+    def depart_document(self, node):
+        self.head_prefix.extend([self.doctype,
+                                 self.head_prefix_template %
+                                 {'lang': self.settings.language_code}])
+        self.html_prolog.append(self.doctype)
+        self.head = self.meta[:] + self.head
+        if 'name="dcterms.' in ''.join(self.meta):
+            self.head.append('<link rel="schema.dcterms"'
+                             ' href="http://purl.org/dc/terms/"/>')
+        if self.math_header:
+            if self.math_output == 'mathjax':
+                self.head.extend(self.math_header)
+            else:
+                self.stylesheet.extend(self.math_header)
+        # skip content-type meta tag with interpolated charset value:
+        self.html_head.extend(self.head[1:])
+        self.body_prefix.append(self.starttag(node, **self.documenttag_args))
+        self.body_suffix.insert(0, f'</{self.documenttag_args["tagname"]}>\n')
+        self.fragment.extend(self.body)  # self.fragment is the "naked" body
+        self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
+                              + self.docinfo + self.body
+                              + self.body_suffix[:-1])
+        assert not self.context, f'len(context) = {len(self.context)}'
+
+    def visit_emphasis(self, node):
+        self.body.append(self.starttag(node, 'em', ''))
+
+    def depart_emphasis(self, node):
+        self.body.append('</em>')
+
+    def visit_entry(self, node):
+        atts = {'classes': []}
+        if isinstance(node.parent.parent, nodes.thead):
+            atts['classes'].append('head')
+        if node.parent.parent.parent.stubs[node.parent.column]:
+            # "stubs" list is an attribute of the tgroup element
+            atts['classes'].append('stub')
+        if atts['classes']:
+            tagname = 'th'
+        else:
+            tagname = 'td'
+        node.parent.column += 1
+        if 'morerows' in node:
+            atts['rowspan'] = node['morerows'] + 1
+        if 'morecols' in node:
+            atts['colspan'] = node['morecols'] + 1
+            node.parent.column += node['morecols']
+        self.body.append(self.starttag(node, tagname, '', **atts))
+        self.context.append('</%s>\n' % tagname.lower())
+
+    def depart_entry(self, node):
+        self.body.append(self.context.pop())
+
+    def visit_enumerated_list(self, node):
+        atts = {'classes': []}
+        if 'start' in node:
+            atts['start'] = node['start']
+        if 'enumtype' in node:
+            atts['classes'].append(node['enumtype'])
+        if self.is_compactable(node):
+            atts['classes'].append('simple')
+        self.body.append(self.starttag(node, 'ol', **atts))
+
+    def depart_enumerated_list(self, node):
+        self.body.append('</ol>\n')
+
+    def visit_field_list(self, node):
+        atts = {}
+        classes = node.setdefault('classes', [])
+        for i, cls in enumerate(classes):
+            if cls.startswith('field-indent-'):
+                try:
+                    indent_length = length_or_percentage_or_unitless(
+                                                        cls[13:], 'px')
+                except ValueError:
+                    break
+                atts['style'] = '--field-indent: %s;' % indent_length
+                classes.pop(i)
+                break
+        classes.append('field-list')
+        if self.is_compactable(node):
+            classes.append('simple')
+        self.body.append(self.starttag(node, 'dl', **atts))
+
+    def depart_field_list(self, node):
+        self.body.append('</dl>\n')
+
+    def visit_field(self, node):
+        # Insert children (<field_name> and <field_body>) directly.
+        # Transfer "id" attribute to the <field_name> child node.
+        for child in node:
+            if isinstance(child, nodes.field_name):
+                child['ids'].extend(node['ids'])
+
+    def depart_field(self, node):
+        pass
+
+    # as field is ignored, pass class arguments to field-name and field-body:
+    def visit_field_name(self, node):
+        self.body.append(self.starttag(node, 'dt', '',
+                                       classes=node.parent['classes']))
+
+    def depart_field_name(self, node):
+        self.body.append('<span class="colon">:</span></dt>\n')
+
+    def visit_field_body(self, node):
+        self.body.append(self.starttag(node, 'dd', '',
+                                       classes=node.parent['classes']))
+        # prevent misalignment of following content if the field is empty:
+        if not node.children:
+            self.body.append('<p></p>')
+
+    def depart_field_body(self, node):
+        self.body.append('</dd>\n')
+
+    def visit_figure(self, node):
+        atts = {'class': 'figure'}
+        if node.get('width'):
+            atts['style'] = 'width: %s' % node['width']
+        if node.get('align'):
+            atts['class'] += " align-" + node['align']
+        self.body.append(self.starttag(node, 'div', **atts))
+
+    def depart_figure(self, node):
+        self.body.append('</div>\n')
+
+    def visit_footer(self, node):
+        self.context.append(len(self.body))
+
+    def depart_footer(self, node):
+        start = self.context.pop()
+        footer = [self.starttag(node, 'div', CLASS='footer'),
+                  '<hr class="footer" />\n']
+        footer.extend(self.body[start:])
+        footer.append('\n</div>\n')
+        self.footer.extend(footer)
+        self.body_suffix[:0] = footer
+        del self.body[start:]
+
+    def visit_footnote(self, node):
+        # No native HTML element: use <aside> with ARIA role
+        # (html4css1 uses tables).
+        # Wrap groups of footnotes for easier styling.
+        label_style = self.settings.footnote_references  # brackets/superscript
+        if not isinstance(node.previous_sibling(), type(node)):
+            self.body.append(f'<aside class="footnote-list {label_style}">\n')
+        self.body.append(self.starttag(node, 'aside',
+                                       classes=[node.tagname, label_style],
+                                       role="doc-footnote"))
+
+    def depart_footnote(self, node):
+        self.body.append('</aside>\n')
+        if not isinstance(node.next_node(descend=False, siblings=True),
+                          type(node)):
+            self.body.append('</aside>\n')
+
+    def visit_footnote_reference(self, node):
+        href = '#' + node['refid']
+        classes = [self.settings.footnote_references]
+        self.body.append(self.starttag(node, 'a', suffix='', classes=classes,
+                                       role='doc-noteref', href=href))
+        self.body.append('<span class="fn-bracket">[</span>')
+
+    def depart_footnote_reference(self, node):
+        self.body.append('<span class="fn-bracket">]</span>')
+        self.body.append('</a>')
+
+    # Docutils-generated text: put section numbers in a span for CSS styling:
+    def visit_generated(self, node):
+        if 'sectnum' in node['classes']:
+            # get section number (strip trailing no-break-spaces)
+            sectnum = node.astext().rstrip(' ')
+            self.body.append('<span class="sectnum">%s </span>'
+                             % self.encode(sectnum))
+            # Content already processed:
+            raise nodes.SkipNode
+
+    def depart_generated(self, node):
+        pass
+
+    def visit_header(self, node):
+        self.context.append(len(self.body))
+
+    def depart_header(self, node):
+        start = self.context.pop()
+        header = [self.starttag(node, 'div', CLASS='header')]
+        header.extend(self.body[start:])
+        header.append('\n<hr class="header"/>\n</div>\n')
+        self.body_prefix.extend(header)
+        self.header.extend(header)
+        del self.body[start:]
+
+    def visit_image(self, node):
+        # reference/embed images (still images and videos)
+        uri = node['uri']
+        alt = node.get('alt', uri)
+        mimetype = mimetypes.guess_type(uri)[0]
+        element = ''  # the HTML element (including potential children)
+        atts = {}  # attributes for the HTML tag
+        # alignment is handled by CSS rules
+        if 'align' in node:
+            atts['class'] = 'align-%s' % node['align']
+        # set size with "style" attribute (more universal, accepts dimensions)
+        size_declaration = self.image_size(node)
+        if size_declaration:
+            atts['style'] = size_declaration
+
+        # ``:loading:`` option (embed, link, lazy), default from setting,
+        # exception: only embed videos if told via directive option
+        loading = 'link' if mimetype in self.videotypes else self.image_loading
+        loading = node.get('loading', loading)
+        if loading == 'lazy':
+            atts['loading'] = 'lazy'
+        elif loading == 'embed':
+            try:
+                imagepath = self.uri2imagepath(uri)
+                with open(imagepath, 'rb') as imagefile:
+                    imagedata = imagefile.read()
+            except (ValueError, OSError) as err:
+                self.messages.append(self.document.reporter.error(
+                    f'Cannot embed image "{uri}":\n  {err}', base_node=node))
+                # TODO: get external files with urllib.request (cf. odtwriter)?
+            else:
+                self.settings.record_dependencies.add(imagepath)
+                if mimetype == 'image/svg+xml':
+                    element = self.prepare_svg(node, imagedata,
+                                               size_declaration)
+                else:
+                    data64 = base64.b64encode(imagedata).decode()
+                    uri = f'data:{mimetype};base64,{data64}'
+
+        # No newlines around inline images (but all images may be nested
+        # in a `reference` node which is a `TextElement` instance):
+        if (not isinstance(node.parent, nodes.TextElement)
+            or isinstance(node.parent, nodes.reference)
+            and not isinstance(node.parent.parent, nodes.TextElement)):
+            suffix = '\n'
+        else:
+            suffix = ''
+
+        if mimetype in self.videotypes:
+            atts['title'] = alt
+            if 'controls' in node['classes']:
+                node['classes'].remove('controls')
+                atts['controls'] = 'controls'
+            element = (self.starttag(node, "video", suffix, src=uri, **atts)
+                       + f'<a href="{node["uri"]}">{alt}</a>{suffix}'
+                       + f'</video>{suffix}')
+        elif mimetype == 'application/x-shockwave-flash':
+            atts['type'] = mimetype
+            element = (self.starttag(node, 'object', '', data=uri, **atts)
+                       + f'{alt}</object>{suffix}')
+        elif element:  # embedded SVG, see above
+            element += suffix
+        else:
+            atts['alt'] = alt
+            element = self.emptytag(node, 'img', suffix, src=uri, **atts)
+        self.body.append(element)
+        if suffix:  # block-element
+            self.report_messages(node)
+
+    def depart_image(self, node):
+        pass
+
+    def visit_inline(self, node):
+        self.body.append(self.starttag(node, 'span', ''))
+
+    def depart_inline(self, node):
+        self.body.append('</span>')
+
+    # footnote and citation labels:
+    def visit_label(self, node):
+        self.body.append('<span class="label">')
+        self.body.append('<span class="fn-bracket">[</span>')
+        # footnote/citation backrefs:
+        if self.settings.footnote_backlinks:
+            backrefs = node.parent.get('backrefs', [])
+            if len(backrefs) == 1:
+                self.body.append('<a role="doc-backlink"'
+                                 ' href="#%s">' % backrefs[0])
+
+    def depart_label(self, node):
+        backrefs = []
+        if self.settings.footnote_backlinks:
+            backrefs = node.parent.get('backrefs', backrefs)
+        if len(backrefs) == 1:
+            self.body.append('</a>')
+        self.body.append('<span class="fn-bracket">]</span></span>\n')
+        if len(backrefs) > 1:
+            backlinks = ['<a role="doc-backlink" href="#%s">%s</a>' % (ref, i)
+                         for (i, ref) in enumerate(backrefs, 1)]
+            self.body.append('<span class="backrefs">(%s)</span>\n'
+                             % ','.join(backlinks))
+
+    def visit_legend(self, node):
+        self.body.append(self.starttag(node, 'div', CLASS='legend'))
+
+    def depart_legend(self, node):
+        self.body.append('</div>\n')
+
+    def visit_line(self, node):
+        self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
+        if not len(node):
+            self.body.append('<br />')
+
+    def depart_line(self, node):
+        self.body.append('</div>\n')
+
+    def visit_line_block(self, node):
+        self.body.append(self.starttag(node, 'div', CLASS='line-block'))
+
+    def depart_line_block(self, node):
+        self.body.append('</div>\n')
+
+    def visit_list_item(self, node):
+        self.body.append(self.starttag(node, 'li', ''))
+
+    def depart_list_item(self, node):
+        self.body.append('</li>\n')
+
+    # inline literal
+    def visit_literal(self, node):
+        # special case: "code" role
+        classes = node['classes']
+        if 'code' in classes:
+            # filter 'code' from class arguments
+            classes.pop(classes.index('code'))
+            self.body.append(self.starttag(node, 'code', ''))
+            return
+        self.body.append(
+            self.starttag(node, 'span', '', CLASS='docutils literal'))
+        text = node.astext()
+        if not isinstance(node.parent, nodes.literal_block):
+            text = text.replace('\n', ' ')
+        # Protect text like ``--an-option`` and the regular expression
+        # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+        for token in self.words_and_spaces.findall(text):
+            if token.strip() and self.in_word_wrap_point.search(token):
+                self.body.append('<span class="pre">%s</span>'
+                                 % self.encode(token))
+            else:
+                self.body.append(self.encode(token))
+        self.body.append('</span>')
+        raise nodes.SkipNode  # content already processed
+
+    def depart_literal(self, node):
+        # skipped unless literal element is from "code" role:
+        self.body.append('</code>')
+
+    def visit_literal_block(self, node):
+        self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
+        if 'code' in node['classes']:
+            self.body.append('<code>')
+
+    def depart_literal_block(self, node):
+        if 'code' in node['classes']:
+            self.body.append('</code>')
+        self.body.append('</pre>\n')
+
+    # Mathematics:
+    # As there is no native HTML math support, we provide alternatives
+    # for the math-output: LaTeX and MathJax simply wrap the content,
+    # HTML and MathML also convert the math_code.
+    # HTML element:
+    math_tags = {  # format: (inline, block, [class arguments])
+                 'html': ('span', 'div', ['formula']),
+                 'latex': ('tt', 'pre', ['math']),
+                 'mathjax': ('span', 'div', ['math']),
+                 'mathml': ('', 'div', []),
+                 'problematic': ('span', 'pre', ['math', 'problematic']),
+                 }
+
+    def visit_math(self, node):
+        # Also called from `visit_math_block()`:
+        is_block = isinstance(node, nodes.math_block)
+        format = self.math_output
+        math_code = node.astext().translate(unichar2tex.uni2tex_table)
+
+        # preamble code and conversion
+        if format == 'html':
+            if self.math_options and not self.math_header:
+                self.math_header = [
+                    self.stylesheet_call(utils.find_file_in_dirs(
+                        s, self.settings.stylesheet_dirs), adjust_path=True)
+                    for s in self.math_options.split(',')]
+            math2html.DocumentParameters.displaymode = is_block
+            # TODO: fix display mode in matrices and fractions
+            math_code = wrap_math_code(math_code, is_block)
+            math_code = math2html.math2html(math_code)
+        elif format == 'latex':
+            math_code = self.encode(math_code)
+        elif format == 'mathjax':
+            if not self.math_header:
+                if self.math_options:
+                    self.mathjax_url = self.math_options
+                else:
+                    self.document.reporter.warning(
+                        'No MathJax URL specified, using local fallback '
+                        '(see config.html).', base_node=node)
+                # append MathJax configuration
+                # (input LaTeX with AMS, output common HTML):
+                if '?' not in self.mathjax_url:
+                    self.mathjax_url += '?config=TeX-AMS_CHTML'
+                self.math_header = [self.mathjax_script % self.mathjax_url]
+            if is_block:
+                math_code = wrap_math_code(math_code, is_block)
+            else:
+                math_code = rf'\({math_code}\)'
+            math_code = self.encode(math_code)
+        elif format == 'mathml':
+            if 'XHTML 1' in self.doctype:
+                self.doctype = self.doctype_mathml
+                self.content_type = self.content_type_mathml
+            if self.math_options:
+                converter = getattr(tex2mathml_extern, self.math_options)
+            else:
+                converter = latex2mathml.tex2mathml
+            try:
+                math_code = converter(math_code, as_block=is_block)
+            except (MathError, OSError) as err:
+                details = getattr(err, 'details', [])
+                self.messages.append(self.document.reporter.warning(
+                    err, *details, base_node=node))
+                math_code = self.encode(node.astext())
+                if self.settings.report_level <= 2:
+                    format = 'problematic'
+                else:
+                    format = 'latex'
+                if isinstance(err, OSError):
+                    # report missing converter only once
+                    self.math_output = format
+
+        # append to document body
+        tag = self.math_tags[format][is_block]
+        suffix = '\n' if is_block else ''
+        if tag:
+            self.body.append(self.starttag(node, tag, suffix=suffix,
+                                           classes=self.math_tags[format][2]))
+        self.body.extend([math_code, suffix])
+        if tag:
+            self.body.append(f'</{tag}>{suffix}')
+        # Content already processed:
+        raise nodes.SkipChildren
+
+    def depart_math(self, node):
+        pass
+
+    def visit_math_block(self, node):
+        self.visit_math(node)
+
+    def depart_math_block(self, node):
+        self.report_messages(node)
+
+    # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
+    # HTML5/polyglot recommends using both
+    def visit_meta(self, node):
+        self.meta.append(self.emptytag(node, 'meta',
+                                       **node.non_default_attributes()))
+
+    def depart_meta(self, node):
+        pass
+
+    def visit_option(self, node):
+        self.body.append(self.starttag(node, 'span', '', CLASS='option'))
+
+    def depart_option(self, node):
+        self.body.append('</span>')
+        if isinstance(node.next_node(descend=False, siblings=True),
+                      nodes.option):
+            self.body.append(', ')
+
+    def visit_option_argument(self, node):
+        self.body.append(node.get('delimiter', ' '))
+        self.body.append(self.starttag(node, 'var', ''))
+
+    def depart_option_argument(self, node):
+        self.body.append('</var>')
+
+    def visit_option_group(self, node):
+        self.body.append(self.starttag(node, 'dt', ''))
+        self.body.append('<kbd>')
+
+    def depart_option_group(self, node):
+        self.body.append('</kbd></dt>\n')
+
+    def visit_option_list(self, node):
+        self.body.append(
+            self.starttag(node, 'dl', CLASS='option-list'))
+
+    def depart_option_list(self, node):
+        self.body.append('</dl>\n')
+
+    def visit_option_list_item(self, node):
+        pass
+
+    def depart_option_list_item(self, node):
+        pass
+
+    def visit_option_string(self, node):
+        pass
+
+    def depart_option_string(self, node):
+        pass
+
+    def visit_organization(self, node):
+        self.visit_docinfo_item(node, 'organization')
+
+    def depart_organization(self, node):
+        self.depart_docinfo_item()
+
+    # Do not omit <p> tags
+    # --------------------
+    #
+    # The HTML4CSS1 writer does this to "produce
+    # visually compact lists (less vertical whitespace)". This writer
+    # relies on CSS rules for visual compactness.
+    #
+    # * In XHTML 1.1, e.g., a <blockquote> element may not contain
+    #   character data, so you cannot drop the <p> tags.
+    # * Keeping simple paragraphs in the field_body enables a CSS
+    #   rule to start the field-body on a new line if the label is too long
+    # * it makes the code simpler.
+    #
+    # TODO: omit paragraph tags in simple table cells?
+
+    def visit_paragraph(self, node):
+        self.body.append(self.starttag(node, 'p', ''))
+
+    def depart_paragraph(self, node):
+        self.body.append('</p>')
+        if not (isinstance(node.parent, (nodes.list_item, nodes.entry))
+                and (len(node.parent) == 1)):
+            self.body.append('\n')
+            self.report_messages(node)
+
+    def visit_problematic(self, node):
+        if node.hasattr('refid'):
+            self.body.append('<a href="#%s">' % node['refid'])
+            self.context.append('</a>')
+        else:
+            self.context.append('')
+        self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
+
+    def depart_problematic(self, node):
+        self.body.append('</span>')
+        self.body.append(self.context.pop())
+
+    def visit_raw(self, node):
+        if 'html' in node.get('format', '').split():
+            if isinstance(node.parent, nodes.TextElement):
+                tagname = 'span'
+            else:
+                tagname = 'div'
+            if node['classes']:
+                self.body.append(self.starttag(node, tagname, suffix=''))
+            self.body.append(node.astext())
+            if node['classes']:
+                self.body.append('</%s>' % tagname)
+        # Keep non-HTML raw text out of output:
+        raise nodes.SkipNode
+
+    def visit_reference(self, node):
+        atts = {'classes': ['reference']}
+        suffix = ''
+        if 'refuri' in node:
+            atts['href'] = node['refuri']
+            if (self.settings.cloak_email_addresses
+                and atts['href'].startswith('mailto:')):
+                atts['href'] = self.cloak_mailto(atts['href'])
+                self.in_mailto = True
+            atts['classes'].append('external')
+        else:
+            assert 'refid' in node, \
+                   'References must have "refuri" or "refid" attribute.'
+            atts['href'] = '#' + node['refid']
+            atts['classes'].append('internal')
+        if len(node) == 1 and isinstance(node[0], nodes.image):
+            atts['classes'].append('image-reference')
+        if not isinstance(node.parent, nodes.TextElement):
+            suffix = '\n'
+        self.body.append(self.starttag(node, 'a', suffix, **atts))
+
+    def depart_reference(self, node):
+        self.body.append('</a>')
+        if not isinstance(node.parent, nodes.TextElement):
+            self.body.append('\n')
+        self.in_mailto = False
+
+    def visit_revision(self, node):
+        self.visit_docinfo_item(node, 'revision', meta=False)
+
+    def depart_revision(self, node):
+        self.depart_docinfo_item()
+
+    def visit_row(self, node):
+        self.body.append(self.starttag(node, 'tr', ''))
+        node.column = 0
+
+    def depart_row(self, node):
+        self.body.append('</tr>\n')
+
+    def visit_rubric(self, node):
+        self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
+
+    def depart_rubric(self, node):
+        self.body.append('</p>\n')
+
+    def visit_section(self, node):
+        self.section_level += 1
+        self.body.append(
+            self.starttag(node, 'div', CLASS='section'))
+
+    def depart_section(self, node):
+        self.section_level -= 1
+        self.body.append('</div>\n')
+
+    # TODO: use the new HTML5 element <aside>
+    def visit_sidebar(self, node):
+        self.body.append(
+            self.starttag(node, 'div', CLASS='sidebar'))
+        self.in_sidebar = True
+
+    def depart_sidebar(self, node):
+        self.body.append('</div>\n')
+        self.in_sidebar = False
+
+    def visit_status(self, node):
+        self.visit_docinfo_item(node, 'status', meta=False)
+
+    def depart_status(self, node):
+        self.depart_docinfo_item()
+
+    def visit_strong(self, node):
+        self.body.append(self.starttag(node, 'strong', ''))
+
+    def depart_strong(self, node):
+        self.body.append('</strong>')
+
+    def visit_subscript(self, node):
+        self.body.append(self.starttag(node, 'sub', ''))
+
+    def depart_subscript(self, node):
+        self.body.append('</sub>')
+
+    def visit_substitution_definition(self, node):
+        """Internal only."""
+        raise nodes.SkipNode
+
+    def visit_substitution_reference(self, node):
+        self.unimplemented_visit(node)
+
+    # h1–h6 elements must not be used to markup subheadings, subtitles,
+    # alternative titles and taglines unless intended to be the heading for a
+    # new section or subsection.
+    # -- http://www.w3.org/TR/html51/sections.html#headings-and-sections
+    def visit_subtitle(self, node):
+        if isinstance(node.parent, nodes.sidebar):
+            classes = ['sidebar-subtitle']
+        elif isinstance(node.parent, nodes.document):
+            classes = ['subtitle']
+            self.in_document_title = len(self.body) + 1
+        elif isinstance(node.parent, nodes.section):
+            classes = ['section-subtitle']
+        self.body.append(self.starttag(node, 'p', '', classes=classes))
+
+    def depart_subtitle(self, node):
+        self.body.append('</p>\n')
+        if isinstance(node.parent, nodes.document):
+            self.subtitle = self.body[self.in_document_title:-1]
+            self.in_document_title = 0
+            self.body_pre_docinfo.extend(self.body)
+            self.html_subtitle.extend(self.body)
+            del self.body[:]
+
+    def visit_superscript(self, node):
+        self.body.append(self.starttag(node, 'sup', ''))
+
+    def depart_superscript(self, node):
+        self.body.append('</sup>')
+
+    def visit_system_message(self, node):
+        self.body.append(self.starttag(node, 'aside', CLASS='system-message'))
+        self.body.append('<p class="system-message-title">')
+        backref_text = ''
+        if len(node['backrefs']):
+            backrefs = node['backrefs']
+            if len(backrefs) == 1:
+                backref_text = ('; <em><a href="#%s">backlink</a></em>'
+                                % backrefs[0])
+            else:
+                i = 1
+                backlinks = []
+                for backref in backrefs:
+                    backlinks.append('<a href="#%s">%s</a>' % (backref, i))
+                    i += 1
+                backref_text = ('; <em>backlinks: %s</em>'
+                                % ', '.join(backlinks))
+        if node.hasattr('line'):
+            line = ', line %s' % node['line']
+        else:
+            line = ''
+        self.body.append('System Message: %s/%s '
+                         '(<span class="docutils literal">%s</span>%s)%s</p>\n'
+                         % (node['type'], node['level'],
+                            self.encode(node['source']), line, backref_text))
+
+    def depart_system_message(self, node):
+        self.body.append('</aside>\n')
+
+    def visit_table(self, node):
+        atts = {'classes': self.settings.table_style.replace(',', ' ').split()}
+        if 'align' in node:
+            atts['classes'].append('align-%s' % node['align'])
+        if 'width' in node:
+            atts['style'] = 'width: %s;' % node['width']
+        tag = self.starttag(node, 'table', **atts)
+        self.body.append(tag)
+
+    def depart_table(self, node):
+        self.body.append('</table>\n')
+        self.report_messages(node)
+
+    def visit_target(self, node):
+        if ('refuri' not in node
+                and 'refid' not in node
+                and 'refname' not in node):
+            self.body.append(self.starttag(node, 'span', '', CLASS='target'))
+            self.context.append('</span>')
+        else:
+            self.context.append('')
+
+    def depart_target(self, node):
+        self.body.append(self.context.pop())
+
+    # no hard-coded vertical alignment in table body
+    def visit_tbody(self, node):
+        self.body.append(self.starttag(node, 'tbody'))
+
+    def depart_tbody(self, node):
+        self.body.append('</tbody>\n')
+
+    def visit_term(self, node):
+        if 'details' in node.parent.parent['classes']:
+            self.body.append(self.starttag(node, 'summary', suffix=''))
+        else:
+            # The parent node (definition_list_item) is omitted in HTML.
+            self.body.append(self.starttag(node, 'dt', suffix='',
+                                           classes=node.parent['classes'],
+                                           ids=node.parent['ids']))
+
+    def depart_term(self, node):
+        # Nest (optional) classifier(s) in the <dt> element
+        if node.next_node(nodes.classifier, descend=False, siblings=True):
+            return  # skip (depart_classifier() calls this function again)
+        if 'details' in node.parent.parent['classes']:
+            self.body.append('</summary>\n')
+        else:
+            self.body.append('</dt>\n')
+
+    def visit_tgroup(self, node):
+        self.colspecs = []
+        node.stubs = []
+
+    def depart_tgroup(self, node):
+        pass
+
+    def visit_thead(self, node):
+        self.body.append(self.starttag(node, 'thead'))
+
+    def depart_thead(self, node):
+        self.body.append('</thead>\n')
+
+    def section_title_tags(self, node):
+        atts = {}
+        h_level = self.section_level + self.initial_header_level - 1
+        # Only 6 heading levels have dedicated HTML tags.
+        tagname = 'h%i' % min(h_level, 6)
+        if h_level > 6:
+            atts['aria-level'] = h_level
+        start_tag = self.starttag(node, tagname, '', **atts)
+        if node.hasattr('refid'):
+            atts = {}
+            atts['class'] = 'toc-backref'
+            atts['role'] = 'doc-backlink'  # HTML5 only
+            atts['href'] = '#' + node['refid']
+            start_tag += self.starttag(nodes.reference(), 'a', '', **atts)
+            close_tag = '</a></%s>\n' % tagname
+        else:
+            close_tag = '</%s>\n' % tagname
+        return start_tag, close_tag
+
+    def visit_title(self, node):
+        close_tag = '</p>\n'
+        if isinstance(node.parent, nodes.topic):
+            # TODO: use role="heading" or <h1>? (HTML5 only)
+            self.body.append(
+                self.starttag(node, 'p', '', CLASS='topic-title'))
+            if (self.settings.toc_backlinks
+                and 'contents' in node.parent['classes']):
+                self.body.append('<a class="reference internal" href="#top">')
+                close_tag = '</a></p>\n'
+        elif isinstance(node.parent, nodes.sidebar):
+            # TODO: use role="heading" or <h1>? (HTML5 only)
+            self.body.append(
+                self.starttag(node, 'p', '', CLASS='sidebar-title'))
+        elif isinstance(node.parent, nodes.Admonition):
+            self.body.append(
+                  self.starttag(node, 'p', '', CLASS='admonition-title'))
+        elif isinstance(node.parent, nodes.table):
+            self.body.append(self.starttag(node, 'caption', ''))
+            close_tag = '</caption>\n'
+        elif isinstance(node.parent, nodes.document):
+            self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
+            close_tag = '</h1>\n'
+            self.in_document_title = len(self.body)
+        else:
+            assert isinstance(node.parent, nodes.section)
+            # Get correct heading and evt. backlink tags
+            start_tag, close_tag = self.section_title_tags(node)
+            self.body.append(start_tag)
+        self.context.append(close_tag)
+
+    def depart_title(self, node):
+        self.body.append(self.context.pop())
+        if self.in_document_title:
+            self.title = self.body[self.in_document_title:-1]
+            self.in_document_title = 0
+            self.body_pre_docinfo.extend(self.body)
+            self.html_title.extend(self.body)
+            del self.body[:]
+
+    def visit_title_reference(self, node):
+        self.body.append(self.starttag(node, 'cite', ''))
+
+    def depart_title_reference(self, node):
+        self.body.append('</cite>')
+
+    def visit_topic(self, node):
+        self.body.append(self.starttag(node, 'div', CLASS='topic'))
+
+    def depart_topic(self, node):
+        self.body.append('</div>\n')
+
+    def visit_transition(self, node):
+        self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
+
+    def depart_transition(self, node):
+        pass
+
+    def visit_version(self, node):
+        self.visit_docinfo_item(node, 'version', meta=False)
+
+    def depart_version(self, node):
+        self.depart_docinfo_item()
+
+    def unimplemented_visit(self, node):
+        raise NotImplementedError('visiting unimplemented node type: %s'
+                                  % node.__class__.__name__)
+
+
+class SimpleListChecker(nodes.GenericNodeVisitor):
+
+    """
+    Raise `nodes.NodeFound` if non-simple list item is encountered.
+
+    Here "simple" means a list item containing nothing other than a single
+    paragraph, a simple list, or a paragraph followed by a simple list.
+
+    This version also checks for simple field lists and docinfo.
+    """
+
+    def default_visit(self, node):
+        raise nodes.NodeFound
+
+    def visit_list_item(self, node):
+        children = [child for child in node.children
+                    if not isinstance(child, nodes.Invisible)]
+        if (children and isinstance(children[0], nodes.paragraph)
+            and (isinstance(children[-1], nodes.bullet_list)
+                 or isinstance(children[-1], nodes.enumerated_list)
+                 or isinstance(children[-1], nodes.field_list))):
+            children.pop()
+        if len(children) <= 1:
+            return
+        else:
+            raise nodes.NodeFound
+
+    def pass_node(self, node):
+        pass
+
+    def ignore_node(self, node):
+        # ignore nodes that are never complex (can contain only inline nodes)
+        raise nodes.SkipNode
+
+    # Paragraphs and text
+    visit_Text = ignore_node
+    visit_paragraph = ignore_node
+
+    # Lists
+    visit_bullet_list = pass_node
+    visit_enumerated_list = pass_node
+    visit_docinfo = pass_node
+
+    # Docinfo nodes:
+    visit_author = ignore_node
+    visit_authors = visit_list_item
+    visit_address = visit_list_item
+    visit_contact = pass_node
+    visit_copyright = ignore_node
+    visit_date = ignore_node
+    visit_organization = ignore_node
+    visit_status = ignore_node
+    visit_version = visit_list_item
+
+    # Definition list:
+    visit_definition_list = pass_node
+    visit_definition_list_item = pass_node
+    visit_term = ignore_node
+    visit_classifier = pass_node
+    visit_definition = visit_list_item
+
+    # Field list:
+    visit_field_list = pass_node
+    visit_field = pass_node
+    # the field body corresponds to a list item
+    visit_field_body = visit_list_item
+    visit_field_name = ignore_node
+
+    # Invisible nodes should be ignored.
+    visit_comment = ignore_node
+    visit_substitution_definition = ignore_node
+    visit_target = ignore_node
+    visit_pending = ignore_node
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py b/.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py
new file mode 100644
index 00000000..f4169295
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py
@@ -0,0 +1,187 @@
+# $Id: docutils_xml.py 9502 2023-12-14 22:39:08Z milde $
+# Author: David Goodger, Paul Tremblay, Guenter Milde
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""
+Simple document tree Writer, writes Docutils XML according to
+https://docutils.sourceforge.io/docs/ref/docutils.dtd.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from io import StringIO
+import xml.sax.saxutils
+
+import docutils
+from docutils import frontend, nodes, writers, utils
+
+
+class RawXmlError(docutils.ApplicationError):
+    pass
+
+
+class Writer(writers.Writer):
+
+    supported = ('xml',)
+    """Formats this writer supports."""
+
+    settings_spec = (
+        '"Docutils XML" Writer Options',
+        None,
+        (('Generate XML with newlines before and after tags.',
+          ['--newlines'],
+          {'action': 'store_true', 'validator': frontend.validate_boolean}),
+         ('Generate XML with indents and newlines.',
+          ['--indents'],  # TODO use integer value for number of spaces?
+          {'action': 'store_true', 'validator': frontend.validate_boolean}),
+         ('Omit the XML declaration.  Use with caution.',
+          ['--no-xml-declaration'],
+          {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
+           'validator': frontend.validate_boolean}),
+         ('Omit the DOCTYPE declaration.',
+          ['--no-doctype'],
+          {'dest': 'doctype_declaration', 'default': 1,
+           'action': 'store_false', 'validator': frontend.validate_boolean}),))
+
+    settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
+
+    config_section = 'docutils_xml writer'
+    config_section_dependencies = ('writers',)
+
+    output = None
+    """Final translated form of `document`."""
+
+    def __init__(self):
+        writers.Writer.__init__(self)
+        self.translator_class = XMLTranslator
+
+    def translate(self):
+        self.visitor = visitor = self.translator_class(self.document)
+        self.document.walkabout(visitor)
+        self.output = ''.join(visitor.output)
+
+
+class XMLTranslator(nodes.GenericNodeVisitor):
+
+    # TODO: add stylesheet options similar to HTML and LaTeX writers?
+    # xml_stylesheet = '<?xml-stylesheet type="text/xsl" href="%s"?>\n'
+    doctype = (
+        '<!DOCTYPE document PUBLIC'
+        ' "+//IDN docutils.sourceforge.net//DTD Docutils Generic//EN//XML"'
+        ' "http://docutils.sourceforge.net/docs/ref/docutils.dtd">\n')
+    generator = '<!-- Generated by Docutils %s -->\n'
+
+    xmlparser = xml.sax.make_parser()
+    """SAX parser instance to check/extract raw XML."""
+    xmlparser.setFeature(
+        "http://xml.org/sax/features/external-general-entities", True)
+
+    def __init__(self, document):
+        nodes.NodeVisitor.__init__(self, document)
+
+        # Reporter
+        self.warn = self.document.reporter.warning
+        self.error = self.document.reporter.error
+
+        # Settings
+        self.settings = settings = document.settings
+        self.indent = self.newline = ''
+        if settings.newlines:
+            self.newline = '\n'
+        if settings.indents:
+            self.newline = '\n'
+            self.indent = '    '  # TODO make this configurable?
+        self.level = 0       # indentation level
+        self.in_simple = 0   # level of nesting inside mixed-content elements
+        self.fixed_text = 0  # level of nesting inside FixedText elements
+
+        # Output
+        self.output = []
+        if settings.xml_declaration:
+            self.output.append(utils.xml_declaration(settings.output_encoding))
+        if settings.doctype_declaration:
+            self.output.append(self.doctype)
+        self.output.append(self.generator % docutils.__version__)
+
+        # initialize XML parser
+        self.the_handle = TestXml()
+        self.xmlparser.setContentHandler(self.the_handle)
+
+    # generic visit and depart methods
+    # --------------------------------
+
+    simple_nodes = (nodes.TextElement, nodes.meta,
+                    nodes.image, nodes.colspec, nodes.transition)
+
+    def default_visit(self, node):
+        """Default node visit method."""
+        if not self.in_simple:
+            self.output.append(self.indent*self.level)
+        self.output.append(node.starttag(xml.sax.saxutils.quoteattr))
+        self.level += 1
+        # `nodes.literal` is not an instance of FixedTextElement by design,
+        # see docs/ref/rst/restructuredtext.html#inline-literals
+        if isinstance(node, (nodes.FixedTextElement, nodes.literal)):
+            self.fixed_text += 1
+        if isinstance(node, self.simple_nodes):
+            self.in_simple += 1
+        if not self.in_simple:
+            self.output.append(self.newline)
+
+    def default_departure(self, node):
+        """Default node depart method."""
+        self.level -= 1
+        if not self.in_simple:
+            self.output.append(self.indent*self.level)
+        self.output.append(node.endtag())
+        if isinstance(node, (nodes.FixedTextElement, nodes.literal)):
+            self.fixed_text -= 1
+        if isinstance(node, self.simple_nodes):
+            self.in_simple -= 1
+        if not self.in_simple:
+            self.output.append(self.newline)
+
+    # specific visit and depart methods
+    # ---------------------------------
+
+    def visit_Text(self, node):
+        text = xml.sax.saxutils.escape(node.astext())
+        # indent text if we are not in a FixedText element:
+        if not self.fixed_text:
+            text = text.replace('\n', '\n'+self.indent*self.level)
+        self.output.append(text)
+
+    def depart_Text(self, node):
+        pass
+
+    def visit_raw(self, node):
+        if 'xml' not in node.get('format', '').split():
+            # skip other raw content?
+            # raise nodes.SkipNode
+            self.default_visit(node)
+            return
+        # wrap in <raw> element
+        self.default_visit(node)      # or not?
+        xml_string = node.astext()
+        self.output.append(xml_string)
+        self.default_departure(node)  # or not?
+        # Check validity of raw XML:
+        try:
+            self.xmlparser.parse(StringIO(xml_string))
+        except xml.sax._exceptions.SAXParseException:
+            col_num = self.the_handle.locator.getColumnNumber()
+            line_num = self.the_handle.locator.getLineNumber()
+            srcline = node.line
+            if not isinstance(node.parent, nodes.TextElement):
+                srcline += 2  # directive content start line
+            msg = 'Invalid raw XML in column %d, line offset %d:\n%s' % (
+                   col_num, line_num, node.astext())
+            self.warn(msg, source=node.source, line=srcline+line_num-1)
+        raise nodes.SkipNode  # content already processed
+
+
+class TestXml(xml.sax.handler.ContentHandler):
+
+    def setDocumentLocator(self, locator):
+        self.locator = locator
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py
new file mode 100644
index 00000000..799d30e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py
@@ -0,0 +1,955 @@
+# $Id: __init__.py 9558 2024-03-11 17:48:52Z milde $
+# Author: David Goodger
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""
+Simple HyperText Markup Language document tree Writer.
+
+The output conforms to the XHTML version 1.0 Transitional DTD
+(*almost* strict).  The output contains a minimum of formatting
+information.  The cascading style sheet "html4css1.css" is required
+for proper viewing with a modern graphical browser.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import os.path
+import re
+
+from docutils import frontend, nodes, writers
+from docutils.writers import _html_base
+from docutils.writers._html_base import PIL
+
+
+class Writer(writers._html_base.Writer):
+
+    supported = ('html', 'html4', 'html4css1', 'xhtml', 'xhtml10')
+    """Formats this writer supports."""
+
+    default_stylesheets = ['html4css1.css']
+    default_stylesheet_dirs = ['.',
+                               os.path.abspath(os.path.dirname(__file__)),
+                               os.path.abspath(os.path.join(
+                                   os.path.dirname(os.path.dirname(__file__)),
+                                   'html5_polyglot'))  # for math.css
+                               ]
+    default_template = os.path.join(
+        os.path.dirname(os.path.abspath(__file__)), 'template.txt')
+
+    # use a copy of the parent spec with some modifications
+    settings_spec = frontend.filter_settings_spec(
+        writers._html_base.Writer.settings_spec,
+        template=(
+            'Template file. (UTF-8 encoded, default: "%s")' % default_template,
+            ['--template'],
+            {'default': default_template, 'metavar': '<file>'}),
+        stylesheet_path=(
+            'Comma separated list of stylesheet paths. '
+            'Relative paths are expanded if a matching file is found in '
+            'the --stylesheet-dirs. With --link-stylesheet, '
+            'the path is rewritten relative to the output HTML file. '
+            '(default: "%s")' % ','.join(default_stylesheets),
+            ['--stylesheet-path'],
+            {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+             'validator': frontend.validate_comma_separated_list,
+             'default': default_stylesheets}),
+        stylesheet_dirs=(
+            'Comma-separated list of directories where stylesheets are found. '
+            'Used by --stylesheet-path when expanding relative path '
+            'arguments. (default: "%s")' % ','.join(default_stylesheet_dirs),
+            ['--stylesheet-dirs'],
+            {'metavar': '<dir[,dir,...]>',
+             'validator': frontend.validate_comma_separated_list,
+             'default': default_stylesheet_dirs}),
+        initial_header_level=(
+            'Specify the initial header level. Does not affect document '
+            'title & subtitle (see --no-doc-title). (default: 1 for "<h1>")',
+            ['--initial-header-level'],
+            {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
+             'metavar': '<level>'}),
+        xml_declaration=(
+            'Prepend an XML declaration (default). ',
+            ['--xml-declaration'],
+            {'default': True, 'action': 'store_true',
+             'validator': frontend.validate_boolean}),
+        )
+    settings_spec = settings_spec + (
+        'HTML4 Writer Options',
+        '',
+        (('Specify the maximum width (in characters) for one-column field '
+          'names.  Longer field names will span an entire row of the table '
+          'used to render the field list.  Default is 14 characters.  '
+          'Use 0 for "no limit".',
+          ['--field-name-limit'],
+          {'default': 14, 'metavar': '<level>',
+           'validator': frontend.validate_nonnegative_int}),
+         ('Specify the maximum width (in characters) for options in option '
+          'lists.  Longer options will span an entire row of the table used '
+          'to render the option list.  Default is 14 characters.  '
+          'Use 0 for "no limit".',
+          ['--option-limit'],
+          {'default': 14, 'metavar': '<level>',
+           'validator': frontend.validate_nonnegative_int}),
+         )
+        )
+
+    config_section = 'html4css1 writer'
+
+    def __init__(self):
+        self.parts = {}
+        self.translator_class = HTMLTranslator
+
+
+class HTMLTranslator(writers._html_base.HTMLTranslator):
+    """
+    The html4css1 writer has been optimized to produce visually compact
+    lists (less vertical whitespace).  HTML's mixed content models
+    allow list items to contain "<li><p>body elements</p></li>" or
+    "<li>just text</li>" or even "<li>text<p>and body
+    elements</p>combined</li>", each with different effects.  It would
+    be best to stick with strict body elements in list items, but they
+    affect vertical spacing in older browsers (although they really
+    shouldn't).
+    The html5_polyglot writer solves this using CSS2.
+
+    Here is an outline of the optimization:
+
+    - Check for and omit <p> tags in "simple" lists: list items
+      contain either a single paragraph, a nested simple list, or a
+      paragraph followed by a nested simple list.  This means that
+      this list can be compact:
+
+          - Item 1.
+          - Item 2.
+
+      But this list cannot be compact:
+
+          - Item 1.
+
+            This second paragraph forces space between list items.
+
+          - Item 2.
+
+    - In non-list contexts, omit <p> tags on a paragraph if that
+      paragraph is the only child of its parent (footnotes & citations
+      are allowed a label first).
+
+    - Regardless of the above, in definitions, table cells, field bodies,
+      option descriptions, and list items, mark the first child with
+      'class="first"' and the last child with 'class="last"'.  The stylesheet
+      sets the margins (top & bottom respectively) to 0 for these elements.
+
+    The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
+    option) disables list whitespace optimization.
+    """
+
+    # The following definitions are required for display in browsers limited
+    # to CSS1 or backwards compatible behaviour of the writer:
+
+    doctype = (
+        '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
+        ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
+
+    content_type = ('<meta http-equiv="Content-Type"'
+                    ' content="text/html; charset=%s" />\n')
+    content_type_mathml = ('<meta http-equiv="Content-Type"'
+                           ' content="application/xhtml+xml; charset=%s" />\n')
+
+    # encode also non-breaking space
+    special_characters = _html_base.HTMLTranslator.special_characters.copy()
+    special_characters[0xa0] = '&nbsp;'
+
+    # use character reference for dash (not valid in HTML5)
+    attribution_formats = {'dash': ('&mdash;', ''),
+                           'parentheses': ('(', ')'),
+                           'parens': ('(', ')'),
+                           'none': ('', '')}
+
+    # ersatz for first/last pseudo-classes missing in CSS1
+    def set_first_last(self, node):
+        self.set_class_on_child(node, 'first', 0)
+        self.set_class_on_child(node, 'last', -1)
+
+    # add newline after opening tag
+    def visit_address(self, node):
+        self.visit_docinfo_item(node, 'address', meta=False)
+        self.body.append(self.starttag(node, 'pre', CLASS='address'))
+
+    def depart_address(self, node):
+        self.body.append('\n</pre>\n')
+        self.depart_docinfo_item()
+
+    # ersatz for first/last pseudo-classes
+    def visit_admonition(self, node):
+        node['classes'].insert(0, 'admonition')
+        self.body.append(self.starttag(node, 'div'))
+        self.set_first_last(node)
+
+    def depart_admonition(self, node=None):
+        self.body.append('</div>\n')
+
+    # author, authors: use <br> instead of paragraphs
+    def visit_author(self, node):
+        if isinstance(node.parent, nodes.authors):
+            if self.author_in_authors:
+                self.body.append('\n<br />')
+        else:
+            self.visit_docinfo_item(node, 'author')
+
+    def depart_author(self, node):
+        if isinstance(node.parent, nodes.authors):
+            self.author_in_authors = True
+        else:
+            self.depart_docinfo_item()
+
+    def visit_authors(self, node):
+        self.visit_docinfo_item(node, 'authors')
+        self.author_in_authors = False  # initialize
+
+    def depart_authors(self, node):
+        self.depart_docinfo_item()
+
+    # use "width" argument instead of "style: 'width'":
+    def visit_colspec(self, node):
+        self.colspecs.append(node)
+        # "stubs" list is an attribute of the tgroup element:
+        node.parent.stubs.append(node.attributes.get('stub'))
+
+    def depart_colspec(self, node):
+        # write out <colgroup> when all colspecs are processed
+        if isinstance(node.next_node(descend=False, siblings=True),
+                      nodes.colspec):
+            return
+        if ('colwidths-auto' in node.parent.parent['classes']
+            or ('colwidths-auto' in self.settings.table_style
+                and 'colwidths-given' not in node.parent.parent['classes'])):
+            return
+        total_width = sum(node['colwidth'] for node in self.colspecs)
+        self.body.append(self.starttag(node, 'colgroup'))
+        for node in self.colspecs:
+            colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
+            self.body.append(self.emptytag(node, 'col',
+                                           width='%i%%' % colwidth))
+        self.body.append('</colgroup>\n')
+
+    # Compact lists:
+    # exclude definition lists and field lists (non-compact by default)
+
+    def is_compactable(self, node):
+        return ('compact' in node['classes']
+                or (self.settings.compact_lists
+                    and 'open' not in node['classes']
+                    and (self.compact_simple
+                         or 'contents' in node.parent['classes']
+                         # TODO: self.in_contents
+                         or self.check_simple_list(node))))
+
+    # citations: Use table for bibliographic references.
+    def visit_citation(self, node):
+        self.body.append(self.starttag(node, 'table',
+                                       CLASS='docutils citation',
+                                       frame="void", rules="none"))
+        self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
+                         '<tbody valign="top">\n'
+                         '<tr>')
+        self.footnote_backrefs(node)
+
+    def depart_citation(self, node):
+        self.body.append('</td></tr>\n'
+                         '</tbody>\n</table>\n')
+
+    def visit_citation_reference(self, node):
+        href = '#'
+        if 'refid' in node:
+            href += node['refid']
+        elif 'refname' in node:
+            href += self.document.nameids[node['refname']]
+        self.body.append(self.starttag(node, 'a', suffix='[', href=href,
+                                       classes=['citation-reference']))
+
+    def depart_citation_reference(self, node):
+        self.body.append(']</a>')
+
+    # insert classifier-delimiter (not required with CSS2)
+    def visit_classifier(self, node):
+        self.body.append(' <span class="classifier-delimiter">:</span> ')
+        self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
+
+    def depart_classifier(self, node):
+        self.body.append('</span>')
+        self.depart_term(node)  # close the <dt> after last classifier
+
+    # ersatz for first/last pseudo-classes
+    def visit_compound(self, node):
+        self.body.append(self.starttag(node, 'div', CLASS='compound'))
+        if len(node) > 1:
+            node[0]['classes'].append('compound-first')
+            node[-1]['classes'].append('compound-last')
+            for child in node[1:-1]:
+                child['classes'].append('compound-middle')
+
+    def depart_compound(self, node):
+        self.body.append('</div>\n')
+
+    # ersatz for first/last pseudo-classes, no special handling of "details"
+    def visit_definition(self, node):
+        self.body.append(self.starttag(node, 'dd', ''))
+        self.set_first_last(node)
+
+    def depart_definition(self, node):
+        self.body.append('</dd>\n')
+
+    # don't add "simple" class value, no special handling of "details"
+    def visit_definition_list(self, node):
+        self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
+
+    def depart_definition_list(self, node):
+        self.body.append('</dl>\n')
+
+    # no special handling of "details"
+    def visit_definition_list_item(self, node):
+        pass
+
+    def depart_definition_list_item(self, node):
+        pass
+
+    # use a table for description lists
+    def visit_description(self, node):
+        self.body.append(self.starttag(node, 'td', ''))
+        self.set_first_last(node)
+
+    def depart_description(self, node):
+        self.body.append('</td>')
+
+    # use table for docinfo
+    def visit_docinfo(self, node):
+        self.context.append(len(self.body))
+        self.body.append(self.starttag(node, 'table',
+                                       CLASS='docinfo',
+                                       frame="void", rules="none"))
+        self.body.append('<col class="docinfo-name" />\n'
+                         '<col class="docinfo-content" />\n'
+                         '<tbody valign="top">\n')
+        self.in_docinfo = True
+
+    def depart_docinfo(self, node):
+        self.body.append('</tbody>\n</table>\n')
+        self.in_docinfo = False
+        start = self.context.pop()
+        self.docinfo = self.body[start:]
+        self.body = []
+
+    def visit_docinfo_item(self, node, name, meta=True):
+        if meta:
+            meta_tag = '<meta name="%s" content="%s" />\n' \
+                       % (name, self.attval(node.astext()))
+            self.meta.append(meta_tag)
+        self.body.append(self.starttag(node, 'tr', ''))
+        self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
+                         % self.language.labels[name])
+        if len(node):
+            if isinstance(node[0], nodes.Element):
+                node[0]['classes'].append('first')
+            if isinstance(node[-1], nodes.Element):
+                node[-1]['classes'].append('last')
+
+    def depart_docinfo_item(self):
+        self.body.append('</td></tr>\n')
+
+    # add newline after opening tag
+    def visit_doctest_block(self, node):
+        self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
+
+    def depart_doctest_block(self, node):
+        self.body.append('\n</pre>\n')
+
+    # insert an NBSP into empty cells, ersatz for first/last
+    def visit_entry(self, node):
+        writers._html_base.HTMLTranslator.visit_entry(self, node)
+        if len(node) == 0:              # empty cell
+            self.body.append('&nbsp;')
+        self.set_first_last(node)
+
+    def depart_entry(self, node):
+        self.body.append(self.context.pop())
+
+    # ersatz for first/last pseudo-classes
+    def visit_enumerated_list(self, node):
+        """
+        The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
+        cannot be emulated in CSS1 (HTML 5 reincludes it).
+        """
+        atts = {}
+        if 'start' in node:
+            atts['start'] = node['start']
+        if 'enumtype' in node:
+            atts['class'] = node['enumtype']
+        # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
+        # single "format" attribute? Use CSS2?
+        old_compact_simple = self.compact_simple
+        self.context.append((self.compact_simple, self.compact_p))
+        self.compact_p = None
+        self.compact_simple = self.is_compactable(node)
+        if self.compact_simple and not old_compact_simple:
+            atts['class'] = (atts.get('class', '') + ' simple').strip()
+        self.body.append(self.starttag(node, 'ol', **atts))
+
+    def depart_enumerated_list(self, node):
+        self.compact_simple, self.compact_p = self.context.pop()
+        self.body.append('</ol>\n')
+
+    # use table for field-list:
+    def visit_field(self, node):
+        self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
+
+    def depart_field(self, node):
+        self.body.append('</tr>\n')
+
+    def visit_field_body(self, node):
+        self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
+        self.set_class_on_child(node, 'first', 0)
+        field = node.parent
+        if (self.compact_field_list
+            or isinstance(field.parent, nodes.docinfo)
+            or field.parent.index(field) == len(field.parent) - 1):
+            # If we are in a compact list, the docinfo, or if this is
+            # the last field of the field list, do not add vertical
+            # space after last element.
+            self.set_class_on_child(node, 'last', -1)
+
+    def depart_field_body(self, node):
+        self.body.append('</td>\n')
+
+    def visit_field_list(self, node):
+        self.context.append((self.compact_field_list, self.compact_p))
+        self.compact_p = None
+        if 'compact' in node['classes']:
+            self.compact_field_list = True
+        elif (self.settings.compact_field_lists
+              and 'open' not in node['classes']):
+            self.compact_field_list = True
+        if self.compact_field_list:
+            for field in node:
+                field_body = field[-1]
+                assert isinstance(field_body, nodes.field_body)
+                children = [n for n in field_body
+                            if not isinstance(n, nodes.Invisible)]
+                if not (len(children) == 0
+                        or len(children) == 1
+                        and isinstance(children[0],
+                                       (nodes.paragraph, nodes.line_block))):
+                    self.compact_field_list = False
+                    break
+        self.body.append(self.starttag(node, 'table', frame='void',
+                                       rules='none',
+                                       CLASS='docutils field-list'))
+        self.body.append('<col class="field-name" />\n'
+                         '<col class="field-body" />\n'
+                         '<tbody valign="top">\n')
+
+    def depart_field_list(self, node):
+        self.body.append('</tbody>\n</table>\n')
+        self.compact_field_list, self.compact_p = self.context.pop()
+
+    def visit_field_name(self, node):
+        atts = {}
+        if self.in_docinfo:
+            atts['class'] = 'docinfo-name'
+        else:
+            atts['class'] = 'field-name'
+        if (self.settings.field_name_limit
+            and len(node.astext()) > self.settings.field_name_limit):
+            atts['colspan'] = 2
+            self.context.append('</tr>\n'
+                                + self.starttag(node.parent, 'tr', '',
+                                                CLASS='field')
+                                + '<td>&nbsp;</td>')
+        else:
+            self.context.append('')
+        self.body.append(self.starttag(node, 'th', '', **atts))
+
+    def depart_field_name(self, node):
+        self.body.append(':</th>')
+        self.body.append(self.context.pop())
+
+    # use table for footnote text
+    def visit_footnote(self, node):
+        self.body.append(self.starttag(node, 'table',
+                                       CLASS='docutils footnote',
+                                       frame="void", rules="none"))
+        self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
+                         '<tbody valign="top">\n'
+                         '<tr>')
+        self.footnote_backrefs(node)
+
+    def footnote_backrefs(self, node):
+        backlinks = []
+        backrefs = node['backrefs']
+        if self.settings.footnote_backlinks and backrefs:
+            if len(backrefs) == 1:
+                self.context.append('')
+                self.context.append('</a>')
+                self.context.append('<a class="fn-backref" href="#%s">'
+                                    % backrefs[0])
+            else:
+                for (i, backref) in enumerate(backrefs, 1):
+                    backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
+                                     % (backref, i))
+                self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
+                self.context += ['', '']
+        else:
+            self.context.append('')
+            self.context += ['', '']
+        # If the node does not only consist of a label.
+        if len(node) > 1:
+            # If there are preceding backlinks, we do not set class
+            # 'first', because we need to retain the top-margin.
+            if not backlinks:
+                node[1]['classes'].append('first')
+            node[-1]['classes'].append('last')
+
+    def depart_footnote(self, node):
+        self.body.append('</td></tr>\n'
+                         '</tbody>\n</table>\n')
+
+    # insert markers in text (pseudo-classes are not supported in CSS1):
+    def visit_footnote_reference(self, node):
+        href = '#' + node['refid']
+        format = self.settings.footnote_references
+        if format == 'brackets':
+            suffix = '['
+            self.context.append(']')
+        else:
+            assert format == 'superscript'
+            suffix = '<sup>'
+            self.context.append('</sup>')
+        self.body.append(self.starttag(node, 'a', suffix,
+                                       CLASS='footnote-reference', href=href))
+
+    def depart_footnote_reference(self, node):
+        self.body.append(self.context.pop() + '</a>')
+
+    # just pass on generated text
+    def visit_generated(self, node):
+        pass
+
+    # Backwards-compatibility implementation:
+    # * Do not use <video>,
+    # * don't embed images,
+    # * use <object> instead of <img> for SVG.
+    #   (SVG not supported by IE up to version 8,
+    #   html4css1 strives for IE6 compatibility.)
+    object_image_types = {'.svg': 'image/svg+xml',
+                          '.swf': 'application/x-shockwave-flash',
+                          '.mp4': 'video/mp4',
+                          '.webm': 'video/webm',
+                          '.ogg': 'video/ogg',
+                          }
+
+    def visit_image(self, node):
+        atts = {}
+        uri = node['uri']
+        ext = os.path.splitext(uri)[1].lower()
+        if ext in self.object_image_types:
+            atts['data'] = uri
+            atts['type'] = self.object_image_types[ext]
+        else:
+            atts['src'] = uri
+            atts['alt'] = node.get('alt', uri)
+        # image size
+        if 'width' in node:
+            atts['width'] = node['width']
+        if 'height' in node:
+            atts['height'] = node['height']
+        if 'scale' in node:
+            if (PIL and ('width' not in node or 'height' not in node)
+                and self.settings.file_insertion_enabled):
+                imagepath = self.uri2imagepath(uri)
+                try:
+                    with PIL.Image.open(imagepath) as img:
+                        img_size = img.size
+                except (OSError, UnicodeEncodeError):
+                    pass  # TODO: warn/info?
+                else:
+                    self.settings.record_dependencies.add(
+                        imagepath.replace('\\', '/'))
+                    if 'width' not in atts:
+                        atts['width'] = '%dpx' % img_size[0]
+                    if 'height' not in atts:
+                        atts['height'] = '%dpx' % img_size[1]
+            for att_name in 'width', 'height':
+                if att_name in atts:
+                    match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
+                    assert match
+                    atts[att_name] = '%s%s' % (
+                        float(match.group(1)) * (float(node['scale']) / 100),
+                        match.group(2))
+        style = []
+        for att_name in 'width', 'height':
+            if att_name in atts:
+                if re.match(r'^[0-9.]+$', atts[att_name]):
+                    # Interpret unitless values as pixels.
+                    atts[att_name] += 'px'
+                style.append('%s: %s;' % (att_name, atts[att_name]))
+                del atts[att_name]
+        if style:
+            atts['style'] = ' '.join(style)
+        # No newlines around inline images.
+        if (not isinstance(node.parent, nodes.TextElement)
+            or isinstance(node.parent, nodes.reference)
+            and not isinstance(node.parent.parent, nodes.TextElement)):
+            suffix = '\n'
+        else:
+            suffix = ''
+        if 'align' in node:
+            atts['class'] = 'align-%s' % node['align']
+        if ext in self.object_image_types:
+            # do NOT use an empty tag: incorrect rendering in browsers
+            self.body.append(self.starttag(node, 'object', '', **atts)
+                             + node.get('alt', uri) + '</object>' + suffix)
+        else:
+            self.body.append(self.emptytag(node, 'img', suffix, **atts))
+
+    def depart_image(self, node):
+        pass
+
+    # use table for footnote text,
+    # context added in footnote_backrefs.
+    def visit_label(self, node):
+        self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
+                                       CLASS='label'))
+
+    def depart_label(self, node):
+        self.body.append(f']{self.context.pop()}</td><td>{self.context.pop()}')
+
+    # ersatz for first/last pseudo-classes
+    def visit_list_item(self, node):
+        self.body.append(self.starttag(node, 'li', ''))
+        if len(node):
+            node[0]['classes'].append('first')
+
+    def depart_list_item(self, node):
+        self.body.append('</li>\n')
+
+    # use <tt> (not supported by HTML5),
+    # cater for limited styling options in CSS1 using hard-coded NBSPs
+    def visit_literal(self, node):
+        # special case: "code" role
+        classes = node['classes']
+        if 'code' in classes:
+            # filter 'code' from class arguments
+            node['classes'] = [cls for cls in classes if cls != 'code']
+            self.body.append(self.starttag(node, 'code', ''))
+            return
+        self.body.append(
+            self.starttag(node, 'tt', '', CLASS='docutils literal'))
+        text = node.astext()
+        for token in self.words_and_spaces.findall(text):
+            if token.strip():
+                # Protect text like "--an-option" and the regular expression
+                # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+                if self.in_word_wrap_point.search(token):
+                    self.body.append('<span class="pre">%s</span>'
+                                     % self.encode(token))
+                else:
+                    self.body.append(self.encode(token))
+            elif token in ('\n', ' '):
+                # Allow breaks at whitespace:
+                self.body.append(token)
+            else:
+                # Protect runs of multiple spaces; the last space can wrap:
+                self.body.append('&nbsp;' * (len(token) - 1) + ' ')
+        self.body.append('</tt>')
+        # Content already processed:
+        raise nodes.SkipNode
+
+    def depart_literal(self, node):
+        # skipped unless literal element is from "code" role:
+        self.body.append('</code>')
+
+    # add newline after wrapper tags, don't use <code> for code
+    def visit_literal_block(self, node):
+        self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
+
+    def depart_literal_block(self, node):
+        self.body.append('\n</pre>\n')
+
+    # use table for option list
+    def visit_option_group(self, node):
+        atts = {}
+        if (self.settings.option_limit
+            and len(node.astext()) > self.settings.option_limit):
+            atts['colspan'] = 2
+            self.context.append('</tr>\n<tr><td>&nbsp;</td>')
+        else:
+            self.context.append('')
+        self.body.append(
+            self.starttag(node, 'td', CLASS='option-group', **atts))
+        self.body.append('<kbd>')
+        self.context.append(0)          # count number of options
+
+    def depart_option_group(self, node):
+        self.context.pop()
+        self.body.append('</kbd></td>\n')
+        self.body.append(self.context.pop())
+
+    def visit_option_list(self, node):
+        self.body.append(
+              self.starttag(node, 'table', CLASS='docutils option-list',
+                            frame="void", rules="none"))
+        self.body.append('<col class="option" />\n'
+                         '<col class="description" />\n'
+                         '<tbody valign="top">\n')
+
+    def depart_option_list(self, node):
+        self.body.append('</tbody>\n</table>\n')
+
+    def visit_option_list_item(self, node):
+        self.body.append(self.starttag(node, 'tr', ''))
+
+    def depart_option_list_item(self, node):
+        self.body.append('</tr>\n')
+
+    # Omit <p> tags to produce visually compact lists (less vertical
+    # whitespace) as CSS styling requires CSS2.
+    def should_be_compact_paragraph(self, node):
+        """
+        Determine if the <p> tags around paragraph ``node`` can be omitted.
+        """
+        if (isinstance(node.parent, nodes.document)
+            or isinstance(node.parent, nodes.compound)):
+            # Never compact paragraphs in document or compound.
+            return False
+        for key, value in node.attlist():
+            if (node.is_not_default(key)
+                and not (key == 'classes'
+                         and value in ([], ['first'],
+                                       ['last'], ['first', 'last']))):
+                # Attribute which needs to survive.
+                return False
+        first = isinstance(node.parent[0], nodes.label)  # skip label
+        for child in node.parent.children[first:]:
+            # only first paragraph can be compact
+            if isinstance(child, nodes.Invisible):
+                continue
+            if child is node:
+                break
+            return False
+        parent_length = len([n for n in node.parent if not isinstance(
+            n, (nodes.Invisible, nodes.label))])
+        if (self.compact_simple
+            or self.compact_field_list
+            or self.compact_p and parent_length == 1):
+            return True
+        return False
+
+    def visit_paragraph(self, node):
+        if self.should_be_compact_paragraph(node):
+            self.context.append('')
+        else:
+            self.body.append(self.starttag(node, 'p', ''))
+            self.context.append('</p>\n')
+
+    def depart_paragraph(self, node):
+        self.body.append(self.context.pop())
+        self.report_messages(node)
+
+    # ersatz for first/last pseudo-classes
+    def visit_sidebar(self, node):
+        self.body.append(
+            self.starttag(node, 'div', CLASS='sidebar'))
+        self.set_first_last(node)
+        self.in_sidebar = True
+
+    def depart_sidebar(self, node):
+        self.body.append('</div>\n')
+        self.in_sidebar = False
+
+    # <sub> not allowed in <pre>
+    def visit_subscript(self, node):
+        if isinstance(node.parent, nodes.literal_block):
+            self.body.append(self.starttag(node, 'span', '',
+                                           CLASS='subscript'))
+        else:
+            self.body.append(self.starttag(node, 'sub', ''))
+
+    def depart_subscript(self, node):
+        if isinstance(node.parent, nodes.literal_block):
+            self.body.append('</span>')
+        else:
+            self.body.append('</sub>')
+
+    # Use <h*> for subtitles (deprecated in HTML 5)
+    def visit_subtitle(self, node):
+        if isinstance(node.parent, nodes.sidebar):
+            self.body.append(self.starttag(node, 'p', '',
+                                           CLASS='sidebar-subtitle'))
+            self.context.append('</p>\n')
+        elif isinstance(node.parent, nodes.document):
+            self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
+            self.context.append('</h2>\n')
+            self.in_document_title = len(self.body)
+        elif isinstance(node.parent, nodes.section):
+            tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
+            self.body.append(
+                self.starttag(node, tag, '', CLASS='section-subtitle')
+                + self.starttag({}, 'span', '', CLASS='section-subtitle'))
+            self.context.append('</span></%s>\n' % tag)
+
+    def depart_subtitle(self, node):
+        self.body.append(self.context.pop())
+        if self.in_document_title:
+            self.subtitle = self.body[self.in_document_title:-1]
+            self.in_document_title = 0
+            self.body_pre_docinfo.extend(self.body)
+            self.html_subtitle.extend(self.body)
+            del self.body[:]
+
+    # <sup> not allowed in <pre> in HTML 4
+    def visit_superscript(self, node):
+        if isinstance(node.parent, nodes.literal_block):
+            self.body.append(self.starttag(node, 'span', '',
+                                           CLASS='superscript'))
+        else:
+            self.body.append(self.starttag(node, 'sup', ''))
+
+    def depart_superscript(self, node):
+        if isinstance(node.parent, nodes.literal_block):
+            self.body.append('</span>')
+        else:
+            self.body.append('</sup>')
+
+    # <tt> element deprecated in HTML 5
+    def visit_system_message(self, node):
+        self.body.append(self.starttag(node, 'div', CLASS='system-message'))
+        self.body.append('<p class="system-message-title">')
+        backref_text = ''
+        if len(node['backrefs']):
+            backrefs = node['backrefs']
+            if len(backrefs) == 1:
+                backref_text = ('; <em><a href="#%s">backlink</a></em>'
+                                % backrefs[0])
+            else:
+                i = 1
+                backlinks = []
+                for backref in backrefs:
+                    backlinks.append('<a href="#%s">%s</a>' % (backref, i))
+                    i += 1
+                backref_text = ('; <em>backlinks: %s</em>'
+                                % ', '.join(backlinks))
+        if node.hasattr('line'):
+            line = ', line %s' % node['line']
+        else:
+            line = ''
+        self.body.append('System Message: %s/%s '
+                         '(<tt class="docutils">%s</tt>%s)%s</p>\n'
+                         % (node['type'], node['level'],
+                            self.encode(node['source']), line, backref_text))
+
+    def depart_system_message(self, node):
+        self.body.append('</div>\n')
+
+    # "hard coded" border setting
+    def visit_table(self, node):
+        self.context.append(self.compact_p)
+        self.compact_p = True
+        atts = {'border': 1}
+        classes = ['docutils', self.settings.table_style]
+        if 'align' in node:
+            classes.append('align-%s' % node['align'])
+        if 'width' in node:
+            atts['style'] = 'width: %s' % node['width']
+        self.body.append(
+            self.starttag(node, 'table', CLASS=' '.join(classes), **atts))
+
+    def depart_table(self, node):
+        self.compact_p = self.context.pop()
+        self.body.append('</table>\n')
+
+    # hard-coded vertical alignment
+    def visit_tbody(self, node):
+        self.body.append(self.starttag(node, 'tbody', valign='top'))
+
+    def depart_tbody(self, node):
+        self.body.append('</tbody>\n')
+
+    # no special handling of "details" in definition list
+    def visit_term(self, node):
+        self.body.append(self.starttag(node, 'dt', '',
+                                       classes=node.parent['classes'],
+                                       ids=node.parent['ids']))
+
+    def depart_term(self, node):
+        # Nest (optional) classifier(s) in the <dt> element
+        if node.next_node(nodes.classifier, descend=False, siblings=True):
+            return  # skip (depart_classifier() calls this function again)
+        self.body.append('</dt>\n')
+
+    # hard-coded vertical alignment
+    def visit_thead(self, node):
+        self.body.append(self.starttag(node, 'thead', valign='bottom'))
+
+    def depart_thead(self, node):
+        self.body.append('</thead>\n')
+
+    # auxiliary method, called by visit_title()
+    # "with-subtitle" class, no ARIA roles
+    def section_title_tags(self, node):
+        classes = []
+        h_level = self.section_level + self.initial_header_level - 1
+        if (len(node.parent) >= 2
+            and isinstance(node.parent[1], nodes.subtitle)):
+            classes.append('with-subtitle')
+        if h_level > 6:
+            classes.append('h%i' % h_level)
+        tagname = 'h%i' % min(h_level, 6)
+        start_tag = self.starttag(node, tagname, '', classes=classes)
+        if node.hasattr('refid'):
+            atts = {}
+            atts['class'] = 'toc-backref'
+            atts['href'] = '#' + node['refid']
+            start_tag += self.starttag({}, 'a', '', **atts)
+            close_tag = '</a></%s>\n' % tagname
+        else:
+            close_tag = '</%s>\n' % tagname
+        return start_tag, close_tag
+
+
+class SimpleListChecker(writers._html_base.SimpleListChecker):
+
+    """
+    Raise `nodes.NodeFound` if non-simple list item is encountered.
+
+    Here "simple" means a list item containing nothing other than a single
+    paragraph, a simple list, or a paragraph followed by a simple list.
+    """
+
+    def visit_list_item(self, node):
+        children = []
+        for child in node.children:
+            if not isinstance(child, nodes.Invisible):
+                children.append(child)
+        if (children and isinstance(children[0], nodes.paragraph)
+            and (isinstance(children[-1], nodes.bullet_list)
+                 or isinstance(children[-1], nodes.enumerated_list))):
+            children.pop()
+        if len(children) <= 1:
+            return
+        else:
+            raise nodes.NodeFound
+
+    # def visit_bullet_list(self, node):
+    #     pass
+
+    # def visit_enumerated_list(self, node):
+    #     pass
+
+    def visit_paragraph(self, node):
+        raise nodes.SkipNode
+
+    def visit_definition_list(self, node):
+        raise nodes.NodeFound
+
+    def visit_docinfo(self, node):
+        raise nodes.NodeFound
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css
new file mode 100644
index 00000000..1d0d3e7c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css
@@ -0,0 +1,350 @@
+/*
+:Author: David Goodger (goodger@python.org)
+:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
+:Copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the HTML output of Docutils.
+Despite the name, some widely supported CSS2 features are used.
+
+See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
+customize this style sheet.
+*/
+
+/* used to remove borders from tables and images */
+.borderless, table.borderless td, table.borderless th {
+  border: 0 }
+
+table.borderless td, table.borderless th {
+  /* Override padding for "table.docutils td" with "! important".
+     The right padding separates the table cells. */
+  padding: 0 0.5em 0 0 ! important }
+
+.first {
+  /* Override more specific margin styles with "! important". */
+  margin-top: 0 ! important }
+
+.last, .with-subtitle {
+  margin-bottom: 0 ! important }
+
+.hidden {
+  display: none }
+
+.subscript {
+  vertical-align: sub;
+  font-size: smaller }
+
+.superscript {
+  vertical-align: super;
+  font-size: smaller }
+
+a.toc-backref {
+  text-decoration: none ;
+  color: black }
+
+blockquote.epigraph {
+  margin: 2em 5em ; }
+
+dl.docutils dd {
+  margin-bottom: 0.5em }
+
+object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
+  overflow: hidden;
+}
+
+/* Uncomment (and remove this text!) to get bold-faced definition list terms
+dl.docutils dt {
+  font-weight: bold }
+*/
+
+div.abstract {
+  margin: 2em 5em }
+
+div.abstract p.topic-title {
+  font-weight: bold ;
+  text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+  margin: 2em ;
+  border: medium outset ;
+  padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+  font-weight: bold ;
+  font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title, .code .error {
+  color: red ;
+  font-weight: bold ;
+  font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+   compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+  margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+  margin-top: 0.5em }
+*/
+
+div.dedication {
+  margin: 2em 5em ;
+  text-align: center ;
+  font-style: italic }
+
+div.dedication p.topic-title {
+  font-weight: bold ;
+  font-style: normal }
+
+div.figure {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+div.footer, div.header {
+  clear: both;
+  font-size: smaller }
+
+div.line-block {
+  display: block ;
+  margin-top: 1em ;
+  margin-bottom: 1em }
+
+div.line-block div.line-block {
+  margin-top: 0 ;
+  margin-bottom: 0 ;
+  margin-left: 1.5em }
+
+div.sidebar {
+  margin: 0 0 0.5em 1em ;
+  border: medium outset ;
+  padding: 1em ;
+  background-color: #ffffee ;
+  width: 40% ;
+  float: right ;
+  clear: right }
+
+div.sidebar p.rubric {
+  font-family: sans-serif ;
+  font-size: medium }
+
+div.system-messages {
+  margin: 5em }
+
+div.system-messages h1 {
+  color: red }
+
+div.system-message {
+  border: medium outset ;
+  padding: 1em }
+
+div.system-message p.system-message-title {
+  color: red ;
+  font-weight: bold }
+
+div.topic {
+  margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+  margin-top: 0.4em }
+
+h1.title {
+  text-align: center }
+
+h2.subtitle {
+  text-align: center }
+
+hr.docutils {
+  width: 75% }
+
+img.align-left, .figure.align-left, object.align-left, table.align-left {
+  clear: left ;
+  float: left ;
+  margin-right: 1em }
+
+img.align-right, .figure.align-right, object.align-right, table.align-right {
+  clear: right ;
+  float: right ;
+  margin-left: 1em }
+
+img.align-center, .figure.align-center, object.align-center {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+table.align-center {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.align-left {
+  text-align: left }
+
+.align-center {
+  clear: both ;
+  text-align: center }
+
+.align-right {
+  text-align: right }
+
+/* reset inner alignment in figures */
+div.align-right {
+  text-align: inherit }
+
+/* div.align-center * { */
+/*   text-align: left } */
+
+.align-top    {
+  vertical-align: top }
+
+.align-middle {
+  vertical-align: middle }
+
+.align-bottom {
+  vertical-align: bottom }
+
+ol.simple, ul.simple {
+  margin-bottom: 1em }
+
+ol.arabic {
+  list-style: decimal }
+
+ol.loweralpha {
+  list-style: lower-alpha }
+
+ol.upperalpha {
+  list-style: upper-alpha }
+
+ol.lowerroman {
+  list-style: lower-roman }
+
+ol.upperroman {
+  list-style: upper-roman }
+
+p.attribution {
+  text-align: right ;
+  margin-left: 50% }
+
+p.caption {
+  font-style: italic }
+
+p.credits {
+  font-style: italic ;
+  font-size: smaller }
+
+p.label {
+  white-space: nowrap }
+
+p.rubric {
+  font-weight: bold ;
+  font-size: larger ;
+  color: maroon ;
+  text-align: center }
+
+p.sidebar-title {
+  font-family: sans-serif ;
+  font-weight: bold ;
+  font-size: larger }
+
+p.sidebar-subtitle {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+p.topic-title {
+  font-weight: bold }
+
+pre.address {
+  margin-bottom: 0 ;
+  margin-top: 0 ;
+  font: inherit }
+
+pre.literal-block, pre.doctest-block, pre.math, pre.code {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+pre.code .ln { color: gray; } /* line numbers */
+pre.code, code { background-color: #eeeeee }
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+span.classifier {
+  font-family: sans-serif ;
+  font-style: oblique }
+
+span.classifier-delimiter {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+span.interpreted {
+  font-family: sans-serif }
+
+span.option {
+  white-space: nowrap }
+
+span.pre {
+  white-space: pre }
+
+span.problematic, pre.problematic {
+  color: red }
+
+span.section-subtitle {
+  /* font-size relative to parent (h1..h6 element) */
+  font-size: 80% }
+
+table.citation {
+  border-left: solid 1px gray;
+  margin-left: 1px }
+
+table.docinfo {
+  margin: 2em 4em }
+
+table.docutils {
+  margin-top: 0.5em ;
+  margin-bottom: 0.5em }
+
+table.footnote {
+  border-left: solid 1px black;
+  margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+  padding-left: 0.5em ;
+  padding-right: 0.5em ;
+  vertical-align: top }
+
+table.docutils th.field-name, table.docinfo th.docinfo-name {
+  font-weight: bold ;
+  text-align: left ;
+  white-space: nowrap ;
+  padding-left: 0 }
+
+/* "booktabs" style (no vertical lines) */
+table.docutils.booktabs {
+  border: 0px;
+  border-top: 2px solid;
+  border-bottom: 2px solid;
+  border-collapse: collapse;
+}
+table.docutils.booktabs * {
+  border: 0px;
+}
+table.docutils.booktabs th {
+  border-bottom: thin solid;
+  text-align: left;
+}
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+  font-size: 100% }
+
+ul.auto-toc {
+  list-style-type: none }
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt
new file mode 100644
index 00000000..2591bce3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt
@@ -0,0 +1,8 @@
+%(head_prefix)s
+%(head)s
+%(stylesheet)s
+%(body_prefix)s
+%(body_pre_docinfo)s
+%(docinfo)s
+%(body)s
+%(body_suffix)s
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py
new file mode 100644
index 00000000..c9bdf66c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py
@@ -0,0 +1,393 @@
+# $Id: __init__.py 9539 2024-02-17 10:36:51Z milde $
+# :Author: Günter Milde <milde@users.sf.net>
+#          Based on the html4css1 writer by David Goodger.
+# :Maintainer: docutils-develop@lists.sourceforge.net
+# :Copyright: © 2005, 2009, 2015 Günter Milde,
+#             portions from html4css1 © David Goodger.
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+# Use "best practice" as recommended by the W3C:
+# http://www.w3.org/2009/cheatsheet/
+
+"""
+Plain HyperText Markup Language document tree Writer.
+
+The output conforms to the `HTML 5` specification.
+
+The cascading style sheet "minimal.css" is required for proper viewing,
+the style sheet "plain.css" improves reading experience.
+"""
+__docformat__ = 'reStructuredText'
+
+from pathlib import Path
+
+from docutils import frontend, nodes
+from docutils.writers import _html_base
+
+
+class Writer(_html_base.Writer):
+
+    supported = ('html5', 'xhtml', 'html')
+    """Formats this writer supports."""
+
+    default_stylesheets = ['minimal.css', 'plain.css']
+    default_stylesheet_dirs = ['.', str(Path(__file__).parent)]
+    default_template = Path(__file__).parent / 'template.txt'
+
+    # use a copy of the parent spec with some modifications
+    settings_spec = frontend.filter_settings_spec(
+        _html_base.Writer.settings_spec,
+        template=(
+            f'Template file. (UTF-8 encoded, default: "{default_template}")',
+            ['--template'],
+            {'default': default_template, 'metavar': '<file>'}),
+        stylesheet_path=(
+            'Comma separated list of stylesheet paths. '
+            'Relative paths are expanded if a matching file is found in '
+            'the --stylesheet-dirs. With --link-stylesheet, '
+            'the path is rewritten relative to the output HTML file. '
+            '(default: "%s")' % ','.join(default_stylesheets),
+            ['--stylesheet-path'],
+            {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+             'validator': frontend.validate_comma_separated_list,
+             'default': default_stylesheets}),
+        stylesheet_dirs=(
+            'Comma-separated list of directories where stylesheets are found. '
+            'Used by --stylesheet-path when expanding relative path '
+            'arguments. (default: "%s")' % ','.join(default_stylesheet_dirs),
+            ['--stylesheet-dirs'],
+            {'metavar': '<dir[,dir,...]>',
+             'validator': frontend.validate_comma_separated_list,
+             'default': default_stylesheet_dirs}),
+        initial_header_level=(
+            'Specify the initial header level. Does not affect document '
+            'title & subtitle (see --no-doc-title). (default: 2 for "<h2>")',
+            ['--initial-header-level'],
+            {'choices': '1 2 3 4 5 6'.split(), 'default': '2',
+             'metavar': '<level>'}),
+        no_xml_declaration=(
+            'Omit the XML declaration (default).',
+            ['--no-xml-declaration'],
+            {'dest': 'xml_declaration', 'action': 'store_false'}),
+    )
+    settings_spec = settings_spec + (
+        'HTML5 Writer Options',
+        '',
+        ((frontend.SUPPRESS_HELP,  # Obsoleted by "--image-loading"
+          ['--embed-images'],
+          {'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         (frontend.SUPPRESS_HELP,  # Obsoleted by "--image-loading"
+          ['--link-images'],
+          {'dest': 'embed_images', 'action': 'store_false'}),
+         ('Suggest at which point images should be loaded: '
+          '"embed", "link" (default), or "lazy".',
+          ['--image-loading'],
+          {'choices': ('embed', 'link', 'lazy'),
+           # 'default': 'link'  # default set in _html_base.py
+           }),
+         ('Append a self-link to section headings.',
+          ['--section-self-link'],
+          {'default': False, 'action': 'store_true'}),
+         ('Do not append a self-link to section headings. (default)',
+          ['--no-section-self-link'],
+          {'dest': 'section_self_link', 'action': 'store_false'}),
+         )
+        )
+
+    config_section = 'html5 writer'
+
+    def __init__(self):
+        self.parts = {}
+        self.translator_class = HTMLTranslator
+
+
+class HTMLTranslator(_html_base.HTMLTranslator):
+    """
+    This writer generates `polyglot markup`: HTML5 that is also valid XML.
+
+    Safe subclassing: when overriding, treat ``visit_*`` and ``depart_*``
+    methods as a unit to prevent breaks due to internal changes. See the
+    docstring of docutils.writers._html_base.HTMLTranslator for details
+    and examples.
+    """
+
+    # self.starttag() arguments for the main document
+    documenttag_args = {'tagname': 'main'}
+
+    # add meta tag to fix rendering in mobile browsers
+    def __init__(self, document):
+        super().__init__(document)
+        self.meta.append('<meta name="viewport" '
+                         'content="width=device-width, initial-scale=1" />\n')
+
+    # <acronym> tag obsolete in HTML5. Use the <abbr> tag instead.
+    def visit_acronym(self, node):
+        # @@@ implementation incomplete ("title" attribute)
+        self.body.append(self.starttag(node, 'abbr', ''))
+
+    def depart_acronym(self, node):
+        self.body.append('</abbr>')
+
+    # no standard meta tag name in HTML5, use separate "author" meta tags
+    # https://www.w3.org/TR/html5/document-metadata.html#standard-metadata-names
+    def visit_authors(self, node):
+        self.visit_docinfo_item(node, 'authors', meta=False)
+        for subnode in node:
+            self.meta.append('<meta name="author" content='
+                             f'"{self.attval(subnode.astext())}" />\n')
+
+    def depart_authors(self, node):
+        self.depart_docinfo_item()
+
+    # use the <figcaption> semantic tag.
+    def visit_caption(self, node):
+        if isinstance(node.parent, nodes.figure):
+            self.body.append('<figcaption>\n')
+        self.body.append(self.starttag(node, 'p', ''))
+
+    def depart_caption(self, node):
+        self.body.append('</p>\n')
+        # <figcaption> is closed in depart_figure(), as legend may follow.
+
+    # use HTML block-level tags if matching class value found
+    supported_block_tags = {'ins', 'del'}
+
+    def visit_container(self, node):
+        # If there is exactly one of the "supported block tags" in
+        # the list of class values, use it as tag name:
+        classes = node['classes']
+        tags = [cls for cls in classes
+                if cls in self.supported_block_tags]
+        if len(tags) == 1:
+            node.html5tagname = tags[0]
+            classes.remove(tags[0])
+        else:
+            node.html5tagname = 'div'
+        self.body.append(self.starttag(node, node.html5tagname,
+                                       CLASS='docutils container'))
+
+    def depart_container(self, node):
+        self.body.append(f'</{node.html5tagname}>\n')
+        del node.html5tagname
+
+    # no standard meta tag name in HTML5, use dcterms.rights
+    # see https://wiki.whatwg.org/wiki/MetaExtensions
+    def visit_copyright(self, node):
+        self.visit_docinfo_item(node, 'copyright', meta=False)
+        self.meta.append('<meta name="dcterms.rights" '
+                         f'content="{self.attval(node.astext())}" />\n')
+
+    def depart_copyright(self, node):
+        self.depart_docinfo_item()
+
+    # no standard meta tag name in HTML5, use dcterms.date
+    def visit_date(self, node):
+        self.visit_docinfo_item(node, 'date', meta=False)
+        self.meta.append('<meta name="dcterms.date" '
+                         f'content="{self.attval(node.astext())}" />\n')
+
+    def depart_date(self, node):
+        self.depart_docinfo_item()
+
+    # use new HTML5 <figure> and <figcaption> elements
+    def visit_figure(self, node):
+        atts = {}
+        if node.get('width'):
+            atts['style'] = f"width: {node['width']}"
+        if node.get('align'):
+            atts['class'] = f"align-{node['align']}"
+        self.body.append(self.starttag(node, 'figure', **atts))
+
+    def depart_figure(self, node):
+        if len(node) > 1:
+            self.body.append('</figcaption>\n')
+        self.body.append('</figure>\n')
+
+    # use HTML5 <footer> element
+    def visit_footer(self, node):
+        self.context.append(len(self.body))
+
+    def depart_footer(self, node):
+        start = self.context.pop()
+        footer = [self.starttag(node, 'footer')]
+        footer.extend(self.body[start:])
+        footer.append('</footer>\n')
+        self.footer.extend(footer)
+        self.body_suffix[:0] = footer
+        del self.body[start:]
+
+    # use HTML5 <header> element
+    def visit_header(self, node):
+        self.context.append(len(self.body))
+
+    def depart_header(self, node):
+        start = self.context.pop()
+        header = [self.starttag(node, 'header')]
+        header.extend(self.body[start:])
+        header.append('</header>\n')
+        self.body_prefix.extend(header)
+        self.header.extend(header)
+        del self.body[start:]
+
+    # use HTML text-level tags if matching class value found
+    supported_inline_tags = {'code', 'kbd', 'dfn', 'samp', 'var',
+                             'bdi', 'del', 'ins', 'mark', 'small',
+                             'b', 'i', 'q', 's', 'u'}
+
+    # Use `supported_inline_tags` if found in class values
+    def visit_inline(self, node):
+        classes = node['classes']
+        node.html5tagname = 'span'
+        # Special handling for "code" directive content
+        if (isinstance(node.parent, nodes.literal_block)
+            and 'code' in node.parent.get('classes')
+            or isinstance(node.parent, nodes.literal)
+            and getattr(node.parent, 'html5tagname', None) == 'code'):
+            if classes == ['ln']:
+                # line numbers are not part of the "fragment of computer code"
+                if self.body[-1] == '<code>':
+                    del self.body[-1]
+                else:
+                    self.body.append('</code>')
+                node.html5tagname = 'small'
+        else:
+            tags = [cls for cls in self.supported_inline_tags
+                    if cls in classes]
+            if len(tags):
+                node.html5tagname = tags[0]
+                classes.remove(node.html5tagname)
+        self.body.append(self.starttag(node, node.html5tagname, ''))
+
+    def depart_inline(self, node):
+        self.body.append(f'</{node.html5tagname}>')
+        if (node.html5tagname == 'small' and node.get('classes') == ['ln']
+            and isinstance(node.parent, nodes.literal_block)):
+            self.body.append(f'<code data-lineno="{node.astext()}">')
+        del node.html5tagname
+
+    # place inside HTML5 <figcaption> element (together with caption)
+    def visit_legend(self, node):
+        if not isinstance(node.parent[1], nodes.caption):
+            self.body.append('<figcaption>\n')
+        self.body.append(self.starttag(node, 'div', CLASS='legend'))
+
+    def depart_legend(self, node):
+        self.body.append('</div>\n')
+        # <figcaption> closed in visit_figure()
+
+    # use HTML5 text-level tags if matching class value found
+    def visit_literal(self, node):
+        classes = node['classes']
+        html5tagname = 'span'
+        tags = [cls for cls in self.supported_inline_tags
+                if cls in classes]
+        if len(tags):
+            html5tagname = tags[0]
+            classes.remove(html5tagname)
+        if html5tagname == 'code':
+            node.html5tagname = html5tagname
+            self.body.append(self.starttag(node, html5tagname, ''))
+            return
+        self.body.append(
+            self.starttag(node, html5tagname, '', CLASS='docutils literal'))
+        text = node.astext()
+        # remove hard line breaks (except if in a parsed-literal block)
+        if not isinstance(node.parent, nodes.literal_block):
+            text = text.replace('\n', ' ')
+        # Protect text like ``--an-option`` and the regular expression
+        # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+        for token in self.words_and_spaces.findall(text):
+            if token.strip() and self.in_word_wrap_point.search(token):
+                self.body.append(
+                    f'<span class="pre">{self.encode(token)}</span>')
+            else:
+                self.body.append(self.encode(token))
+        self.body.append(f'</{html5tagname}>')
+        # Content already processed:
+        raise nodes.SkipNode
+
+    def depart_literal(self, node):
+        # skipped unless literal element is from "code" role:
+        self.depart_inline(node)
+
+    # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
+    # HTML5/polyglot recommends using both
+    def visit_meta(self, node):
+        if node.hasattr('lang'):
+            node['xml:lang'] = node['lang']
+        self.meta.append(self.emptytag(node, 'meta',
+                                       **node.non_default_attributes()))
+
+    def depart_meta(self, node):
+        pass
+
+    # no standard meta tag name in HTML5
+    def visit_organization(self, node):
+        self.visit_docinfo_item(node, 'organization', meta=False)
+
+    def depart_organization(self, node):
+        self.depart_docinfo_item()
+
+    # use the new HTML5 element <section>
+    def visit_section(self, node):
+        self.section_level += 1
+        self.body.append(
+            self.starttag(node, 'section'))
+
+    def depart_section(self, node):
+        self.section_level -= 1
+        self.body.append('</section>\n')
+
+    # use the new HTML5 element <aside>
+    def visit_sidebar(self, node):
+        self.body.append(
+            self.starttag(node, 'aside', CLASS='sidebar'))
+        self.in_sidebar = True
+
+    def depart_sidebar(self, node):
+        self.body.append('</aside>\n')
+        self.in_sidebar = False
+
+    # Use new HTML5 element <aside> or <nav>
+    # Add class value to <body>, if there is a ToC in the document
+    # (see responsive.css how this is used for a navigation sidebar).
+    def visit_topic(self, node):
+        atts = {'classes': ['topic']}
+        if 'contents' in node['classes']:
+            node.html5tagname = 'nav'
+            del atts['classes']
+            if isinstance(node.parent, nodes.document):
+                atts['role'] = 'doc-toc'
+                self.body_prefix[0] = '</head>\n<body class="with-toc">\n'
+        elif 'abstract' in node['classes']:
+            node.html5tagname = 'div'
+            atts['role'] = 'doc-abstract'
+        elif 'dedication' in node['classes']:
+            node.html5tagname = 'div'
+            atts['role'] = 'doc-dedication'
+        else:
+            node.html5tagname = 'aside'
+        self.body.append(self.starttag(node, node.html5tagname, **atts))
+
+    def depart_topic(self, node):
+        self.body.append(f'</{node.html5tagname}>\n')
+        del node.html5tagname
+
+    # append self-link
+    def section_title_tags(self, node):
+        start_tag, close_tag = super().section_title_tags(node)
+        ids = node.parent['ids']
+        if (ids and getattr(self.settings, 'section_self_link', None)
+            and not isinstance(node.parent, nodes.document)):
+            self_link = ('<a class="self-link" title="link to this section"'
+                         f' href="#{ids[0]}"></a>')
+            close_tag = close_tag.replace('</h', self_link + '</h')
+        return start_tag, close_tag
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css
new file mode 100644
index 00000000..75908529
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css
@@ -0,0 +1,26 @@
+/* italic-field-name.css: */
+/* Alternative style for Docutils field-lists */
+
+/* :Copyright: © 2023 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_,      */
+/*    in short:                                                            */
+/*                                                                         */
+/*    Copying and distribution of this file, with or without modification, */
+/*    are permitted in any medium without royalty provided the copyright   */
+/*    notice and this notice are preserved.                                */
+/*                                                                         */
+/*    This file is offered as-is, without any warranty.                    */
+/*                                                                         */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause     */
+
+/* In many contexts, a **bold** field name is too heavy styling. */
+/* Use *italic* instead:: */
+
+dl.field-list > dt {
+  font-weight: normal;
+  font-style: italic;
+}
+dl.field-list > dt > .colon {
+  font-style: normal;
+  padding-left: 0.05ex;
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css
new file mode 100644
index 00000000..eb1ba72e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css
@@ -0,0 +1,332 @@
+/*
+*   math2html: convert LaTeX equations to HTML output.
+*
+*   Copyright (C) 2009,2010 Alex Fernández
+*                 2021      Günter Milde
+*
+*   Released under the terms of the `2-Clause BSD license'_, in short:
+*   Copying and distribution of this file, with or without modification,
+*   are permitted in any medium without royalty provided the copyright
+*   notice and this notice are preserved.
+*   This file is offered as-is, without any warranty.
+*
+* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
+*
+*   Based on eLyXer: convert LyX source files to HTML output.
+*   http://elyxer.nongnu.org/
+*
+*
+* CSS file for LaTeX formulas.
+*
+* References: http://www.zipcon.net/~swhite/docs/math/math.html
+*             http://www.cs.tut.fi/~jkorpela/math/
+*/
+
+/* Formulas */
+.formula {
+	text-align: center;
+	margin: 1.2em 0;
+	line-height: 1.4;
+}
+span.formula {
+	white-space: nowrap;
+}
+div.formula {
+	padding: 0.5ex;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+/* Basic features */
+a.eqnumber {
+	display: inline-block;
+	float: right;
+	clear: right;
+	font-weight: bold;
+}
+span.unknown {
+	color: #800000;
+}
+span.ignored, span.arraydef {
+	display: none;
+}
+.phantom {
+	visibility: hidden;
+}
+.formula i {
+	letter-spacing: 0.1ex;
+}
+
+/* Alignment */
+.align-l {
+	text-align: left;
+}
+.align-r {
+	text-align: right;
+}
+.align-c {
+	text-align: center;
+}
+
+/* Structures */
+span.hspace {
+	display: inline-block;
+}
+span.overline, span.bar {
+	text-decoration: overline;
+}
+.fraction, .fullfraction, .textfraction {
+	display: inline-block;
+	vertical-align: middle;
+	text-align: center;
+}
+span.formula .fraction,
+.textfraction,
+span.smallmatrix {
+	font-size: 80%;
+	line-height: 1;
+}
+span.numerator {
+	display: block;
+	line-height: 1;
+}
+span.denominator {
+	display: block;
+	line-height: 1;
+	padding: 0ex;
+	border-top: thin solid;
+}
+.formula sub, .formula sup {
+	font-size: 80%;
+}
+sup.numerator, sup.unit {
+	vertical-align: 80%;
+}
+sub.denominator, sub.unit {
+	vertical-align: -20%;
+}
+span.smallsymbol {
+	font-size: 75%;
+	line-height: 75%;
+}
+span.boldsymbol {
+	font-weight: bold;
+}
+span.sqrt {
+	display: inline-block;
+	vertical-align: middle;
+	padding: 0.1ex;
+}
+sup.root {
+	position: relative;
+	left: 1.4ex;
+}
+span.radical {
+	display: inline-block;
+	padding: 0ex;
+	/* font-size: 160%; for DejaVu, not required with STIX */
+	line-height: 100%;
+	vertical-align: top;
+	vertical-align: middle;
+}
+
+span.root {
+	display: inline-block;
+	border-top: thin solid;
+	padding: 0ex;
+	vertical-align: middle;
+}
+div.formula .bigoperator,
+.displaystyle .bigoperator,
+.displaystyle .bigoperator {
+	line-height: 120%;
+	font-size: 140%;
+	padding-right: 0.2ex;
+}
+span.fraction .bigoperator,
+span.scriptstyle .bigoperator {
+	line-height: inherit;
+	font-size: inherit;
+	padding-right: 0;
+}
+span.bigdelimiter {
+	display: inline-block;
+}
+span.bigdelimiter.size1 {
+	transform: scale(1, 1.2);
+	line-height: 1.2;
+}
+span.bigdelimiter.size2 {
+	transform: scale(1, 1.62);
+	line-height: 1.62%;
+
+}
+span.bigdelimiter.size3 {
+	transform: scale(1, 2.05);
+	line-height: 2.05%;
+}
+span.bigdelimiter.size4 {
+	transform: scale(1, 2.47);
+	line-height: 2.47%;
+}
+/* vertically stacked sub and superscript */
+span.scripts {
+	display: inline-table;
+	vertical-align: middle;
+	padding-right: 0.2ex;
+}
+.script {
+	display: table-row;
+	text-align: left;
+	line-height: 150%;
+}
+span.limits {
+	display: inline-table;
+	vertical-align: middle;
+}
+.limit {
+	display: table-row;
+	line-height: 99%;
+}
+sup.limit, sub.limit {
+	line-height: 100%;
+}
+span.embellished,
+span.embellished > .base {
+	display: inline-block;
+}
+span.embellished > sup,
+span.embellished > sub {
+	display: inline-block;
+	font-size: 100%;
+	position: relative;
+	bottom: 0.3em;
+	width: 0px;
+}
+span.embellished > sub {
+	top: 0.4em;
+}
+
+/* Environments */
+span.array, span.bracketcases, span.binomial, span.environment {
+	display: inline-table;
+	text-align: center;
+	vertical-align: middle;
+}
+span.arrayrow, span.binomrow {
+	display: table-row;
+	padding: 0;
+	border: 0;
+}
+span.arraycell, span.bracket, span.case, span.binomcell, span.environmentcell {
+	display: table-cell;
+	padding: 0ex 0.2ex;
+	line-height: 1; /* 99%; */
+	border: 0ex;
+}
+.environment.align > .arrayrow > .arraycell.align-l {
+	padding-right: 2em;
+}
+
+/* Inline binomials */
+span.binom {
+	display: inline-block;
+	vertical-align: middle;
+	text-align: center;
+	font-size: 80%;
+}
+span.binomstack {
+	display: block;
+	padding: 0em;
+}
+
+/* Over- and underbraces */
+span.overbrace {
+	border-top: 2pt solid;
+}
+span.underbrace {
+	border-bottom: 2pt solid;
+}
+
+/* Stackrel */
+span.stackrel {
+	display: inline-block;
+	text-align: center;
+}
+span.upstackrel {
+	display: block;
+	padding: 0em;
+	font-size: 80%;
+	line-height: 64%;
+	position: relative;
+	top: 0.15em;
+
+}
+span.downstackrel {
+	display: block;
+	vertical-align: bottom;
+	padding: 0em;
+}
+
+/* Fonts */
+.formula {
+	font-family: STIX, "DejaVu Serif", "DejaVu Math TeX Gyre", serif;
+}
+span.radical,   /* ensure correct size of square-root sign */
+span.integral { /* upright integral signs for better alignment of indices */
+	font-family: "STIXIntegralsUp", STIX;
+	/* font-size: 115%; match apparent size with DejaVu */
+}
+span.bracket {
+  /* some "STIX" and "DejaVu Math TeX Gyre" bracket pieces don't fit */
+	font-family: "DejaVu Serif", serif;
+}
+span.mathsf, span.textsf {
+	font-family: sans-serif;
+}
+span.mathrm, span.textrm {
+	font-family: STIX, "DejaVu Serif", "DejaVu Math TeX Gyre", serif;
+}
+span.mathtt, span.texttt {
+	font-family: monospace;
+}
+span.text, span.textnormal,
+span.mathsf, span.mathtt, span.mathrm {
+	font-style: normal;
+}
+span.fraktur {
+	font-family: "Lucida Blackletter", eufm10, blackletter;
+}
+span.blackboard {
+	font-family: Blackboard, msbm10, serif;
+}
+span.scriptfont {
+	font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
+	font-style: italic;
+}
+span.mathscr {
+  font-family: MathJax_Script, rsfs10,  cursive;
+  font-style: italic;
+}
+span.textsc {
+	font-variant: small-caps;
+}
+span.textsl {
+	font-style: oblique;
+}
+
+/* Colors */
+span.colorbox {
+	display: inline-block;
+	padding: 5px;
+}
+span.fbox {
+	display: inline-block;
+	border: thin solid black;
+	padding: 2px;
+}
+span.boxed, span.framebox {
+	display: inline-block;
+	border: thin solid black;
+	padding: 5px;
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css
new file mode 100644
index 00000000..66f0658d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css
@@ -0,0 +1,293 @@
+/* Minimal style sheet for the HTML output of Docutils.                    */
+/*                                                                         */
+/* :Author: Günter Milde, based on html4css1.css by David Goodger          */
+/* :Id: $Id: minimal.css 9545 2024-02-17 10:37:56Z milde $                                                               */
+/* :Copyright: © 2015, 2021 Günter Milde.                                  */
+/* :License: Released under the terms of the `2-Clause BSD license`_,      */
+/*    in short:                                                            */
+/*                                                                         */
+/*    Copying and distribution of this file, with or without modification, */
+/*    are permitted in any medium without royalty provided the copyright   */
+/*    notice and this notice are preserved.                                */
+/*                                                                         */
+/*    This file is offered as-is, without any warranty.                    */
+/*                                                                         */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause     */
+
+/* This CSS3 stylesheet defines rules for Docutils elements without        */
+/* HTML equivalent. It is required to make the document semantics visible. */
+/*                                                                         */
+/* .. _validates: http://jigsaw.w3.org/css-validator/validator$link        */
+
+/* titles */
+p.topic-title,
+p.admonition-title,
+p.system-message-title {
+  font-weight: bold;
+}
+p.sidebar-title,
+p.rubric {
+  font-weight: bold;
+  font-size: larger;
+}
+p.rubric {
+  color: maroon;
+}
+p.subtitle,
+p.section-subtitle,
+p.sidebar-subtitle {
+  font-weight: bold;
+  margin-top: -0.5em;
+}
+h1 + p.subtitle {
+  font-size: 1.6em;
+}
+a.toc-backref {
+  color: inherit;
+  text-decoration: none;
+}
+
+/* Warnings, Errors */
+.system-messages h2,
+.system-message-title,
+pre.problematic,
+span.problematic {
+  color: red;
+}
+
+/* Inline Literals */
+.docutils.literal {
+  font-family: monospace;
+  white-space: pre-wrap;
+}
+/* do not wrap at hyphens and similar: */
+.literal > span.pre { white-space: nowrap; }
+
+/* keep line-breaks (\n) visible */
+.pre-wrap { white-space: pre-wrap; }
+
+/* Lists */
+
+/* compact and simple lists: no margin between items */
+.simple  li, .simple  ul, .simple  ol,
+.compact li, .compact ul, .compact ol,
+.simple  > li p, dl.simple  > dd,
+.compact > li p, dl.compact > dd {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+/* Nested Paragraphs */
+p:first-child { margin-top: 0; }
+p:last-child { margin-bottom: 0; }
+details > p:last-child { margin-bottom: 1em; }
+
+/* Table of Contents */
+.contents ul.auto-toc { /* section numbers present */
+  list-style-type: none;
+}
+
+/* Enumerated Lists */
+ol.arabic     { list-style: decimal }
+ol.loweralpha { list-style: lower-alpha }
+ol.upperalpha { list-style: upper-alpha }
+ol.lowerroman { list-style: lower-roman }
+ol.upperroman { list-style: upper-roman }
+
+/* Definition Lists and Derivatives */
+dt .classifier { font-style: italic }
+dt .classifier:before {
+  font-style: normal;
+  margin: 0.5em;
+  content: ":";
+}
+/* Field Lists and similar */
+/* bold field name, content starts on the same line */
+dl.field-list,
+dl.option-list,
+dl.docinfo {
+  display: flow-root;
+}
+dl.field-list > dt,
+dl.option-list > dt,
+dl.docinfo > dt {
+  font-weight: bold;
+  clear: left;
+  float: left;
+  margin: 0;
+  padding: 0;
+  padding-right: 0.25em;
+}
+/* Offset for field content (corresponds to the --field-name-limit option) */
+dl.field-list > dd,
+dl.option-list > dd,
+dl.docinfo > dd {
+  margin-left:  9em; /* ca. 14 chars in the test examples, fit all Docinfo fields */
+}
+/* start nested lists on new line */
+dd > dl:first-child,
+dd > ul:first-child,
+dd > ol:first-child {
+  clear: left;
+}
+/* start field-body on a new line after long field names */
+dl.field-list > dd > *:first-child,
+dl.option-list > dd > *:first-child
+{
+  display: inline-block;
+  width: 100%;
+  margin: 0;
+}
+
+/* Bibliographic Fields (docinfo) */
+dl.docinfo pre.address {
+  font: inherit;
+  margin: 0.5em 0;
+}
+dl.docinfo > dd.authors > p { margin: 0; }
+
+/* Option Lists */
+dl.option-list > dt { font-weight: normal; }
+span.option { white-space: nowrap; }
+
+/* Footnotes and Citations  */
+
+.footnote, .citation { margin: 1em 0; } /* default paragraph skip (Firefox) */
+/* hanging indent */
+.citation { padding-left: 2em; }
+.footnote { padding-left: 1.7em; }
+.footnote.superscript { padding-left: 1.0em; }
+.citation > .label { margin-left: -2em; }
+.footnote > .label { margin-left: -1.7em; }
+.footnote.superscript > .label { margin-left: -1.0em; }
+
+.footnote > .label + *,
+.citation > .label + * {
+  display: inline-block;
+  margin-top: 0;
+  vertical-align: top;
+}
+.footnote > .backrefs + *,
+.citation > .backrefs + * {
+  margin-top: 0;
+}
+.footnote > .label + p, .footnote > .backrefs + p,
+.citation > .label + p, .citation > .backrefs + p {
+  display: inline;
+  vertical-align: inherit;
+}
+
+.backrefs { user-select: none; }
+.backrefs > a { font-style: italic; }
+
+/* superscript footnotes */
+a[role="doc-noteref"].superscript,
+.footnote.superscript > .label,
+.footnote.superscript > .backrefs {
+  vertical-align: super;
+  font-size: smaller;
+  line-height: 1;
+}
+a[role="doc-noteref"].superscript > .fn-bracket,
+.footnote.superscript > .label > .fn-bracket {
+  /* hide brackets in display but leave for copy/paste */
+  display: inline-block;
+  width: 0;
+  overflow: hidden;
+}
+[role="doc-noteref"].superscript + [role="doc-noteref"].superscript {
+  padding-left: 0.15em; /* separate consecutive footnote references */
+  /* TODO: unfortunately, "+" also selects with text between the references. */
+}
+
+/* Alignment */
+.align-left   {
+  text-align: left;
+  margin-right: auto;
+}
+.align-center {
+  text-align: center;
+  margin-left: auto;
+  margin-right: auto;
+}
+.align-right  {
+  text-align: right;
+  margin-left: auto;
+}
+.align-top    { vertical-align: top; }
+.align-middle { vertical-align: middle; }
+.align-bottom { vertical-align: bottom; }
+
+/* reset inner alignment in figures and tables */
+figure.align-left, figure.align-right,
+table.align-left, table.align-center, table.align-right {
+  text-align: inherit;
+}
+
+/* Text Blocks */
+.topic { margin: 1em 2em; }
+.sidebar,
+.admonition,
+.system-message {
+  margin: 1em 2em;
+  border: thin solid;
+  padding: 0.5em 1em;
+}
+div.line-block { display: block; }
+div.line-block div.line-block, pre { margin-left: 2em; }
+
+/* Code line numbers: dropped when copying text from the page */
+pre.code .ln { display: none; }
+pre.code code:before {
+  content: attr(data-lineno); /* …, none) fallback not supported by any browser */
+  color: gray;
+}
+
+/* Tables */
+table {
+  border-collapse: collapse;
+}
+td, th {
+  border: thin solid silver;
+  padding: 0 1ex;
+}
+.borderless td, .borderless th {
+  border: 0;
+  padding: 0;
+  padding-right: 0.5em /* separate table cells */
+}
+
+table > caption, figcaption {
+  text-align: left;
+  margin-top: 0.2em;
+  margin-bottom: 0.2em;
+}
+table.captionbelow {
+  caption-side: bottom;
+}
+
+/* MathML (see "math.css" for --math-output=HTML) */
+math .boldsymbol { font-weight: bold; }
+math.boxed, math .boxed {padding: 0.25em; border: thin solid; }
+/* style table similar to AMS "align" or "aligned" environment: */
+mtable.cases > mtr > mtd { text-align: left; }
+mtable.ams-align > mtr > mtd { padding-left: 0; padding-right: 0; }
+mtable.ams-align > mtr > mtd:nth-child(2n) { text-align: left; }
+mtable.ams-align > mtr > mtd:nth-child(2n+1) { text-align: right; }
+mtable.ams-align > mtr > mtd:nth-child(2n+3) { padding-left: 2em; }
+.mathscr mi, mi.mathscr {
+  font-family: STIX, XITSMathJax_Script, rsfs10,
+               "Asana Math", Garamond, cursive;
+}
+
+/* Document Header and Footer */
+header { border-bottom: 1px solid black; }
+footer { border-top: 1px solid black; }
+
+/* Images are block-level by default in Docutils */
+/* New HTML5 block elements: set display for older browsers */
+img, svg, header, footer, main, aside, nav, section, figure, video, details {
+  display: block;
+}
+svg { width: auto; height: auto; }  /* enable scaling of SVG images */
+/* inline images */
+p img, p svg, p video { display: inline; }
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css
new file mode 100644
index 00000000..f0f089bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css
@@ -0,0 +1,307 @@
+/* CSS31_ style sheet for the output of Docutils HTML writers.             */
+/* Rules for easy reading and pre-defined style variants.                  */
+/*                                                                         */
+/* :Author: Günter Milde, based on html4css1.css by David Goodger          */
+/* :Id: $Id: plain.css 9615 2024-04-06 13:28:15Z milde $                                                               */
+/* :Copyright: © 2015 Günter Milde.                                        */
+/* :License: Released under the terms of the `2-Clause BSD license`_,      */
+/*    in short:                                                            */
+/*                                                                         */
+/*    Copying and distribution of this file, with or without modification, */
+/*    are permitted in any medium without royalty provided the copyright   */
+/*    notice and this notice are preserved.                                */
+/*                                                                         */
+/*    This file is offered as-is, without any warranty.                    */
+/*                                                                         */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause     */
+/* .. _CSS3: https://www.w3.org/Style/CSS/                                 */
+
+
+/* Document Structure */
+/* ****************** */
+
+/* "page layout" */
+body {
+  margin: 0;
+  background-color: #dbdbdb;
+  --field-indent: 9em; /* default indent of fields in field lists */
+}
+main, footer, header {
+  line-height:1.6;
+  /* avoid long lines --> better reading */
+  /* optimum is 45…75 characters/line <http://webtypography.net/2.1.2> */
+  /* OTOH: lines should not be too short because of missing hyphenation, */
+  max-width: 50rem;
+  padding: 1px 2%; /* 1px on top avoids grey bar above title (mozilla) */
+  margin: auto;
+}
+main {
+  counter-reset: table figure;
+  background-color: white;
+}
+footer, header {
+  font-size: smaller;
+  padding: 0.5em 2%;
+  border: none;
+}
+
+/* Table of Contents */
+ul.auto-toc > li > p {
+  padding-left: 1em;
+  text-indent: -1em;
+}
+nav.contents ul {
+  padding-left: 1em;
+}
+main > nav.contents ul ul ul ul:not(.auto-toc) {
+  list-style-type: '\2B29\ ';
+}
+main > nav.contents ul ul ul ul ul:not(.auto-toc) {
+  list-style-type: '\2B1D\ ';
+}
+
+/* Transitions */
+hr.docutils {
+  width: 80%;
+  margin-top: 1em;
+  margin-bottom: 1em;
+  clear: both;
+}
+
+/* Paragraphs */
+
+/* vertical space (parskip) */
+p, ol, ul, dl, li,
+.footnote, .citation,
+div > math,
+table {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+}
+
+h1, h2, h3, h4, h5, h6,
+dd, details > p:last-child {
+  margin-bottom: 0.5em;
+}
+
+/* Lists */
+/* ===== */
+
+/* Definition Lists */
+/* Indent lists nested in definition lists */
+dd > ul:only-child, dd > ol:only-child { padding-left: 1em; }
+
+/* Description Lists */
+/* styled like in most dictionaries, encyclopedias etc. */
+dl.description {
+  display: flow-root;
+}
+dl.description > dt {
+  font-weight: bold;
+  clear: left;
+  float: left;
+  margin: 0;
+  padding: 0;
+  padding-right: 0.3em;
+}
+dl.description > dd:after {
+  display: table;
+  content: "";
+  clear: left; /* clearfix for empty descriptions */
+}
+
+/* Field Lists */
+
+dl.field-list > dd,
+dl.docinfo > dd {
+  margin-left: var(--field-indent); /* adapted in media queries or HTML */
+}
+
+/* example for custom field-name width */
+dl.field-list.narrow > dd {
+  --field-indent: 5em;
+}
+/* run-in: start field-body on same line after long field names */
+dl.field-list.run-in > dd p {
+  display: block;
+}
+
+/* Bibliographic Fields */
+
+/* generally, bibliographic fields use dl.docinfo */
+/* but dedication and abstract are placed into divs */
+div.abstract p.topic-title {
+  text-align: center;
+}
+div.dedication {
+  margin: 2em 5em;
+  text-align: center;
+  font-style: italic;
+}
+div.dedication p.topic-title {
+  font-style: normal;
+}
+
+/* disclosures */
+details { padding-left: 1em; }
+summary { margin-left: -1em; }
+
+/* Text Blocks */
+/* =========== */
+
+/* Literal Blocks */
+pre.literal-block, pre.doctest-block,
+pre.math, pre.code {
+  font-family: monospace;
+}
+
+/* Block Quotes and Topics */
+bockquote { margin: 1em 2em; }
+blockquote p.attribution,
+.topic p.attribution {
+  text-align: right;
+  margin-left: 20%;
+}
+
+/* Tables */
+/* ====== */
+
+/* th { vertical-align: bottom; } */
+
+table tr { text-align: left; }
+
+/* "booktabs" style (no vertical lines) */
+table.booktabs {
+  border: 0;
+  border-top: 2px solid;
+  border-bottom: 2px solid;
+  border-collapse: collapse;
+}
+table.booktabs * {
+  border: 0;
+}
+table.booktabs th {
+  border-bottom: thin solid;
+}
+
+/* numbered tables (counter defined in div.document) */
+table.numbered > caption:before {
+  counter-increment: table;
+  content: "Table " counter(table) ": ";
+  font-weight: bold;
+}
+
+/* Explicit Markup Blocks */
+/* ====================== */
+
+/* Footnotes and Citations */
+/* ----------------------- */
+
+/* line on the left */
+.footnote-list {
+  border-left: solid thin;
+  padding-left: 0.25em;
+}
+
+/* Directives */
+/* ---------- */
+
+/* Body Elements */
+/* ~~~~~~~~~~~~~ */
+
+/* Images and Figures */
+
+/* let content flow to the side of aligned images and figures */
+figure.align-left,
+img.align-left,
+svg.align-left,
+video.align-left,
+div.align-left,
+object.align-left {
+  clear: left;
+  float: left;
+  margin-right: 1em;
+}
+figure.align-right,
+img.align-right,
+svg.align-right,
+video.align-right,
+div.align-right,
+object.align-right {
+  clear: right;
+  float: right;
+  margin-left: 1em;
+}
+/* Stop floating sidebars, images and figures */
+h1, h2, h3, h4, footer, header { clear: both; }
+
+/* Numbered figures */
+figure.numbered > figcaption > p:before {
+  counter-increment: figure;
+  content: "Figure " counter(figure) ": ";
+  font-weight: bold;
+}
+
+/* Admonitions and System Messages */
+.caution p.admonition-title,
+.attention p.admonition-title,
+.danger p.admonition-title,
+.error p.admonition-title,
+.warning p.admonition-title,
+div.error {
+  color: red;
+}
+
+/* Sidebar */
+/* Move right. In a layout with fixed margins, */
+/* it can be moved into the margin.            */
+aside.sidebar {
+  width: 30%;
+  max-width: 26em;
+  float: right;
+  clear: right;
+  margin-left: 1em;
+  margin-right: -1%;
+  background-color: #fffffa;
+}
+
+
+/* Code */
+pre.code { padding: 0.7ex }
+pre.code, code { background-color: #eeeeee }
+/* basic highlighting: for a complete scheme, see */
+/* https://docutils.sourceforge.io/sandbox/stylesheets/ */
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+
+/* Epigraph           */
+/* Highlights         */
+/* Pull-Quote         */
+/* Compound Paragraph */
+/* Container          */
+
+/* Inline Markup */
+/* ============= */
+
+sup, sub { line-height: 0.8; } /* do not add leading for lines with sup/sub */
+
+/* Inline Literals                                          */
+/* possible values: normal, nowrap, pre, pre-wrap, pre-line */
+/*   span.docutils.literal { white-space: pre-wrap; }       */
+
+/* Hyperlink References */
+a { text-decoration: none; }
+
+/* External Targets       */
+/*   span.target.external */
+/* Internal Targets       */
+/*   span.target.internal */
+/* Footnote References    */
+/*   a[role="doc-noteref"] */
+/* Citation References    */
+/*   a.citation-reference */
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css
new file mode 100644
index 00000000..234fa90b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css
@@ -0,0 +1,486 @@
+/* CSS3_ style sheet for the output of Docutils HTML5 writer.  */
+/* Generic responsive design for all screen sizes.                         */
+/*                                                                         */
+/* :Author: Günter Milde                                                   */
+/*                                                                         */
+/* :Id: $Id: responsive.css 9615 2024-04-06 13:28:15Z milde $                                                               */
+/* :Copyright: © 2021 Günter Milde.                                        */
+/* :License: Released under the terms of the `2-Clause BSD license`_,      */
+/*    in short:                                                            */
+/*                                                                         */
+/*    Copying and distribution of this file, with or without modification, */
+/*    are permitted in any medium without royalty provided the copyright   */
+/*    notice and this notice are preserved.                                */
+/*                                                                         */
+/*    This file is offered as-is, without any warranty.                    */
+/*                                                                         */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause     */
+/* .. _CSS3: https://www.w3.org/Style/CSS/                                 */
+
+/* Note:                                                                   */
+/* This style sheet is provisional:                                        */
+/* the API is not settled and may change with any minor Docutils version.  */
+
+
+
+/* General Settings */
+/* ================ */
+
+
+* { box-sizing: border-box; }
+
+body {
+  background-color: #fafaf6;
+  margin: auto;
+  --field-indent: 6.6em; /* indent of fields in field lists */
+  --sidebar-margin-right: 0; /* adapted in media queries below */
+}
+main {
+  counter-reset: figure table;
+}
+body > * {
+  background-color: white;
+  line-height: 1.6;
+  padding: 0.5rem calc(29% - 7.2rem); /* go from 5% to 15% (8.15em/54em) */
+  margin: auto;
+  max-width: 100rem;
+}
+sup, sub { /* avoid additional inter-line space for lines with sup/sub */
+  line-height: 1;
+}
+
+/* Vertical Space (Parskip) */
+p, ol, ul, dl, li,
+.topic,
+.footnote, .citation,
+div > math,
+table {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+}
+h1, h2, h3, h4, h5, h6,
+dl > dd, details > p:last-child {
+  margin-bottom: 0.5em;
+}
+
+/* Indented Blocks */
+blockquote, figure, .topic {
+  margin: 1em 2%;
+  padding-left: 1em;
+}
+div.line-block div.line-block,
+pre, dd, dl.option-list {
+  margin-left: calc(2% + 1em);
+}
+
+/* Object styling */
+/* ============== */
+
+footer, header {
+  font-size: small;
+}
+
+/* Frontmatter */
+div.dedication {
+  padding: 0;
+  margin: 1.4em 0;
+  font-style: italic;
+  font-size: large;
+}
+.dedication p.topic-title {
+  display: none;
+}
+
+blockquote p.attribution,
+.topic p.attribution {
+  text-align: right;
+}
+
+/* Table of Contents */
+nav.contents ul {
+  padding-left: 1em;
+}
+ul.auto-toc > li > p { /* hanging indent */
+  padding-left: 1em;
+  text-indent: -1em;
+}
+main > nav.contents  ul:not(.auto-toc) {
+  list-style-type: square;
+}
+main > nav.contents  ul ul:not(.auto-toc) {
+  list-style-type: disc;
+}
+main > nav.contents  ul ul ul:not(.auto-toc) {
+  list-style-type: '\2B29\ ';
+}
+main > nav.contents  ul ul ul ul:not(.auto-toc) {
+  list-style-type: '\2B1D\ ';
+}
+main > nav.contents  ul ul ul ul ul:not(.auto-toc) {
+  list-style-type: '\2B2A\ ';
+}
+nav.contents ul > li::marker {
+  color: grey;
+}
+
+/* Transitions */
+hr {
+  margin: 1em 10%;
+}
+
+/* Lists */
+
+dl.field-list.narrow, dl.docinfo, dl.option-list {
+  --field-indent: 2.4em;
+}
+
+ul, ol {
+  padding-left: 1.1em; /* indent by bullet width (Firefox, DejaVu fonts) */
+}
+dl.field-list > dd,
+dl.docinfo > dd {
+  margin-left: var(--field-indent); /* adapted in media queries or HTML */
+}
+dl.option-list > dd {
+  margin-left: 20%;
+}
+/* run-in: start field-body on same line after long field names */
+dl.field-list.run-in > dd p {
+  display: block;
+}
+/* "description style" like in most dictionaries, encyclopedias etc. */
+dl.description {
+  display: flow-root;
+}
+dl.description > dt {
+  clear: left;
+  float: left;
+  margin: 0;
+  padding: 0;
+  padding-right: 0.3em;
+  font-weight: bold;
+}
+dl.description > dd:after {
+  display: table;
+  content: "";
+  clear: left; /* clearfix for empty descriptions */
+}
+/* start lists nested in description/field lists on new line */
+dd > dl:first-child,
+dd > ul:first-child,
+dd > ol:first-child {
+  clear: left;
+}
+
+/* disclosures */
+details { padding-left: 1em; }
+summary { margin-left: -1em; }
+
+/* Footnotes and Citations */
+.footnote {
+  font-size: small;
+}
+
+/* Images, Figures, and Tables */
+figcaption,
+table > caption {
+  /*   font-size: small; */
+  font-style: italic;
+}
+figcaption > .legend {
+  font-size: small;
+  font-style: initial;
+}
+figure.numbered > figcaption > p:before {
+  counter-increment: figure;
+  content: "Figure " counter(figure) ": ";
+  font-weight: bold;
+  font-style: initial;
+}
+
+table tr {
+  text-align: left;
+  vertical-align: baseline;
+}
+table.booktabs { /* "booktabs" style (no vertical lines) */
+  border-top: 2px solid;
+  border-bottom: 2px solid;
+}
+table.booktabs * {
+  border: 0;
+}
+table.booktabs th {
+  border-bottom: thin solid;
+}
+table.numbered > caption:before {
+  counter-increment: table;
+  content: "Table " counter(table) ": ";
+  font-weight: bold;
+  font-style: initial;
+}
+
+/* Admonitions and System Messages */
+.admonition,
+div.system-message {
+  border: thin solid silver;
+  margin: 1em 2%;
+  padding: 0.5em 1em;
+}
+.caution p.admonition-title,
+.attention p.admonition-title,
+.danger p.admonition-title,
+.warning p.admonition-title,
+div.error {
+  color: maroon;
+}
+div.system-message > p > span.literal {
+  overflow-wrap: break-word;
+}
+
+/* Literal and Code */
+pre.literal-block, pre.doctest{
+  padding: 0.2em;
+  overflow-x: auto;
+}
+.literal-block, .doctest, span.literal {
+  background-color: #f6f9f8;
+}
+.system-message span.literal {
+  background-color: inherit;
+}
+
+/* basic highlighting: for a complete scheme, see */
+/* https://docutils.sourceforge.io/sandbox/stylesheets/ */
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+/* Hyperlink References */
+a {
+  text-decoration: none; /* for chromium */
+  /* Wrap links at any place, if this is the only way to prevent overflow */
+  overflow-wrap: break-word;
+}
+.contents a, a.toc-backref, a.citation-reference {
+  overflow-wrap: inherit;
+}
+/* Undecorated Links (see also minimal.css) */
+/* a.citation-reference, */
+.citation a.fn-backref {
+  color: inherit;
+}
+a:hover {
+  text-decoration: underline;
+}
+*:hover > a.toc-backref:after,
+.topic-title:hover > a:after {
+  content: " \2191"; /* ↑ UPWARDS ARROW */
+  color: grey;
+}
+*:hover > a.self-link:after {
+  content: "\1F517"; /* LINK SYMBOL */
+  color: grey;
+  font-size: smaller;
+  margin-left: 0.2em;
+}
+/* highlight specific targets of the current URL */
+section:target > h2, section:target > h3, section:target > h4,
+section:target > h5, section:target > h6,
+span:target + h2, span:target + h3, span:target + h4,
+span:target + h5, span:target + h6,
+dt:target, span:target,
+.contents :target,
+.contents:target > .topic-title,
+[role="doc-biblioentry"]:target > .label,
+[role="doc-biblioref"]:target,
+[role="note"]:target, /* Docutils 0.18 ... 0.19 */
+[role="doc-footnote"]:target, /* Docutils >= 0.20 */
+[role="doc-noteref"]:target {
+  background-color: #d2e6ec;
+}
+
+/* Block Alignment */
+/* Let content flow to the side of aligned images and figures */
+
+/* no floats around this elements */
+footer, header, hr,
+h1, h2, h3 {
+  clear: both;
+}
+
+img.align-left,
+svg.align-left,
+video.align-left,
+figure.align-left,
+div.align-left,
+table.align-left {
+  margin-left: 0;
+  padding-left: 0;
+  margin-right: 0.5em;
+  clear: left;
+  float: left;
+}
+img.align-right,
+svg.align-right,
+video.align-right,
+figure.align-right,
+div.align-right,
+table.align-right {
+  margin-left: 0.5em;
+  margin-right: 0;
+  clear: right;
+  float: right;
+}
+
+/* Margin Elements */
+/* see below for screen size dependent rules */
+.sidebar,
+.marginal,
+.admonition.marginal {
+  max-width: 40%;
+  border: none;
+  background-color: #efefea;
+  margin: 0.5em var(--sidebar-margin-right) 0.5em 1em;
+  padding: 0.5em;
+  padding-left: 0.7em;
+  clear: right;
+  float: right;
+  font-size: small;
+}
+.sidebar {
+  width: 40%;
+}
+
+/* Adaptive page layout */
+/* ==================== */
+
+@media (max-width: 30em) {
+  /* Smaller margins and no floating elements for small screens */
+  /* (main text less than 40 characters/line) */
+  body > * {
+    padding: 0.5rem 5%;
+    line-height: 1.4
+  }
+  .sidebar,
+  .marginal,
+  .admonition.marginal {
+    width: auto;
+    max-width: 100%;
+    float: none;
+  }
+  dl.option-list,
+  pre {
+    margin-left: 0;
+  }
+  body {
+    --field-indent: 4em;
+  }
+  pre, pre * {
+  font-size: 0.9em;
+  /* overflow: auto; */
+  }
+}
+
+@media (min-width: 54em) {
+  /* Move ToC to the left */
+  /* Main text width before: 70% ≙ 35em ≙ 75…95 chrs (Dejavu/Times) */
+  /*                  after:      ≳ 30em ≙ 54…70 chrs (Dejavu/Times) */
+  body.with-toc {
+    padding-left: 8%;
+  }
+  body.with-toc > * {
+    margin-left: 0;
+    padding-left: 22rem; /* fallback for webkit */
+    padding-left: min(22%, 22rem);
+    padding-right: 7%;
+  }
+  main > nav.contents { /* global ToC */
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: min(25%, 25em);
+    height: 100vh;
+    margin: 0;
+    background-color: #fafaf6;
+    padding: 1em 2% 0 2%;
+    overflow: auto;
+  }
+  main > nav.contents > * {
+    padding-left: 0;
+    line-height: 1.4;
+  }
+  main > nav.contents a {
+  color: inherit;
+  }
+}
+
+@media (min-width: 70em) {
+  body {
+    --field-indent: 9em;
+  }
+}
+
+@media (min-width: 77em) {
+  /* Move marginalia to 6rem from right border       */
+  /* .sidebar,                                       */
+  /* .marginal,                                      */
+  /* .admonition.marginal {                          */
+  /*   margin-right: calc(6rem - 15%);               */
+  /* }                                               */
+  /* BUG: margin is calculated for break point width */
+  /* workaround: variable + many breakpoints         */
+  body > * {
+    padding-left: 18%;
+    padding-right: 28%; /* fallback for webkit */
+    padding-right: min(28%, 28rem);
+    --sidebar-margin-right: -20rem;
+  }
+  /* limit main text to ~ 50em ≙ 85…100 characters DejaVu rsp. …120 Times */
+  body.with-toc > * {
+    padding-left: min(22%, 22rem);
+    padding-right: calc(78% - 50rem); /* fallback for webkit */
+    padding-right: min(78% - 50rem, 28rem);
+    --sidebar-margin-right: 0;
+  }
+}
+
+@media (min-width: 85em) {
+  body.with-toc > * {
+    --sidebar-margin-right: -9rem;
+  }
+}
+
+@media (min-width: 90em) {
+  /* move marginalia into the margin */
+  body > * {
+    padding-left: min(22%, 22rem);
+    --sidebar-margin-right: -23rem;
+  }
+  body.with-toc > * {
+    --sidebar-margin-right: -14rem;
+  }
+}
+
+@media (min-width: 99em) {
+  /* move marginalia out of main text area */
+  body.with-toc > * {
+    --sidebar-margin-right: -20rem;
+  }
+  body > *, body.with-toc > * { /* for webkit */
+    padding-left: 22rem;
+    padding-right: 28rem;
+  }
+  .admonition.marginal,
+  .marginal {
+    width: 40%; /* make marginal figures, ... "full width" */
+  }
+}
+
+@media (min-width: 104em) {
+  body.with-toc > * {
+    --sidebar-margin-right: -23rem;
+  }
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt
new file mode 100644
index 00000000..2591bce3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt
@@ -0,0 +1,8 @@
+%(head_prefix)s
+%(head)s
+%(stylesheet)s
+%(body_prefix)s
+%(body_pre_docinfo)s
+%(docinfo)s
+%(body)s
+%(body_suffix)s
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css
new file mode 100644
index 00000000..cdedfded
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css
@@ -0,0 +1,566 @@
+/* CSS3_ style sheet for the output of Docutils HTML writers.              */
+/* Rules inspired by Edward Tufte's layout design.                         */
+/*                                                                         */
+/* :Author: Günter Milde                                                   */
+/*          based on tufte.css_ by Dave Liepmann                           */
+/*          and the tufte-latex_ package.                                  */
+/*                                                                         */
+/* :Id: $Id: tuftig.css 9503 2023-12-16 22:37:59Z milde $                                                               */
+/* :Copyright: © 2020 Günter Milde.                                        */
+/* :License: Released under the terms of the `2-Clause BSD license`_,      */
+/*    in short:                                                            */
+/*                                                                         */
+/*    Copying and distribution of this file, with or without modification, */
+/*    are permitted in any medium without royalty provided the copyright   */
+/*    notice and this notice are preserved.                                */
+/*                                                                         */
+/*    This file is offered as-is, without any warranty.                    */
+/*                                                                         */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause     */
+/* .. _CSS3: https://www.w3.org/Style/CSS/                                 */
+/* .. _tufte.css: https://edwardtufte.github.io/tufte-css/                 */
+/* .. _tufte-latex_: https://www.ctan.org/pkg/tufte-latex                  */
+
+
+/* General Settings */
+/* ================ */
+
+body {
+  font-family: Georgia, serif;
+  background-color: #fafaf6;
+  font-size: 1.2em;
+  line-height: 1.4;
+  margin: auto;
+}
+main {
+  counter-reset: figure table;
+}
+main, header, footer {
+  padding: 0.5em 5%;
+  background-color: #fefef8;
+  max-width: 100rem;
+}
+
+/* Spacing */
+
+/* vertical space (parskip) */
+p, ol, ul, dl, li,
+h1, h2, h3, h4, h5, h6,
+div.line-block,
+.topic,
+.footnote, .citation,
+table {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+}
+dl > dd {
+  margin-bottom: 0.5em;
+}
+/* exceptions */
+p:first-child {
+  margin-top: 0;
+}
+p:last-child {
+  margin-bottom: 0;
+}
+
+/* Indented Blocks */
+blockquote,
+.topic {
+  /* background-color: Honeydew; */
+  margin: 0.5em 2%;
+  padding-left: 1em;
+}
+div.line-block div.line-block,
+dl.option-list,
+figure > img,
+pre.literal-block, pre.math,
+pre.doctest-block, pre.code {
+  /* background-color: LightCyan; */
+  margin-left: calc(2% + 1em);
+}
+
+/* Object styling */
+/* ============== */
+
+footer, header {
+  font-size: smaller;
+}
+
+/* Titles and Headings */
+
+h2, h3, h4, p.subtitle, p.section-subtitle,
+p.topic-title, p.sidebar-title, p.sidebar-subtitle {
+  font-weight: normal;
+  font-style: italic;
+  text-align: left;
+}
+.sectnum {
+  font-style: normal;
+}
+
+h1.title {
+  text-align: left;
+  margin-top: 2.4em;
+  margin-bottom: 2em;
+  font-size: 2.4em;
+}
+h1 + p.subtitle {
+  margin-top: -2em;
+  margin-bottom: 2em;
+  font-size: 2.0em;
+}
+section {
+  margin-top: 2em;
+}
+h2, .contents > p.topic-title {
+  font-size: 2.2em;
+}
+h2 + p.section-subtitle {
+  font-size: 1.6em;
+}
+h3 {
+  font-size: 1.2em;
+}
+h3 + p.section-subtitle {
+  font-size: 1.1em;
+}
+h4 {
+  font-size: 1em;
+}
+p.section-subtitle {
+  font-size: 1em;
+}
+
+/* Dedication and Abstract */
+div.dedication {
+  padding: 0;
+  margin-left: 0;
+  font-style: italic;
+  font-size: 1.2em;
+}
+/* div.abstract p.topic-title, */
+div.dedication p.topic-title {
+  display: none;
+}
+
+/* Attribution */
+blockquote p.attribution,
+.topic p.attribution {
+  text-align: right;
+}
+
+/* Table of Contents */
+nav.contents {
+  padding: 0;
+  font-style: italic;
+}
+ul.auto-toc > li > p {
+  padding-left: 1em;
+  text-indent: -1em;
+}
+nav.contents ul {
+  padding-left: 1em;
+}
+
+
+/* Transitions */
+hr {
+  border: 0;
+  border-top: 1px solid #ccc;
+  margin: 1em 10%;
+}
+
+/* Lists */
+/* Less indent per level */
+ul, ol {
+  padding-left: 1.1em;
+}
+dd {
+  margin-left: 1.5em;
+}
+dd > dl:first-child,
+dd > ul:first-child,
+dd > ol:first-child {
+  /* lists nested in definition/description/field lists */
+  clear: left;
+}
+
+dl.field-list > dd,
+dl.docinfo > dd,
+dl.option-list > dd {
+  margin-left: 4em;
+}
+/* example for custom field-name width */
+dl.field-list.narrow > dd {
+  margin-left: 3em;
+}
+/* run-in: start field-body on same line after long field names */
+dl.field-list.run-in > dd p {
+  display: block;
+}
+/* italic field name */
+dl.description > dt,
+dl.field-list > dt,
+dl.docinfo > dt {
+  font-weight: normal;
+  font-style: italic;
+}
+
+/* "description style" like in most dictionaries, encyclopedias etc. */
+dl.description > dt {
+  clear: left;
+  float: left;
+  margin: 0;
+  padding: 0;
+  padding-right: 0.5em;
+}
+dl.description > dd:after {
+  display: block;
+  content: "";
+  clear: both;
+}
+
+/* Citation list (style as description list) */
+.citation-list,
+.footnote-list {
+  display: contents;
+}
+.citation {
+  padding-left: 1.5em;
+}
+.citation .label {
+  margin-left: -1.5em;
+}
+
+/* Images and Figures */
+/* Caption to the left (if there is space) or below: */
+figure {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: flex-start;
+  margin: 0.5em 2%;
+  padding-left: 1em;
+}
+figure > img,
+figure.fullwidth > img {
+  margin: 0 0.5em 0.5em 0;
+  padding: 0;
+}
+figcaption {
+  font-size: 0.8em;
+}
+.fullwidth > figcaption {
+  font-size: inherit;
+}
+figure.numbered > figcaption > p:before {
+  counter-increment: figure;
+  content: "Figure " counter(figure) ": ";
+}
+
+/* Tables */
+table tr {
+  text-align: left;
+}
+/* th { vertical-align: bottom; } */
+/* "booktabs" style (no vertical lines) */
+table.booktabs {
+  border-top: 2px solid;
+  border-bottom: 2px solid;
+}
+table.booktabs * {
+  border: 0;
+}
+table.booktabs th {
+  border-bottom: thin solid;
+}
+table.numbered > caption:before {
+  counter-increment: table;
+  content: "Table " counter(table) ": ";
+}
+
+/* Admonitions and System Messages */
+.admonition, .system-message {
+  border-style: solid;
+  border-color: silver;
+  border-width: thin;
+  margin: 1em 0;
+  padding: 0.5em;
+}
+.caution p.admonition-title,
+.attention p.admonition-title,
+.danger p.admonition-title,
+.warning p.admonition-title,
+div.error {
+  color: maroon;
+}
+
+/* Literal and Code */
+pre.literal-block, pre.doctest-block,
+pre.math, pre.code {
+  /* font-family: Consolas, "Liberation Mono", Menlo, monospace; */
+  /* font-size: 0.9em; */
+  overflow: auto;
+}
+/* basic highlighting: for a complete scheme, see */
+/* https://docutils.sourceforge.io/sandbox/stylesheets/ */
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+.sans {
+    font-family: "Gill Sans", "Gill Sans MT", Calibri, "Lucida Sans", "Noto Sans", sans-serif;
+    letter-spacing: .02em;
+}
+
+/* Hyperlink References */
+/* underline that clears descenders */
+a {
+  color: inherit;
+}
+a:link {
+  text-decoration: underline;
+  /* text-decoration-skip-ink: auto; nonstandard selector */
+}
+/* undecorated links */
+.contents a:link, a.toc-backref:link, a.image-reference:link,
+a[role="doc-noteref"]:link, a[role="doc-backlink"]:link, .backrefs a:link,
+a.citation-reference:link,
+a[href^="#system-message"] {
+  text-decoration: none;
+}
+a:link:hover {
+  text-decoration: underline;
+}
+
+/* Block Alignment */
+/* Let content flow to the side of aligned images and figures */
+/* (does not work if the image/figure is a grid element). */
+
+/* no floats around this elements */
+footer, header,
+hr.docutils,
+h1, h2, h3, .contents > p.topic-title,
+.fullwidth {
+  clear: both;
+}
+
+img.align-left,
+svg.align-left,
+video.align-left,
+figure.align-left,
+div.align-left,
+table.align-left {
+  margin-left: 0;
+  padding-left: 0;
+  padding-right: 0.5em;
+  clear: left;
+  float: left;
+}
+figure.align-left > img {
+  margin-left: 0;
+  padding-left: 0;
+}
+
+img.align-right,
+svg.align-right,
+video.align-right,
+div.align-right {
+  padding-left: 0.5em;
+  clear: right;
+  float: right;
+}
+figure.align-right {
+  clear: right;
+  float: right;
+}
+figure.align-right > img {
+  justify-self: right;
+  padding: 0;
+}
+table.align-right {
+  margin-right: 2.5%;
+}
+
+figure.align-center {
+  align-content: center;
+  justify-content: center;
+}
+figure.align-center > img {
+  padding-left: 0;
+  justify-self: center;
+}
+
+/* Margin Elements */
+/* see below for screen size dependent rules */
+aside.sidebar,
+.marginal,
+.admonition.marginal,
+.topic.marginal {
+  background-color: #efefea;
+  box-sizing: border-box;
+  margin-left: 2%;
+  margin-right: 0;
+  padding: 0.5em;
+  font-size: 0.8em;
+}
+aside.sidebar {
+  background-color: inherit;
+}
+figure.marginal > figcaption {
+  font-size: 1em;
+}
+.footnote {
+  font-size: smaller;
+  overflow: auto;
+}
+
+/* Adaptive page layout */
+
+/* no floating for very small Screens */
+/* (main text up to ca. 40 characters/line) */
+@media (min-width: 35em) {
+  main, header, footer {
+    padding: 0.5em calc(15% - 3rem);
+    line-height: 1.6
+  }
+  aside.sidebar,
+  .marginal,
+  .admonition.marginal,
+  .topic.marginal {
+    max-width: 45%;
+    float: right;
+    clear: right;
+  }
+  dl.field-list > dd,
+  dl.docinfo > dd {
+    margin-left: 6em;
+  }
+  dl.option-list > dd {
+    margin-left: 6em;
+  }
+}
+
+/* 2 column layout with wide margin */
+@media (min-width: 65em) {
+  /* use the same grid for main, all sections, and figures */
+  main, section {
+    display: grid;
+    grid-template-columns: [content] minmax(0, 6fr)
+    			   [margin] 3fr [end];
+    grid-column-gap: calc(3em + 1%);
+  }
+  main > section, section > section {
+    grid-column: 1 / end;
+  }
+  main, header, footer {
+    padding-right: 5%; /* less padding right of margin-column */
+  }
+  section > figure {
+    display: contents; /* to place caption in the margin */
+  }
+  /* Main text elements */
+  main > *, section > *,
+  figure > img,
+  .footnote.align-left, /* override the placement in the margin */
+  .citation.align-left {
+    grid-column: content;
+  }
+  .citation.align-left {
+    font-size: 1em;
+    padding-left: 1.5em;
+  }
+  .citation.align-left .label {
+    margin-left: -1.5em;
+  }
+  figure > img { /* indent */
+    margin: 0.5em 2%;
+    padding-left: 1em;
+  }
+
+  /* Margin Elements */
+  /* Sidebar, Footnotes, Citations, Captions */
+  aside.sidebar,
+  .citation,
+  .footnote,
+  figcaption,
+  /* table > caption,  does not work :(*/
+  .marginal,
+  .admonition.marginal,
+  .topic.marginal {
+    /* color: red; */
+    grid-column: margin;
+    width: auto;
+    max-width: 55em;
+    margin: 0.5em 0;
+    border: none;
+    padding: 0;
+    font-size: 0.8em;
+    text-align: initial; /* overwrite align-* */
+    background-color: inherit;
+  }
+  .admonition.marginal {
+    padding: 0.5em;
+  }
+  figure.marginal {
+    display: block;
+    margin: 0.5em 0;
+  }
+  .citation,
+  .footnote {
+    padding-left: 0;
+  }
+  .citation .label,
+  .footnote .label {
+    margin-left: 0;
+  }
+
+  /* Fullwidth Elements */
+  h1.title, p.subtitle,
+  dl.docinfo,
+  div.abstract,
+  div.dedication,
+  nav.contents,
+  aside.system-message,
+  pre,
+  .fullwidth,
+  .fullwidth img,
+  .fullwidth figcaption {
+    /* background-color: Linen; */
+    grid-column: content / end;
+    margin-right: calc(10% - 3rem);
+    max-width: 55em;
+  }
+}
+
+/* 3 column layout */
+
+@media (min-width: 100em) {
+  main, header, footer {
+    padding-left: 30%;
+  }
+  main > nav.contents {
+    position: fixed;
+    top: 0;
+    left: 0;
+    box-sizing: border-box;
+    width: 25%;
+    height: 100vh;
+    margin: 0;
+    background-color: #fafaf6;
+    padding: 5.5em 2%;
+    overflow: auto;
+  }
+  main > nav.contents > * {
+    padding-left: 0;
+  }
+}
+
+/* wrap URLs */
+/*   a:link {    */
+/*     white-space: normal; */
+/*     hyphens: none;	    */
+/*   }			    */
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py
new file mode 100644
index 00000000..d1960a79
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py
@@ -0,0 +1,3323 @@
+# $Id: __init__.py 9581 2024-03-17 23:31:04Z milde $
+# Author: Engelbert Gruber, Günter Milde
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""LaTeX2e document tree Writer."""
+
+__docformat__ = 'reStructuredText'
+
+# code contributions from several people included, thanks to all.
+# some named: David Abrahams, Julien Letessier, Lele Gaifax, and others.
+#
+# convention deactivate code by two # i.e. ##.
+
+from pathlib import Path
+import re
+import string
+from urllib.request import url2pathname
+import warnings
+try:
+    import roman
+except ImportError:
+    import docutils.utils.roman as roman
+
+from docutils import frontend, nodes, languages, writers, utils
+from docutils.transforms import writer_aux
+from docutils.utils.math import pick_math_environment, unichar2tex
+
+LATEX_WRITER_DIR = Path(__file__).parent
+
+
+class Writer(writers.Writer):
+
+    supported = ('latex', 'latex2e')
+    """Formats this writer supports."""
+
+    default_template = 'default.tex'
+    default_template_path = LATEX_WRITER_DIR
+    default_preamble = ('% PDF Standard Fonts\n'
+                        '\\usepackage{mathptmx} % Times\n'
+                        '\\usepackage[scaled=.90]{helvet}\n'
+                        '\\usepackage{courier}')
+    table_style_values = [  # TODO: align-left, align-center, align-right, ??
+                          'booktabs', 'borderless', 'colwidths-auto',
+                          'nolines', 'standard']
+
+    settings_spec = (
+        'LaTeX-Specific Options',
+        None,
+        (('Specify LaTeX documentclass.  Default: "article".',
+          ['--documentclass'],
+          {'default': 'article', }),
+         ('Specify document options.  Multiple options can be given, '
+          'separated by commas.  Default: "a4paper".',
+          ['--documentoptions'],
+          {'default': 'a4paper', }),
+         ('Format for footnote references: one of "superscript" or '
+          '"brackets".  Default: "superscript".',
+          ['--footnote-references'],
+          {'choices': ['superscript', 'brackets'], 'default': 'superscript',
+           'metavar': '<format>',
+           'overrides': 'trim_footnote_reference_space'}),
+         ('Use \\cite command for citations. (future default)',
+          ['--use-latex-citations'],
+          {'default': None, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Use figure floats for citations '
+          '(might get mixed with real figures). (provisional default)',
+          ['--figure-citations'],
+          {'dest': 'use_latex_citations', 'action': 'store_false',
+           'validator': frontend.validate_boolean}),
+         ('Format for block quote attributions: one of "dash" (em-dash '
+          'prefix), "parentheses"/"parens", or "none".  Default: "dash".',
+          ['--attribution'],
+          {'choices': ['dash', 'parentheses', 'parens', 'none'],
+           'default': 'dash', 'metavar': '<format>'}),
+         ('Specify LaTeX packages/stylesheets. '
+          'A style is referenced with "\\usepackage" if extension is '
+          '".sty" or omitted and with "\\input" else. '
+          ' Overrides previous --stylesheet and --stylesheet-path settings.',
+          ['--stylesheet'],
+          {'default': '', 'metavar': '<file[,file,...]>',
+           'overrides': 'stylesheet_path',
+           'validator': frontend.validate_comma_separated_list}),
+         ('Comma separated list of LaTeX packages/stylesheets. '
+          'Relative paths are expanded if a matching file is found in '
+          'the --stylesheet-dirs. With --link-stylesheet, '
+          'the path is rewritten relative to the output *.tex file. ',
+          ['--stylesheet-path'],
+          {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+           'validator': frontend.validate_comma_separated_list}),
+         ('Link to the stylesheet(s) in the output file. (default)',
+          ['--link-stylesheet'],
+          {'dest': 'embed_stylesheet', 'action': 'store_false'}),
+         ('Embed the stylesheet(s) in the output file. '
+          'Stylesheets must be accessible during processing. ',
+          ['--embed-stylesheet'],
+          {'default': False, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Comma-separated list of directories where stylesheets are found. '
+          'Used by --stylesheet-path when expanding relative path arguments. '
+          'Default: ".".',
+          ['--stylesheet-dirs'],
+          {'metavar': '<dir[,dir,...]>',
+           'validator': frontend.validate_comma_separated_list,
+           'default': ['.']}),
+         ('Customization by LaTeX code in the preamble. '
+          'Default: select PDF standard fonts (Times, Helvetica, Courier).',
+          ['--latex-preamble'],
+          {'default': default_preamble}),
+         ('Specify the template file. Default: "%s".' % default_template,
+          ['--template'],
+          {'default': default_template, 'metavar': '<file>'}),
+         ('Table of contents by LaTeX. (default)',
+          ['--use-latex-toc'],
+          {'default': True, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Table of contents by Docutils (without page numbers).',
+          ['--use-docutils-toc'],
+          {'dest': 'use_latex_toc', 'action': 'store_false',
+           'validator': frontend.validate_boolean}),
+         ('Add parts on top of the section hierarchy.',
+          ['--use-part-section'],
+          {'default': False, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Attach author and date to the document info table. (default)',
+          ['--use-docutils-docinfo'],
+          {'dest': 'use_latex_docinfo', 'action': 'store_false',
+           'validator': frontend.validate_boolean}),
+         ('Attach author and date to the document title.',
+          ['--use-latex-docinfo'],
+          {'default': False, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ("Typeset abstract as topic. (default)",
+          ['--topic-abstract'],
+          {'dest': 'use_latex_abstract', 'action': 'store_false',
+           'validator': frontend.validate_boolean}),
+         ("Use LaTeX abstract environment for the document's abstract.",
+          ['--use-latex-abstract'],
+          {'default': False, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Color of any hyperlinks embedded in text. '
+          'Default: "blue" (use "false" to disable).',
+          ['--hyperlink-color'], {'default': 'blue'}),
+         ('Additional options to the "hyperref" package.',
+          ['--hyperref-options'], {'default': ''}),
+         ('Enable compound enumerators for nested enumerated lists '
+          '(e.g. "1.2.a.ii").',
+          ['--compound-enumerators'],
+          {'default': False, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Disable compound enumerators for nested enumerated lists. '
+          '(default)',
+          ['--no-compound-enumerators'],
+          {'action': 'store_false', 'dest': 'compound_enumerators'}),
+         ('Enable section ("." subsection ...) prefixes for compound '
+          'enumerators.  This has no effect without --compound-enumerators.',
+          ['--section-prefix-for-enumerators'],
+          {'default': None, 'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Disable section prefixes for compound enumerators. (default)',
+          ['--no-section-prefix-for-enumerators'],
+          {'action': 'store_false', 'dest': 'section_prefix_for_enumerators'}),
+         ('Set the separator between section number and enumerator '
+          'for compound enumerated lists.  Default: "-".',
+          ['--section-enumerator-separator'],
+          {'default': '-', 'metavar': '<char>'}),
+         ('When possible, use the specified environment for literal-blocks. '
+          'Default: "" (fall back to "alltt").',
+          ['--literal-block-env'],
+          {'default': ''}),
+         ('Deprecated alias for "--literal-block-env=verbatim".',
+          ['--use-verbatim-when-possible'],
+          {'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Table style. "standard" with horizontal and vertical lines, '
+          '"booktabs" (LaTeX booktabs style) only horizontal lines '
+          'above and below the table and below the header, or "borderless". '
+          'Default: "standard"',
+          ['--table-style'],
+          {'default': ['standard'],
+           'metavar': '<format>',
+           'action': 'append',
+           'validator': frontend.validate_comma_separated_list,
+           'choices': table_style_values}),
+         ('LaTeX graphicx package option. '
+          'Possible values are "dvipdfmx", "dvips", "dvisvgm", '
+          '"luatex", "pdftex", and "xetex".'
+          'Default: "".',
+          ['--graphicx-option'],
+          {'default': ''}),
+         ('LaTeX font encoding. '
+          'Possible values are "", "T1" (default), "OT1", "LGR,T1" or '
+          'any other combination of options to the `fontenc` package. ',
+          ['--font-encoding'],
+          {'default': 'T1'}),
+         ('Per default the latex-writer puts the reference title into '
+          'hyperreferences. Specify "ref*" or "pageref*" to get the section '
+          'number or the page number.',
+          ['--reference-label'],
+          {'default': ''}),
+         ('Specify style and database(s) for bibtex, for example '
+          '"--use-bibtex=unsrt,mydb1,mydb2". Provisional!',
+          ['--use-bibtex'],
+          {'default': '',
+           'metavar': '<style,bibfile[,bibfile,...]>',
+           'validator': frontend.validate_comma_separated_list}),
+         ('Use legacy functions with class value list for '
+          '\\DUtitle and \\DUadmonition.',
+          ['--legacy-class-functions'],
+          {'default': False,
+           'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Use \\DUrole and "DUclass" wrappers for class values. '
+          'Place admonition content in an environment. (default)',
+          ['--new-class-functions'],
+          {'dest': 'legacy_class_functions',
+           'action': 'store_false',
+           'validator': frontend.validate_boolean}),
+         ('Use legacy algorithm to determine table column widths. '
+          '(provisional default)',
+          ['--legacy-column-widths'],
+          {'default': None,
+           'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ('Use new algorithm to determine table column widths. '
+          '(future default)',
+          ['--new-column-widths'],
+          {'dest': 'legacy_column_widths',
+           'action': 'store_false',
+           'validator': frontend.validate_boolean}),
+         # TODO: implement "latex footnotes" alternative
+         ('Footnotes with numbers/symbols by Docutils. (default) '
+          '(The alternative, --latex-footnotes, is not implemented yet.)',
+          ['--docutils-footnotes'],
+          {'default': True,
+           'action': 'store_true',
+           'validator': frontend.validate_boolean}),
+         ),
+        )
+
+    relative_path_settings = ('template',)
+    settings_defaults = {'sectnum_depth': 0}  # updated by SectNum transform
+    config_section = 'latex2e writer'
+    config_section_dependencies = ('writers', 'latex writers')
+
+    head_parts = ('head_prefix', 'requirements', 'latex_preamble',
+                  'stylesheet', 'fallbacks', 'pdfsetup', 'titledata')
+    visitor_attributes = head_parts + ('title', 'subtitle',
+                                       'body_pre_docinfo', 'docinfo',
+                                       'dedication', 'abstract', 'body')
+
+    output = None
+    """Final translated form of `document`."""
+
+    def __init__(self):
+        writers.Writer.__init__(self)
+        self.translator_class = LaTeXTranslator
+
+    def get_transforms(self):
+        # Override parent method to add latex-specific transforms
+        return super().get_transforms() + [
+                   # Convert specific admonitions to generic one
+                   writer_aux.Admonitions,
+                   # TODO: footnote collection transform
+                   ]
+
+    def translate(self):
+        visitor = self.translator_class(self.document)
+        self.document.walkabout(visitor)
+        # copy parts
+        for part in self.visitor_attributes:
+            setattr(self, part, getattr(visitor, part))
+        # get template string from file
+        templatepath = Path(self.document.settings.template)
+        if not templatepath.exists():
+            templatepath = self.default_template_path / templatepath.name
+        template = templatepath.read_text(encoding='utf-8')
+        # fill template
+        self.assemble_parts()  # create dictionary of parts
+        self.output = string.Template(template).substitute(self.parts)
+
+    def assemble_parts(self):
+        """Assemble the `self.parts` dictionary of output fragments."""
+        writers.Writer.assemble_parts(self)
+        for part in self.visitor_attributes:
+            lines = getattr(self, part)
+            if part in self.head_parts:
+                if lines:
+                    lines.append('')  # to get a trailing newline
+                self.parts[part] = '\n'.join(lines)
+            else:
+                # body contains inline elements, so join without newline
+                self.parts[part] = ''.join(lines)
+
+
+class Babel:
+    """Language specifics for LaTeX."""
+
+    # TeX (babel) language names:
+    # ! not all of these are supported by Docutils!
+    #
+    # based on LyX' languages file with adaptions to `BCP 47`_
+    # (https://www.rfc-editor.org/rfc/bcp/bcp47.txt) and
+    # http://www.tug.org/TUGboat/Articles/tb29-3/tb93miklavec.pdf
+    # * the key without subtags is the default
+    # * case is ignored
+    # cf. https://docutils.sourceforge.io/docs/howto/i18n.html
+    #     https://www.w3.org/International/articles/language-tags/
+    # and http://www.iana.org/assignments/language-subtag-registry
+    language_codes = {
+        # code          TeX/Babel-name    comment
+        'af':           'afrikaans',
+        'ar':           'arabic',
+        # 'be':           'belarusian',
+        'bg':           'bulgarian',
+        'br':           'breton',
+        'ca':           'catalan',
+        # 'cop':          'coptic',
+        'cs':           'czech',
+        'cy':           'welsh',
+        'da':           'danish',
+        'de':           'ngerman',        # new spelling (de_1996)
+        'de-1901':      'german',         # old spelling
+        'de-AT':        'naustrian',
+        'de-AT-1901':   'austrian',
+        'dsb':          'lowersorbian',
+        'el':           'greek',          # monotonic (el-monoton)
+        'el-polyton':   'polutonikogreek',
+        'en':           'english',        # TeX' default language
+        'en-AU':        'australian',
+        'en-CA':        'canadian',
+        'en-GB':        'british',
+        'en-NZ':        'newzealand',
+        'en-US':        'american',
+        'eo':           'esperanto',
+        'es':           'spanish',
+        'et':           'estonian',
+        'eu':           'basque',
+        # 'fa':           'farsi',
+        'fi':           'finnish',
+        'fr':           'french',
+        'fr-CA':        'canadien',
+        'ga':           'irish',          # Irish Gaelic
+        # 'grc':                          # Ancient Greek
+        'grc-ibycus':   'ibycus',         # Ibycus encoding
+        'gl':           'galician',
+        'he':           'hebrew',
+        'hr':           'croatian',
+        'hsb':          'uppersorbian',
+        'hu':           'magyar',
+        'ia':           'interlingua',
+        'id':           'bahasai',        # Bahasa (Indonesian)
+        'is':           'icelandic',
+        'it':           'italian',
+        'ja':           'japanese',
+        'kk':           'kazakh',
+        'la':           'latin',
+        'lt':           'lithuanian',
+        'lv':           'latvian',
+        'mn':           'mongolian',      # Mongolian, Cyrillic (mn-cyrl)
+        'ms':           'bahasam',        # Bahasa (Malay)
+        'nb':           'norsk',          # Norwegian Bokmal
+        'nl':           'dutch',
+        'nn':           'nynorsk',        # Norwegian Nynorsk
+        'no':           'norsk',          # Norwegian (Bokmal)
+        'pl':           'polish',
+        'pt':           'portuges',
+        'pt-BR':        'brazil',
+        'ro':           'romanian',
+        'ru':           'russian',
+        'se':           'samin',          # North Sami
+        'sh-Cyrl':      'serbianc',       # Serbo-Croatian, Cyrillic
+        'sh-Latn':      'serbian',        # Serbo-Croatian, Latin (cf. 'hr')
+        'sk':           'slovak',
+        'sl':           'slovene',
+        'sq':           'albanian',
+        'sr':           'serbianc',       # Serbian, Cyrillic (contributed)
+        'sr-Latn':      'serbian',        # Serbian, Latin script
+        'sv':           'swedish',
+        # 'th':           'thai',
+        'tr':           'turkish',
+        'uk':           'ukrainian',
+        'vi':           'vietnam',
+        # zh-Latn:      Chinese Pinyin
+        }
+    # normalize (downcase) keys
+    language_codes = {k.lower(): v for k, v in language_codes.items()}
+
+    warn_msg = 'Language "%s" not supported by LaTeX (babel)'
+
+    # "Active characters" are shortcuts that start a LaTeX macro and may need
+    # escaping for literals use. Characters that prevent literal use (e.g.
+    # starting accent macros like "a -> ä) will be deactivated if one of the
+    # defining languages is used in the document.
+    # Special cases:
+    #  ~ (tilde) -- used in estonian, basque, galician, and old versions of
+    #    spanish -- cannot be deactivated as it denotes a no-break space macro,
+    #  " (straight quote) -- used in albanian, austrian, basque
+    #    brazil, bulgarian, catalan, czech, danish, dutch, estonian,
+    #    finnish, galician, german, icelandic, italian, latin, naustrian,
+    #    ngerman, norsk, nynorsk, polish, portuges, russian, serbian, slovak,
+    #    slovene, spanish, swedish, ukrainian, and uppersorbian --
+    #    is escaped as ``\textquotedbl``.
+    active_chars = {
+                    # TeX/Babel-name:  active characters to deactivate
+                    # 'breton':        ':;!?'  # ensure whitespace
+                    # 'esperanto':     '^',
+                    # 'estonian':      '~"`',
+                    # 'french':        ':;!?'  # ensure whitespace
+                    'galician':        '.<>',  # also '~"'
+                    # 'magyar':        '`',  # for special hyphenation cases
+                    'spanish':         '.<>',  # old versions also '~'
+                    # 'turkish':       ':!='  # ensure whitespace
+                   }
+
+    def __init__(self, language_code, reporter=None):
+        self.reporter = reporter
+        self.language = self.language_name(language_code)
+        self.otherlanguages = {}
+
+    def __call__(self):
+        """Return the babel call with correct options and settings"""
+        languages = sorted(self.otherlanguages.keys())
+        languages.append(self.language or 'english')
+        self.setup = [r'\usepackage[%s]{babel}' % ','.join(languages)]
+        # Deactivate "active characters"
+        shorthands = []
+        for c in ''.join(self.active_chars.get(lng, '') for lng in languages):
+            if c not in shorthands:
+                shorthands.append(c)
+        if shorthands:
+            self.setup.append(r'\AtBeginDocument{\shorthandoff{%s}}'
+                              % ''.join(shorthands))
+        # Including '~' in shorthandoff prevents its use as no-break space
+        if 'galician' in languages:
+            self.setup.append(r'\deactivatetilden % restore ~ in Galician')
+        if 'estonian' in languages:
+            self.setup.extend([r'\makeatletter',
+                               r'  \addto\extrasestonian{\bbl@deactivate{~}}',
+                               r'\makeatother'])
+        if 'basque' in languages:
+            self.setup.extend([r'\makeatletter',
+                               r'  \addto\extrasbasque{\bbl@deactivate{~}}',
+                               r'\makeatother'])
+        if (languages[-1] == 'english'
+            and 'french' in self.otherlanguages.keys()):
+            self.setup += ['% Prevent side-effects if French hyphenation '
+                           'patterns are not loaded:',
+                           r'\frenchbsetup{StandardLayout}',
+                           r'\AtBeginDocument{\selectlanguage{%s}'
+                           r'\noextrasfrench}' % self.language]
+        return '\n'.join(self.setup)
+
+    def language_name(self, language_code):
+        """Return TeX language name for `language_code`"""
+        for tag in utils.normalize_language_tag(language_code):
+            try:
+                return self.language_codes[tag]
+            except KeyError:
+                pass
+        if self.reporter is not None:
+            self.reporter.warning(self.warn_msg % language_code)
+        return ''
+
+    def get_language(self):
+        # Obsolete, kept for backwards compatibility with Sphinx
+        return self.language
+
+
+# Building blocks for the latex preamble
+# --------------------------------------
+
+class SortableDict(dict):
+    """Dictionary with additional sorting methods
+
+    Tip: use key starting with with '_' for sorting before small letters
+         and with '~' for sorting after small letters.
+    """
+    def sortedkeys(self):
+        """Return sorted list of keys"""
+        return sorted(self.keys())
+
+    def sortedvalues(self):
+        """Return list of values sorted by keys"""
+        return [self[key] for key in self.sortedkeys()]
+
+
+# PreambleCmds
+# `````````````
+# A container for LaTeX code snippets that can be
+# inserted into the preamble if required in the document.
+#
+# .. The package 'makecmds' would enable shorter definitions using the
+#    \providelength and \provideenvironment commands.
+#    However, it is pretty non-standard (texlive-latex-extra).
+
+class PreambleCmds:
+    """Building blocks for the latex preamble."""
+
+
+# Requirements and Setup
+
+PreambleCmds.color = r"""\usepackage{color}"""
+
+PreambleCmds.float = r"""\usepackage{float} % extended float configuration
+\floatplacement{figure}{H} % place figures here definitely"""
+
+PreambleCmds.linking = r"""%% hyperlinks:
+\ifthenelse{\isundefined{\hypersetup}}{
+  \usepackage[%s]{hyperref}
+  \usepackage{bookmark}
+  \urlstyle{same} %% normal text font (alternatives: tt, rm, sf)
+}{}"""
+
+PreambleCmds.minitoc = r"""%% local table of contents
+\usepackage{minitoc}"""
+
+PreambleCmds.table = r"""\usepackage{longtable,ltcaption,array}
+\setlength{\extrarowheight}{2pt}
+\newlength{\DUtablewidth} % internal use in tables"""
+
+PreambleCmds.table_columnwidth = (
+    r'\newcommand{\DUcolumnwidth}[1]'
+    r'{\dimexpr#1\DUtablewidth-2\tabcolsep\relax}')
+
+PreambleCmds.textcomp = r"""\usepackage{textcomp} % text symbol macros"""
+# TODO? Options [force,almostfull] prevent spurious error messages,
+# see de.comp.text.tex/2005-12/msg01855
+
+# backwards compatibility definitions
+
+PreambleCmds.abstract_legacy = r"""
+% abstract title
+\providecommand*{\DUtitleabstract}[1]{\centerline{\textbf{#1}}}"""
+
+# see https://sourceforge.net/p/docutils/bugs/339/
+PreambleCmds.admonition_legacy = r"""
+% admonition (specially marked topic)
+\providecommand{\DUadmonition}[2][class-arg]{%
+  % try \DUadmonition#1{#2}:
+  \ifcsname DUadmonition#1\endcsname%
+    \csname DUadmonition#1\endcsname{#2}%
+  \else
+    \begin{center}
+      \fbox{\parbox{0.9\linewidth}{#2}}
+    \end{center}
+  \fi
+}"""
+
+PreambleCmds.error_legacy = r"""
+% error admonition title
+\providecommand*{\DUtitleerror}[1]{\DUtitle{\color{red}#1}}"""
+
+PreambleCmds.title_legacy = r"""
+% title for topics, admonitions, unsupported section levels, and sidebar
+\providecommand*{\DUtitle}[2][class-arg]{%
+  % call \DUtitle#1{#2} if it exists:
+  \ifcsname DUtitle#1\endcsname%
+    \csname DUtitle#1\endcsname{#2}%
+  \else
+    \smallskip\noindent\textbf{#2}\smallskip%
+  \fi
+}"""
+
+PreambleCmds.toc_list = r"""
+\providecommand*{\DUCLASScontents}{%
+  \renewenvironment{itemize}%
+    {\begin{list}{}{\setlength{\partopsep}{0pt}
+                    \setlength{\parsep}{0pt}}
+                   }%
+    {\end{list}}%
+}"""
+
+PreambleCmds.ttem = r"""
+% character width in monospaced font
+\newlength{\ttemwidth}
+\settowidth{\ttemwidth}{\ttfamily M}"""
+
+## PreambleCmds.caption = r"""% configure caption layout
+## \usepackage{caption}
+## \captionsetup{singlelinecheck=false}% no exceptions for one-liners"""
+
+
+# Definitions from docutils.sty::
+
+def _read_block(fp):
+    block = [next(fp)]  # first line (empty)
+    for line in fp:
+        if not line.strip():
+            break
+        block.append(line)
+    return ''.join(block).rstrip()
+
+
+with open(LATEX_WRITER_DIR/'docutils.sty', encoding='utf-8') as fp:
+    for line in fp:
+        line = line.strip('% \n')
+        if not line.endswith('::'):
+            continue
+        block_name = line.rstrip(':')
+        if not block_name:
+            continue
+        definitions = _read_block(fp)
+        if block_name in ('color', 'float', 'table', 'textcomp'):
+            definitions = definitions.strip()
+        # print('Block: `%s`'% block_name)
+        # print(definitions)
+        setattr(PreambleCmds, block_name, definitions)
+
+
+# LaTeX encoding maps
+# -------------------
+# ::
+
+class CharMaps:
+    """LaTeX representations for active and Unicode characters."""
+
+    # characters that need escaping even in `alltt` environments:
+    alltt = {
+        ord('\\'): '\\textbackslash{}',
+        ord('{'): '\\{',
+        ord('}'): '\\}',
+    }
+    # characters that normally need escaping:
+    special = {
+        ord('#'): '\\#',
+        ord('$'): '\\$',
+        ord('%'): '\\%',
+        ord('&'): '\\&',
+        ord('~'): '\\textasciitilde{}',
+        ord('_'): '\\_',
+        ord('^'): '\\textasciicircum{}',
+        # straight double quotes are 'active' in many languages
+        ord('"'): '\\textquotedbl{}',
+        # Square brackets are ordinary chars and cannot be escaped with '\',
+        # so we put them in a group '{[}'. (Alternative: ensure that all
+        # macros with optional arguments are terminated with {} and text
+        # inside any optional argument is put in a group ``[{text}]``).
+        # Commands with optional args inside an optional arg must be put in a
+        # group, e.g. ``\item[{\hyperref[label]{text}}]``.
+        ord('['): '{[}',
+        ord(']'): '{]}',
+        # the soft hyphen is unknown in 8-bit text
+        # and not properly handled by XeTeX
+        0x00AD: '\\-',  # SOFT HYPHEN
+    }
+    # Unicode chars that are not recognized by LaTeX's utf8 encoding
+    unsupported_unicode = {
+        # TODO: ensure white space also at the beginning of a line?
+        # 0x00A0: '\\leavevmode\\nobreak\\vadjust{}~'
+        0x2000: '\\enskip',                        # EN QUAD
+        0x2001: '\\quad',                          # EM QUAD
+        0x2002: '\\enskip',                        # EN SPACE
+        0x2003: '\\quad',                          # EM SPACE
+        0x2008: '\\,',                             # PUNCTUATION SPACE
+        0x200b: '\\hspace{0pt}',                   # ZERO WIDTH SPACE
+        0x202F: '\\,',                             # NARROW NO-BREAK SPACE
+        # 0x02d8: '\\\u{ }',                       # BREVE
+        0x2011: '\\hbox{-}',                       # NON-BREAKING HYPHEN
+        0x212b: '\\AA',                            # ANGSTROM SIGN
+        0x21d4: '\\ensuremath{\\Leftrightarrow}',  # LEFT RIGHT DOUBLE ARROW
+        0x2260: '\\ensuremath{\\neq}',             # NOT EQUAL TO
+        0x2261: '\\ensuremath{\\equiv}',           # IDENTICAL TO
+        0x2264: '\\ensuremath{\\le}',              # LESS-THAN OR EQUAL TO
+        0x2265: '\\ensuremath{\\ge}',              # GREATER-THAN OR EQUAL TO
+        # Docutils footnote symbols:
+        0x2660: '\\ensuremath{\\spadesuit}',
+        0x2663: '\\ensuremath{\\clubsuit}',
+        0xfb00: 'ff',                              # LATIN SMALL LIGATURE FF
+        0xfb01: 'fi',                              # LATIN SMALL LIGATURE FI
+        0xfb02: 'fl',                              # LATIN SMALL LIGATURE FL
+        0xfb03: 'ffi',                             # LATIN SMALL LIGATURE FFI
+        0xfb04: 'ffl',                             # LATIN SMALL LIGATURE FFL
+    }
+    # Unicode chars that are recognized by LaTeX's utf8 encoding
+    utf8_supported_unicode = {
+        0x00A0: '~',                   # NO-BREAK SPACE
+        0x00AB: '\\guillemotleft{}',   # LEFT-POINTING DOUBLE ANGLE QUOTATION
+        0x00bb: '\\guillemotright{}',  # RIGHT-POINTING DOUBLE ANGLE QUOTATION
+        0x200C: '\\textcompwordmark{}',  # ZERO WIDTH NON-JOINER
+        0x2013: '\\textendash{}',
+        0x2014: '\\textemdash{}',
+        0x2018: '\\textquoteleft{}',
+        0x2019: '\\textquoteright{}',
+        0x201A: '\\quotesinglbase{}',    # SINGLE LOW-9 QUOTATION MARK
+        0x201C: '\\textquotedblleft{}',
+        0x201D: '\\textquotedblright{}',
+        0x201E: '\\quotedblbase{}',      # DOUBLE LOW-9 QUOTATION MARK
+        0x2030: '\\textperthousand{}',   # PER MILLE SIGN
+        0x2031: '\\textpertenthousand{}',  # PER TEN THOUSAND SIGN
+        0x2039: '\\guilsinglleft{}',
+        0x203A: '\\guilsinglright{}',
+        0x2423: '\\textvisiblespace{}',  # OPEN BOX
+        0x2020: '\\dag{}',
+        0x2021: '\\ddag{}',
+        0x2026: '\\dots{}',
+        0x2122: '\\texttrademark{}',
+    }
+    # recognized with 'utf8', if textcomp is loaded
+    textcomp = {
+        # Latin-1 Supplement
+        0x00a2: '\\textcent{}',              # ¢ CENT SIGN
+        0x00a4: '\\textcurrency{}',          # ¤ CURRENCY SYMBOL
+        0x00a5: '\\textyen{}',               # ¥ YEN SIGN
+        0x00a6: '\\textbrokenbar{}',         # ¦ BROKEN BAR
+        0x00a7: '\\textsection{}',           # § SECTION SIGN
+        0x00a8: '\\textasciidieresis{}',     # ¨ DIAERESIS
+        0x00a9: '\\textcopyright{}',         # © COPYRIGHT SIGN
+        0x00aa: '\\textordfeminine{}',       # ª FEMININE ORDINAL INDICATOR
+        0x00ac: '\\textlnot{}',              # ¬ NOT SIGN
+        0x00ae: '\\textregistered{}',        # ® REGISTERED SIGN
+        0x00af: '\\textasciimacron{}',       # ¯ MACRON
+        0x00b0: '\\textdegree{}',            # ° DEGREE SIGN
+        0x00b1: '\\textpm{}',                # ± PLUS-MINUS SIGN
+        0x00b2: '\\texttwosuperior{}',       # ² SUPERSCRIPT TWO
+        0x00b3: '\\textthreesuperior{}',     # ³ SUPERSCRIPT THREE
+        0x00b4: '\\textasciiacute{}',        # ´ ACUTE ACCENT
+        0x00b5: '\\textmu{}',                # µ MICRO SIGN
+        0x00b6: '\\textparagraph{}',         # ¶ PILCROW SIGN # != \textpilcrow
+        0x00b9: '\\textonesuperior{}',       # ¹ SUPERSCRIPT ONE
+        0x00ba: '\\textordmasculine{}',      # º MASCULINE ORDINAL INDICATOR
+        0x00bc: '\\textonequarter{}',        # 1/4 FRACTION
+        0x00bd: '\\textonehalf{}',           # 1/2 FRACTION
+        0x00be: '\\textthreequarters{}',     # 3/4 FRACTION
+        0x00d7: '\\texttimes{}',             # × MULTIPLICATION SIGN
+        0x00f7: '\\textdiv{}',               # ÷ DIVISION SIGN
+        # others
+        0x0192: '\\textflorin{}',            # LATIN SMALL LETTER F WITH HOOK
+        0x02b9: '\\textasciiacute{}',        # MODIFIER LETTER PRIME
+        0x02ba: '\\textacutedbl{}',          # MODIFIER LETTER DOUBLE PRIME
+        0x2016: '\\textbardbl{}',            # DOUBLE VERTICAL LINE
+        0x2022: '\\textbullet{}',            # BULLET
+        0x2032: '\\textasciiacute{}',        # PRIME
+        0x2033: '\\textacutedbl{}',          # DOUBLE PRIME
+        0x2035: '\\textasciigrave{}',        # REVERSED PRIME
+        0x2036: '\\textgravedbl{}',          # REVERSED DOUBLE PRIME
+        0x203b: '\\textreferencemark{}',     # REFERENCE MARK
+        0x203d: '\\textinterrobang{}',       # INTERROBANG
+        0x2044: '\\textfractionsolidus{}',   # FRACTION SLASH
+        0x2045: '\\textlquill{}',            # LEFT SQUARE BRACKET WITH QUILL
+        0x2046: '\\textrquill{}',            # RIGHT SQUARE BRACKET WITH QUILL
+        0x2052: '\\textdiscount{}',          # COMMERCIAL MINUS SIGN
+        0x20a1: '\\textcolonmonetary{}',     # COLON SIGN
+        0x20a3: '\\textfrenchfranc{}',       # FRENCH FRANC SIGN
+        0x20a4: '\\textlira{}',              # LIRA SIGN
+        0x20a6: '\\textnaira{}',             # NAIRA SIGN
+        0x20a9: '\\textwon{}',               # WON SIGN
+        0x20ab: '\\textdong{}',              # DONG SIGN
+        0x20ac: '\\texteuro{}',              # EURO SIGN
+        0x20b1: '\\textpeso{}',              # PESO SIGN
+        0x20b2: '\\textguarani{}',           # GUARANI SIGN
+        0x2103: '\\textcelsius{}',           # DEGREE CELSIUS
+        0x2116: '\\textnumero{}',            # NUMERO SIGN
+        0x2117: '\\textcircledP{}',          # SOUND RECORDING COPYRIGHT
+        0x211e: '\\textrecipe{}',            # PRESCRIPTION TAKE
+        0x2120: '\\textservicemark{}',       # SERVICE MARK
+        0x2122: '\\texttrademark{}',         # TRADE MARK SIGN
+        0x2126: '\\textohm{}',               # OHM SIGN
+        0x2127: '\\textmho{}',               # INVERTED OHM SIGN
+        0x212e: '\\textestimated{}',         # ESTIMATED SYMBOL
+        0x2190: '\\textleftarrow{}',         # LEFTWARDS ARROW
+        0x2191: '\\textuparrow{}',           # UPWARDS ARROW
+        0x2192: '\\textrightarrow{}',        # RIGHTWARDS ARROW
+        0x2193: '\\textdownarrow{}',         # DOWNWARDS ARROW
+        0x2212: '\\textminus{}',             # MINUS SIGN
+        0x2217: '\\textasteriskcentered{}',  # ASTERISK OPERATOR
+        0x221a: '\\textsurd{}',              # SQUARE ROOT
+        0x2422: '\\textblank{}',             # BLANK SYMBOL
+        0x25e6: '\\textopenbullet{}',        # WHITE BULLET
+        0x25ef: '\\textbigcircle{}',         # LARGE CIRCLE
+        0x266a: '\\textmusicalnote{}',       # EIGHTH NOTE
+        0x26ad: '\\textmarried{}',           # MARRIAGE SYMBOL
+        0x26ae: '\\textdivorced{}',          # DIVORCE SYMBOL
+        0x27e8: '\\textlangle{}',            # MATHEMATICAL LEFT ANGLE BRACKET
+        0x27e9: '\\textrangle{}',            # MATHEMATICAL RIGHT ANGLE BRACKET
+    }
+    # Unicode chars that require a feature/package to render
+    pifont = {
+        0x2665: '\\ding{170}',               # black heartsuit
+        0x2666: '\\ding{169}',               # black diamondsuit
+        0x2713: '\\ding{51}',                # check mark
+        0x2717: '\\ding{55}',                # check mark
+    }
+    # TODO: greek alphabet ... ?
+    # see also LaTeX codec
+    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252124
+    # and unimap.py from TeXML
+
+
+class DocumentClass:
+    """Details of a LaTeX document class."""
+
+    def __init__(self, document_class, with_part=False):
+        self.document_class = document_class
+        self._with_part = with_part
+        self.sections = ['section', 'subsection', 'subsubsection',
+                         'paragraph', 'subparagraph']
+        if self.document_class in ('book', 'memoir', 'report',
+                                   'scrbook', 'scrreprt'):
+            self.sections.insert(0, 'chapter')
+        if self._with_part:
+            self.sections.insert(0, 'part')
+
+    def section(self, level):
+        """Return the LaTeX section name for section `level`.
+
+        The name depends on the specific document class.
+        Level is 1,2,3..., as level 0 is the title.
+        """
+        if level <= len(self.sections):
+            return self.sections[level-1]
+        # unsupported levels
+        return 'DUtitle'
+
+    def latex_section_depth(self, depth):
+        """
+        Return LaTeX equivalent of Docutils section level `depth`.
+
+        Given the value of the ``:depth:`` option of the "contents" or
+        "sectnum" directive, return the corresponding value for the
+        LaTeX ``tocdepth`` or ``secnumdepth`` counters.
+        """
+        depth = min(depth, len(self.sections))  # limit to supported levels
+        if 'chapter' in self.sections:
+            depth -= 1
+        if self.sections[0] == 'part':
+            depth -= 1
+        return depth
+
+
+class Table:
+    """Manage a table while traversing.
+
+    Table style might be
+
+    :standard:   horizontal and vertical lines
+    :booktabs:   only horizontal lines (requires "booktabs" LaTeX package)
+    :borderless: no borders around table cells
+    :nolines:    alias for borderless
+
+    :colwidths-auto:  column widths determined by LaTeX
+    """
+    def __init__(self, translator, latex_type):
+        self._translator = translator
+        self._latex_type = latex_type
+        self.legacy_column_widths = False
+
+        self.close()
+        self._colwidths = []
+        self._rowspan = []
+        self._in_thead = 0
+
+    def open(self):
+        self._open = True
+        self._col_specs = []
+        self.caption = []
+        self._attrs = {}
+        self._in_head = False  # maybe context with search
+
+    def close(self):
+        self._open = False
+        self._col_specs = None
+        self.caption = []
+        self._attrs = {}
+        self.stubs = []
+        self.colwidths_auto = False
+
+    def is_open(self):
+        return self._open
+
+    def set_table_style(self, node, settings):
+        self.legacy_column_widths = settings.legacy_column_widths
+        if 'align' in node:
+            self.set('align', node['align'])
+        # TODO: elif 'align' in classes/settings.table-style:
+        #           self.set('align', ...)
+        borders = [cls.replace('nolines', 'borderless')
+                   for cls in (['standard']
+                               + settings.table_style
+                               + node['classes'])
+                   if cls in ('standard', 'booktabs', 'borderless', 'nolines')]
+        self.borders = borders[-1]
+        self.colwidths_auto = (('colwidths-auto' in node['classes']
+                                or 'colwidths-auto' in settings.table_style)
+                               and 'colwidths-given' not in node['classes']
+                               and 'width' not in node)
+
+    def get_latex_type(self):
+        if self._latex_type == 'longtable' and not self.caption:
+            # do not advance the "table" counter (requires "ltcaption" package)
+            return 'longtable*'
+        return self._latex_type
+
+    def set(self, attr, value):
+        self._attrs[attr] = value
+
+    def get(self, attr):
+        if attr in self._attrs:
+            return self._attrs[attr]
+        return None
+
+    def get_vertical_bar(self):
+        if self.borders == 'standard':
+            return '|'
+        return ''
+
+    def get_opening(self, width=r'\linewidth'):
+        align_map = {'left': '[l]',
+                     'center': '[c]',
+                     'right': '[r]',
+                     None: ''}
+        align = align_map.get(self.get('align'))
+        latex_type = self.get_latex_type()
+        if align and latex_type not in ("longtable", "longtable*"):
+            opening = [r'\noindent\makebox[\linewidth]%s{%%' % (align,),
+                       r'\begin{%s}' % (latex_type,)]
+        else:
+            opening = [r'\begin{%s}%s' % (latex_type, align)]
+        if not self.colwidths_auto:
+            if self.borders == 'standard' and not self.legacy_column_widths:
+                opening.insert(-1, r'\setlength{\DUtablewidth}'
+                               r'{\dimexpr%s-%i\arrayrulewidth\relax}%%'
+                               % (width, len(self._col_specs)+1))
+            else:
+                opening.insert(-1, r'\setlength{\DUtablewidth}{%s}%%' % width)
+        return '\n'.join(opening)
+
+    def get_closing(self):
+        closing = []
+        if self.borders == 'booktabs':
+            closing.append(r'\bottomrule')
+        # elif self.borders == 'standard':
+        #     closing.append(r'\hline')
+        closing.append(r'\end{%s}' % self.get_latex_type())
+        if (self.get('align')
+            and self.get_latex_type() not in ("longtable", "longtable*")):
+            closing.append('}')
+        return '\n'.join(closing)
+
+    def visit_colspec(self, node):
+        self._col_specs.append(node)
+        # "stubs" list is an attribute of the tgroup element:
+        self.stubs.append(node.attributes.get('stub'))
+
+    def get_colspecs(self, node):
+        """Return column specification for longtable.
+        """
+        bar = self.get_vertical_bar()
+        self._rowspan = [0] * len(self._col_specs)
+        if self.colwidths_auto:
+            self._colwidths = []
+            latex_colspecs = ['l'] * len(self._col_specs)
+        elif self.legacy_column_widths:
+            # use old algorithm for backwards compatibility
+            width = 80  # assumed standard line length
+            factor = 0.93  # do not make it full linewidth
+            # first see if we get too wide.
+            total_width = sum(node['colwidth']+1 for node in self._col_specs)
+            if total_width > width:
+                factor *= width / total_width
+            self._colwidths = [(factor * (node['colwidth']+1)/width)
+                               + 0.005 for node in self._col_specs]
+            latex_colspecs = ['p{%.3f\\DUtablewidth}' % colwidth
+                              for colwidth in self._colwidths]
+        else:
+            # No of characters corresponding to table width = 100%
+            #   Characters/line with LaTeX article, A4, Times, default margins
+            #   depends on character:  M: 40, A: 50, x: 70, i: 120.
+            norm_length = 40
+            # Allowance to prevent unpadded columns like
+            #   === ==
+            #   ABC DE
+            #   === ==
+            # getting too narrow:
+            if 'colwidths-given' not in node.parent.parent['classes']:
+                allowance = 1
+            else:
+                allowance = 0  # "widths" option specified, use exact ratio
+            self._colwidths = [(node['colwidth']+allowance)/norm_length
+                               for node in self._col_specs]
+            total_width = sum(self._colwidths)
+            # Limit to 100%, force 100% if table width is specified:
+            if total_width > 1 or 'width' in node.parent.parent.attributes:
+                self._colwidths = [colwidth/total_width
+                                   for colwidth in self._colwidths]
+            latex_colspecs = ['p{\\DUcolumnwidth{%.3f}}' % colwidth
+                              for colwidth in self._colwidths]
+        return bar + bar.join(latex_colspecs) + bar
+
+    def get_column_width(self):
+        """Return columnwidth for current cell (not multicell)."""
+        try:
+            if self.legacy_column_widths:
+                return '%.2f\\DUtablewidth'%self._colwidths[self._cell_in_row]
+            return '\\DUcolumnwidth{%.2f}'%self._colwidths[self._cell_in_row]
+        except IndexError:
+            return '*'
+
+    def get_multicolumn_width(self, start, len_):
+        """Return sum of columnwidths for multicell."""
+        try:
+            multicol_width = sum(self._colwidths[start + co]
+                                 for co in range(len_))
+            if self.legacy_column_widths:
+                return 'p{%.2f\\DUtablewidth}' % multicol_width
+            return 'p{\\DUcolumnwidth{%.3f}}' % multicol_width
+        except IndexError:
+            return 'l'
+
+    def get_caption(self):
+        """Deprecated. Will be removed in Docutils 0.22."""
+        warnings.warn('`writers.latex2e.Table.get_caption()` is obsolete'
+                      ' and will be removed in Docutils 0.22.',
+                      DeprecationWarning, stacklevel=2)
+
+        if not self.caption:
+            return ''
+        caption = ''.join(self.caption)
+        if 1 == self._translator.thead_depth():
+            return r'\caption{%s}\\' '\n' % caption
+        return r'\caption[]{%s (... continued)}\\' '\n' % caption
+
+    def need_recurse(self):
+        if self._latex_type == 'longtable':
+            return 1 == self._translator.thead_depth()
+        return 0
+
+    def visit_thead(self):
+        self._in_thead += 1
+        if self.borders == 'standard':
+            return ['\\hline\n']
+        elif self.borders == 'booktabs':
+            return ['\\toprule\n']
+        return []
+
+    def depart_thead(self):
+        a = []
+        ## if self.borders == 'standard':
+        ##     a.append('\\hline\n')
+        if self.borders == 'booktabs':
+            a.append('\\midrule\n')
+        if self._latex_type == 'longtable':
+            if 1 == self._translator.thead_depth():
+                a.append('\\endfirsthead\n')
+            else:
+                n_c = len(self._col_specs)
+                a.append('\\endhead\n')
+                # footer on all but last page (if it fits):
+                twidth = sum(node['colwidth']+2 for node in self._col_specs)
+                if twidth > 30 or (twidth > 12 and not self.colwidths_auto):
+                    a.append(r'\multicolumn{%d}{%s}'
+                             % (n_c, self.get_multicolumn_width(0, n_c))
+                             + r'{\raggedleft\ldots continued on next page}\\'
+                             + '\n')
+                a.append('\\endfoot\n\\endlastfoot\n')
+            # for longtable one could add firsthead, foot and lastfoot
+        self._in_thead -= 1
+        return a
+
+    def visit_row(self):
+        self._cell_in_row = 0
+
+    def depart_row(self):
+        res = [' \\\\\n']
+        self._cell_in_row = None  # remove cell counter
+        for i in range(len(self._rowspan)):
+            if self._rowspan[i] > 0:
+                self._rowspan[i] -= 1
+
+        if self.borders == 'standard':
+            rowspans = [i+1 for i in range(len(self._rowspan))
+                        if self._rowspan[i] <= 0]
+            if len(rowspans) == len(self._rowspan):
+                res.append('\\hline\n')
+            else:
+                cline = ''
+                rowspans.reverse()
+                # TODO merge clines
+                while True:
+                    try:
+                        c_start = rowspans.pop()
+                    except IndexError:
+                        break
+                    cline += '\\cline{%d-%d}\n' % (c_start, c_start)
+                res.append(cline)
+        return res
+
+    def set_rowspan(self, cell, value):
+        try:
+            self._rowspan[cell] = value
+        except IndexError:
+            pass
+
+    def get_rowspan(self, cell):
+        try:
+            return self._rowspan[cell]
+        except IndexError:
+            return 0
+
+    def get_entry_number(self):
+        return self._cell_in_row
+
+    def visit_entry(self):
+        self._cell_in_row += 1
+
+    def is_stub_column(self):
+        if len(self.stubs) >= self._cell_in_row:
+            return self.stubs[self._cell_in_row]
+        return False
+
+
+class LaTeXTranslator(nodes.NodeVisitor):
+    """
+    Generate code for 8-bit LaTeX from a Docutils document tree.
+
+    See the docstring of docutils.writers._html_base.HTMLTranslator for
+    notes on and examples of safe subclassing.
+    """
+
+    # When options are given to the documentclass, latex will pass them
+    # to other packages, as done with babel.
+    # Dummy settings might be taken from document settings
+
+    # Generate code for typesetting with 8-bit latex/pdflatex vs.
+    # xelatex/lualatex engine. Overwritten by the XeTeX writer
+    is_xetex = False
+
+    # Config setting defaults
+    # -----------------------
+
+    # TODO: use mixins for different implementations.
+    # list environment for docinfo. else tabularx
+    ## use_optionlist_for_docinfo = False # TODO: NOT YET IN USE
+
+    # Use compound enumerations (1.A.1.)
+    compound_enumerators = False
+
+    # If using compound enumerations, include section information.
+    section_prefix_for_enumerators = False
+
+    # This is the character that separates the section ("." subsection ...)
+    # prefix from the regular list enumerator.
+    section_enumerator_separator = '-'
+
+    # Auxiliary variables
+    # -------------------
+
+    has_latex_toc = False  # is there a toc in the doc? (needed by minitoc)
+    section_level = 0
+
+    # Flags to encode():
+    # inside citation reference labels underscores dont need to be escaped
+    inside_citation_reference_label = False
+    verbatim = False                    # do not encode
+    insert_non_breaking_blanks = False  # replace blanks by "~"
+    insert_newline = False              # add latex newline commands
+    literal = False                     # literal text (block or inline)
+    alltt = False                       # inside `alltt` environment
+
+    def __init__(self, document, babel_class=Babel):
+        super().__init__(document)
+        # Reporter
+        # ~~~~~~~~
+        self.warn = self.document.reporter.warning
+        self.error = self.document.reporter.error
+
+        # Settings
+        # ~~~~~~~~
+        self.settings = settings = document.settings
+        # warn of deprecated settings and changing defaults:
+        if settings.use_latex_citations is None and not settings.use_bibtex:
+            settings.use_latex_citations = False
+            warnings.warn('The default for the setting "use_latex_citations" '
+                          'will change to "True" in Docutils 1.0.',
+                          FutureWarning, stacklevel=7)
+        if settings.legacy_column_widths is None:
+            settings.legacy_column_widths = True
+            warnings.warn('The default for the setting "legacy_column_widths" '
+                          'will change to "False" in Docutils 1.0.)',
+                          FutureWarning, stacklevel=7)
+        if settings.use_verbatim_when_possible is not None:
+            warnings.warn(
+                'The configuration setting "use_verbatim_when_possible" '
+                'will be removed in Docutils 2.0. '
+                'Use "literal_block_env: verbatim".',
+                FutureWarning, stacklevel=7)
+
+        self.latex_encoding = self.to_latex_encoding(settings.output_encoding)
+        self.use_latex_toc = settings.use_latex_toc
+        self.use_latex_docinfo = settings.use_latex_docinfo
+        self.use_latex_citations = settings.use_latex_citations
+        self.reference_label = settings.reference_label
+        self.hyperlink_color = settings.hyperlink_color
+        self.compound_enumerators = settings.compound_enumerators
+        self.font_encoding = getattr(settings, 'font_encoding', '')
+        self.section_prefix_for_enumerators = (
+            settings.section_prefix_for_enumerators)
+        self.section_enumerator_separator = (
+            settings.section_enumerator_separator.replace('_', r'\_'))
+        # literal blocks:
+        self.literal_block_env = ''
+        self.literal_block_options = ''
+        if settings.literal_block_env:
+            (none,
+             self.literal_block_env,
+             self.literal_block_options,
+             none) = re.split(r'(\w+)(.*)', settings.literal_block_env)
+        elif settings.use_verbatim_when_possible:
+            self.literal_block_env = 'verbatim'
+
+        if settings.use_bibtex:
+            self.use_latex_citations = True
+        self.bibtex = settings.use_bibtex
+        # language module for Docutils-generated text
+        # (labels, bibliographic_fields, and author_separators)
+        self.language_module = languages.get_language(settings.language_code,
+                                                      document.reporter)
+        self.babel = babel_class(settings.language_code, document.reporter)
+        self.author_separator = self.language_module.author_separators[0]
+        d_options = [settings.documentoptions]
+        if self.babel.language not in ('english', ''):
+            d_options.append(self.babel.language)
+        self.documentoptions = ','.join(filter(None, d_options))
+        self.d_class = DocumentClass(settings.documentclass,
+                                     settings.use_part_section)
+        # graphic package options:
+        if settings.graphicx_option == '':
+            self.graphicx_package = r'\usepackage{graphicx}'
+        else:
+            self.graphicx_package = (r'\usepackage[%s]{graphicx}' %
+                                     settings.graphicx_option)
+        # footnotes: TODO: implement LaTeX footnotes
+        self.docutils_footnotes = settings.docutils_footnotes
+
+        # Output collection stacks
+        # ~~~~~~~~~~~~~~~~~~~~~~~~
+
+        # Document parts
+        self.head_prefix = [r'\documentclass[%s]{%s}' %
+                            (self.documentoptions,
+                             settings.documentclass)]
+        self.requirements = SortableDict()  # made a list in depart_document()
+        self.requirements['__static'] = r'\usepackage{ifthen}'
+        self.latex_preamble = [settings.latex_preamble]
+        self.fallbacks = SortableDict()  # made a list in depart_document()
+        self.pdfsetup = []  # PDF properties (hyperref package)
+        self.title = []
+        self.subtitle = []
+        self.titledata = []  # \title, \author, \date
+        ## self.body_prefix = ['\\begin{document}\n']
+        self.body_pre_docinfo = []  # \maketitle
+        self.docinfo = []
+        self.dedication = []
+        self.abstract = []
+        self.body = []
+        ## self.body_suffix = ['\\end{document}\n']
+
+        self.context = []
+        """Heterogeneous stack.
+
+        Used by visit_* and depart_* functions in conjunction with the tree
+        traversal. Make sure that the pops correspond to the pushes."""
+
+        # Title metadata:
+        self.title_labels = []
+        self.subtitle_labels = []
+        # (if use_latex_docinfo: collects lists of
+        # author/organization/contact/address lines)
+        self.author_stack = []
+        self.date = []
+
+        # PDF properties: pdftitle, pdfauthor
+        self.pdfauthor = []
+        self.pdfinfo = []
+        if settings.language_code != 'en':
+            self.pdfinfo.append('  pdflang={%s},'%settings.language_code)
+
+        # Stack of section counters so that we don't have to use_latex_toc.
+        # This will grow and shrink as processing occurs.
+        # Initialized for potential first-level sections.
+        self._section_number = [0]
+
+        # The current stack of enumerations so that we can expand
+        # them into a compound enumeration.
+        self._enumeration_counters = []
+        # The maximum number of enumeration counters we've used.
+        # If we go beyond this number, we need to create a new
+        # counter; otherwise, just reuse an old one.
+        self._max_enumeration_counters = 0
+
+        self._bibitems = []
+
+        # object for a table while processing.
+        self.table_stack = []
+        self.active_table = Table(self, 'longtable')
+
+        # Where to collect the output of visitor methods (default: body)
+        self.out = self.body
+        self.out_stack = []  # stack of output collectors
+
+        # Process settings
+        # ~~~~~~~~~~~~~~~~
+        # Encodings:
+        # Docutils' output-encoding => TeX input encoding
+        if self.latex_encoding not in ('ascii', 'unicode', 'utf8'):
+            self.requirements['_inputenc'] = (r'\usepackage[%s]{inputenc}'
+                                              % self.latex_encoding)
+        # TeX font encoding
+        if not self.is_xetex:
+            if self.font_encoding:
+                self.requirements['_fontenc'] = (r'\usepackage[%s]{fontenc}' %
+                                                 self.font_encoding)
+            # ensure \textquotedbl is defined:
+            for enc in self.font_encoding.split(','):
+                enc = enc.strip()
+                if enc == 'OT1':
+                    self.requirements['_textquotedblOT1'] = (
+                        r'\DeclareTextSymbol{\textquotedbl}{OT1}{`\"}')
+                elif enc not in ('T1', 'T2A', 'T2B', 'T2C', 'T4', 'T5'):
+                    self.requirements['_textquotedbl'] = (
+                        r'\DeclareTextSymbolDefault{\textquotedbl}{T1}')
+        # page layout with typearea (if there are relevant document options)
+        if (settings.documentclass.find('scr') == -1
+            and (self.documentoptions.find('DIV') != -1
+                 or self.documentoptions.find('BCOR') != -1)):
+            self.requirements['typearea'] = r'\usepackage{typearea}'
+
+        # Stylesheets
+        # (the name `self.stylesheet` is singular because only one
+        # stylesheet was supported before Docutils 0.6).
+        stylesheet_list = utils.get_stylesheet_list(settings)
+        self.fallback_stylesheet = 'docutils' in stylesheet_list
+        if self.fallback_stylesheet:
+            stylesheet_list.remove('docutils')
+            if settings.legacy_class_functions:
+                # docutils.sty is incompatible with legacy functions
+                self.fallback_stylesheet = False
+            else:
+                # require a minimal version:
+                self.fallbacks['docutils.sty'] = (
+                    r'\usepackage{docutils}[2020/08/28]')
+
+        self.stylesheet = [self.stylesheet_call(path)
+                           for path in stylesheet_list]
+
+        # PDF setup
+        if self.hyperlink_color.lower() in ('0', 'false', ''):
+            self.hyperref_options = ''
+        else:
+            self.hyperref_options = ('colorlinks=true,'
+                                     f'linkcolor={self.hyperlink_color},'
+                                     f'urlcolor={self.hyperlink_color}')
+        if settings.hyperref_options:
+            self.hyperref_options += ',' + settings.hyperref_options
+
+        # LaTeX Toc
+        # include all supported sections in toc and PDF bookmarks
+        # (or use documentclass-default (as currently))?
+
+        # Section numbering
+        if settings.sectnum_xform:  # section numbering by Docutils
+            PreambleCmds.secnumdepth = r'\setcounter{secnumdepth}{0}'
+        else:  # section numbering by LaTeX:
+            secnumdepth = settings.sectnum_depth
+            # Possible values of settings.sectnum_depth:
+            # None  "sectnum" directive without depth arg -> LaTeX default
+            #  0    no "sectnum" directive -> no section numbers
+            # >0    value of "depth" argument -> translate to LaTeX levels:
+            #       -1  part    (0 with "article" document class)
+            #        0  chapter (missing in "article" document class)
+            #        1  section
+            #        2  subsection
+            #        3  subsubsection
+            #        4  paragraph
+            #        5  subparagraph
+            if secnumdepth is not None:
+                PreambleCmds.secnumdepth = (
+                    r'\setcounter{secnumdepth}{%d}'
+                    % self.d_class.latex_section_depth(secnumdepth))
+            # start with specified number:
+            if (hasattr(settings, 'sectnum_start')
+                and settings.sectnum_start != 1):
+                self.requirements['sectnum_start'] = (
+                    r'\setcounter{%s}{%d}' % (self.d_class.sections[0],
+                                              settings.sectnum_start-1))
+            # TODO: currently ignored (configure in a stylesheet):
+            ## settings.sectnum_prefix
+            ## settings.sectnum_suffix
+
+    # Auxiliary Methods
+    # -----------------
+
+    def stylesheet_call(self, path):
+        """Return code to reference or embed stylesheet file `path`"""
+        path = Path(path)
+        # is it a package (no extension or *.sty) or "normal" tex code:
+        is_package = path.suffix in ('.sty', '')
+        # Embed content of style file:
+        if self.settings.embed_stylesheet:
+            if is_package:
+                path = path.with_suffix('.sty')  # ensure extension
+            try:
+                content = path.read_text(encoding='utf-8')
+            except OSError as err:
+                msg = f'Cannot embed stylesheet:\n {err}'.replace('\\\\', '/')
+                self.document.reporter.error(msg)
+                return '% ' + msg.replace('\n', '\n% ')
+            else:
+                self.settings.record_dependencies.add(path.as_posix())
+            if is_package:
+                # allow '@' in macro names:
+                content = (f'\\makeatletter\n{content}\n\\makeatother')
+            return (f'% embedded stylesheet: {path.as_posix()}\n'
+                    f'{content}')
+        # Link to style file:
+        if is_package:
+            path = path.parent / path.stem  # drop extension
+            cmd = r'\usepackage{%s}'
+        else:
+            cmd = r'\input{%s}'
+        if self.settings.stylesheet_path:
+            # adapt path relative to output (cf. config.html#stylesheet-path)
+            return cmd % utils.relative_path(self.settings._destination, path)
+        return cmd % path.as_posix()
+
+    def to_latex_encoding(self, docutils_encoding):
+        """Translate docutils encoding name into LaTeX's.
+
+        Default method is remove "-" and "_" chars from docutils_encoding.
+        """
+        tr = {'iso-8859-1': 'latin1',     # west european
+              'iso-8859-2': 'latin2',     # east european
+              'iso-8859-3': 'latin3',     # esperanto, maltese
+              'iso-8859-4': 'latin4',     # north european
+              'iso-8859-5': 'iso88595',   # cyrillic (ISO)
+              'iso-8859-9': 'latin5',     # turkish
+              'iso-8859-15': 'latin9',    # latin9, update to latin1.
+              'mac_cyrillic': 'maccyr',   # cyrillic (on Mac)
+              'windows-1251': 'cp1251',   # cyrillic (on Windows)
+              'koi8-r': 'koi8-r',         # cyrillic (Russian)
+              'koi8-u': 'koi8-u',         # cyrillic (Ukrainian)
+              'windows-1250': 'cp1250',   #
+              'windows-1252': 'cp1252',   #
+              'us-ascii': 'ascii',        # ASCII (US)
+              # unmatched encodings
+              # '': 'applemac',
+              # '': 'ansinew',  # windows 3.1 ansi
+              # '': 'ascii',    # ASCII encoding for the range 32--127.
+              # '': 'cp437',    # dos latin us
+              # '': 'cp850',    # dos latin 1
+              # '': 'cp852',    # dos latin 2
+              # '': 'decmulti',
+              # '': 'latin10',
+              # 'iso-8859-6': ''   # arabic
+              # 'iso-8859-7': ''   # greek
+              # 'iso-8859-8': ''   # hebrew
+              # 'iso-8859-10': ''  # latin6, more complete iso-8859-4
+              }
+        encoding = docutils_encoding.lower()  # normalize case
+        encoding = encoding.split(':')[0]     # strip the error handler
+        if encoding in tr:
+            return tr[encoding]
+        # drop HYPHEN or LOW LINE from "latin_1", "utf-8" and similar
+        return encoding.replace('_', '').replace('-', '')
+
+    def language_label(self, docutil_label):
+        return self.language_module.labels[docutil_label]
+
+    def encode(self, text):
+        """Return text with 'problematic' characters escaped.
+
+        * Escape the special printing characters ``# $ % & ~ _ ^ \\ { }``,
+          square brackets ``[ ]``, double quotes and (in OT1) ``< | >``.
+        * Translate non-supported Unicode characters.
+        * Separate ``-`` (and more in literal text) to prevent input ligatures.
+        """
+        if self.verbatim:
+            return text
+        # Set up the translation table:
+        table = CharMaps.alltt.copy()
+        if not self.alltt:
+            table.update(CharMaps.special)
+        # keep the underscore in citation references
+        if self.inside_citation_reference_label and not self.alltt:
+            del table[ord('_')]
+        # Workarounds for OT1 font-encoding
+        if self.font_encoding in ['OT1', ''] and not self.is_xetex:
+            # * out-of-order characters in cmtt
+            if self.literal:
+                # replace underscore by underlined blank,
+                # because this has correct width.
+                table[ord('_')] = '\\underline{~}'
+                # the backslash doesn't work, so we use a mirrored slash.
+                # \reflectbox is provided by graphicx:
+                self.requirements['graphicx'] = self.graphicx_package
+                table[ord('\\')] = '\\reflectbox{/}'
+            # * ``< | >`` come out as different chars (except for cmtt):
+            else:
+                table[ord('|')] = '\\textbar{}'
+                table[ord('<')] = '\\textless{}'
+                table[ord('>')] = '\\textgreater{}'
+        if self.insert_non_breaking_blanks:
+            table[ord(' ')] = '~'
+            # tab chars may occur in included files (literal or code)
+            # quick-and-dirty replacement with spaces
+            # (for better results use `--literal-block-env=lstlisting`)
+            table[ord('\t')] = '~' * self.settings.tab_width
+        # Unicode replacements for 8-bit tex engines (not required with XeTeX)
+        if not self.is_xetex:
+            if not self.latex_encoding.startswith('utf8'):
+                table.update(CharMaps.unsupported_unicode)
+                table.update(CharMaps.utf8_supported_unicode)
+                table.update(CharMaps.textcomp)
+            table.update(CharMaps.pifont)
+            # Characters that require a feature/package to render
+            for ch in text:
+                cp = ord(ch)
+                if cp in CharMaps.textcomp and not self.fallback_stylesheet:
+                    self.requirements['textcomp'] = PreambleCmds.textcomp
+                elif cp in CharMaps.pifont:
+                    self.requirements['pifont'] = '\\usepackage{pifont}'
+                # preamble-definitions for unsupported Unicode characters
+                elif (self.latex_encoding == 'utf8'
+                      and cp in CharMaps.unsupported_unicode):
+                    self.requirements['_inputenc'+str(cp)] = (
+                        '\\DeclareUnicodeCharacter{%04X}{%s}'
+                        % (cp, CharMaps.unsupported_unicode[cp]))
+        text = text.translate(table)
+
+        # Break up input ligatures e.g. '--' to '-{}-'.
+        if not self.is_xetex:  # Not required with xetex/luatex
+            separate_chars = '-'
+            # In monospace-font, we also separate ',,', '``' and "''" and some
+            # other characters which can't occur in non-literal text.
+            if self.literal:
+                separate_chars += ',`\'"<>'
+            for char in separate_chars * 2:
+                # Do it twice ("* 2") because otherwise we would replace
+                # '---' by '-{}--'.
+                text = text.replace(char + char, char + '{}' + char)
+
+        # Literal line breaks (in address or literal blocks):
+        if self.insert_newline:
+            lines = text.split('\n')
+            # Add a protected space to blank lines (except the last)
+            # to avoid ``! LaTeX Error: There's no line here to end.``
+            for i, line in enumerate(lines[:-1]):
+                if not line.lstrip():
+                    lines[i] += '~'
+            text = (r'\\' + '\n').join(lines)
+        if self.literal and not self.insert_non_breaking_blanks:
+            # preserve runs of spaces but allow wrapping
+            text = text.replace('  ', ' ~')
+        return text
+
+    def attval(self, text,
+               whitespace=re.compile('[\n\r\t\v\f]')):
+        """Cleanse, encode, and return attribute value text."""
+        return self.encode(whitespace.sub(' ', text))
+
+    # TODO: is this used anywhere? -> update (use template) or delete
+    ## def astext(self):
+    ##     """Assemble document parts and return as string."""
+    ##     head = '\n'.join(self.head_prefix + self.stylesheet + self.head)
+    ##     body = ''.join(self.body_prefix  + self.body + self.body_suffix)
+    ##     return head + '\n' + body
+
+    def is_inline(self, node):
+        """Check whether a node represents an inline or block-level element"""
+        return isinstance(node.parent, nodes.TextElement)
+
+    def append_hypertargets(self, node):
+        """Append hypertargets for all ids of `node`"""
+        # hypertarget places the anchor at the target's baseline,
+        # so we raise it explicitly
+        self.out.append('%\n'.join('\\raisebox{1em}{\\hypertarget{%s}{}}' %
+                                   id for id in node['ids']))
+
+    def ids_to_labels(self, node, set_anchor=True, protect=False,
+                      newline=False):
+        """Return list of label definitions for all ids of `node`
+
+        If `set_anchor` is True, an anchor is set with \\phantomsection.
+        If `protect` is True, the \\label cmd is made robust.
+        If `newline` is True, a newline is added if there are labels.
+        """
+        prefix = '\\protect' if protect else ''
+        labels = [prefix + '\\label{%s}' % id for id in node['ids']]
+        if set_anchor and labels:
+            labels.insert(0, '\\phantomsection')
+        if newline and labels:
+            labels.append('\n')
+        return labels
+
+    def set_align_from_classes(self, node):
+        """Convert ``align-*`` class arguments into alignment args."""
+        # separate:
+        align = [cls for cls in node['classes'] if cls.startswith('align-')]
+        if align:
+            node['align'] = align[-1].replace('align-', '')
+            node['classes'] = [cls for cls in node['classes']
+                               if not cls.startswith('align-')]
+
+    def insert_align_declaration(self, node, default=None):
+        align = node.get('align', default)
+        if align == 'left':
+            self.out.append('\\raggedright\n')
+        elif align == 'center':
+            self.out.append('\\centering\n')
+        elif align == 'right':
+            self.out.append('\\raggedleft\n')
+
+    def duclass_open(self, node):
+        """Open a group and insert declarations for class values."""
+        if not isinstance(node.parent, nodes.compound):
+            self.out.append('\n')
+        for cls in node['classes']:
+            if cls.startswith('language-'):
+                language = self.babel.language_name(cls[9:])
+                if language:
+                    self.babel.otherlanguages[language] = True
+                    self.out.append('\\begin{selectlanguage}{%s}\n' % language)
+            elif (isinstance(node, nodes.table)
+                  and cls in Writer.table_style_values + ['colwidths-given']):
+                pass
+            else:
+                if not self.fallback_stylesheet:
+                    self.fallbacks['DUclass'] = PreambleCmds.duclass
+                self.out.append('\\begin{DUclass}{%s}\n' % cls)
+
+    def duclass_close(self, node):
+        """Close a group of class declarations."""
+        for cls in reversed(node['classes']):
+            if cls.startswith('language-'):
+                language = self.babel.language_name(cls[9:])
+                if language:
+                    self.out.append('\\end{selectlanguage}\n')
+            elif (isinstance(node, nodes.table)
+                  and cls in Writer.table_style_values + ['colwidths-given']):
+                pass
+            else:
+                if not self.fallback_stylesheet:
+                    self.fallbacks['DUclass'] = PreambleCmds.duclass
+                self.out.append('\\end{DUclass}\n')
+
+    def push_output_collector(self, new_out):
+        self.out_stack.append(self.out)
+        self.out = new_out
+
+    def pop_output_collector(self):
+        self.out = self.out_stack.pop()
+
+    def term_postfix(self, node):
+        """
+        Return LaTeX code required between term or field name and content.
+
+        In a LaTeX "description" environment (used for definition
+        lists and non-docinfo field lists), a ``\\leavevmode``
+        between an item's label and content ensures the correct
+        placement of certain block constructs.
+        """
+        for child in node:
+            if not isinstance(child, (nodes.Invisible, nodes.footnote,
+                                      nodes.citation)):
+                break
+        else:
+            return ''
+        if isinstance(child, (nodes.container, nodes.compound)):
+            return self.term_postfix(child)
+        if isinstance(child, nodes.image):
+            return '\\leavevmode\n'  # Images get an additional newline.
+        if not isinstance(child, (nodes.paragraph, nodes.math_block)):
+            return '\\leavevmode'
+        return ''
+
+    # Visitor methods
+    # ---------------
+
+    def visit_Text(self, node):
+        self.out.append(self.encode(node.astext()))
+
+    def depart_Text(self, node):
+        pass
+
+    def visit_abbreviation(self, node):
+        node['classes'].insert(0, 'abbreviation')
+        self.visit_inline(node)
+
+    def depart_abbreviation(self, node):
+        self.depart_inline(node)
+
+    def visit_acronym(self, node):
+        node['classes'].insert(0, 'acronym')
+        self.visit_inline(node)
+
+    def depart_acronym(self, node):
+        self.depart_inline(node)
+
+    def visit_address(self, node):
+        self.visit_docinfo_item(node, 'address')
+
+    def depart_address(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_admonition(self, node):
+        # strip the generic 'admonition' from the list of classes
+        node['classes'] = [cls for cls in node['classes']
+                           if cls != 'admonition']
+        if self.settings.legacy_class_functions:
+            self.fallbacks['admonition'] = PreambleCmds.admonition_legacy
+            if 'error' in node['classes']:
+                self.fallbacks['error'] = PreambleCmds.error_legacy
+            self.out.append('\n\\DUadmonition[%s]{'%','.join(node['classes']))
+            return
+        if not self.fallback_stylesheet:
+            self.fallbacks['admonition'] = PreambleCmds.admonition
+        if 'error' in node['classes'] and not self.fallback_stylesheet:
+            self.fallbacks['error'] = PreambleCmds.error
+        self.duclass_open(node)
+        self.out.append('\\begin{DUadmonition}')
+
+    def depart_admonition(self, node):
+        if self.settings.legacy_class_functions:
+            self.out.append('}\n')
+            return
+        self.out.append('\\end{DUadmonition}\n')
+        self.duclass_close(node)
+
+    def visit_author(self, node):
+        self.pdfauthor.append(self.attval(node.astext()))
+        self.visit_docinfo_item(node, 'author')
+
+    def depart_author(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_authors(self, node):
+        # not used: visit_author is called anyway for each author.
+        pass
+
+    def depart_authors(self, node):
+        pass
+
+    def visit_block_quote(self, node):
+        self.duclass_open(node)
+        self.out.append('\\begin{quote}')
+
+    def depart_block_quote(self, node):
+        self.out.append('\\end{quote}\n')
+        self.duclass_close(node)
+
+    def visit_bullet_list(self, node):
+        self.duclass_open(node)
+        self.out.append('\\begin{itemize}')
+
+    def depart_bullet_list(self, node):
+        self.out.append('\\end{itemize}\n')
+        self.duclass_close(node)
+
+    def visit_superscript(self, node):
+        self.out.append(r'\textsuperscript{')
+        self.visit_inline(node)
+
+    def depart_superscript(self, node):
+        self.depart_inline(node)
+        self.out.append('}')
+
+    def visit_subscript(self, node):
+        self.out.append(r'\textsubscript{')
+        self.visit_inline(node)
+
+    def depart_subscript(self, node):
+        self.depart_inline(node)
+        self.out.append('}')
+
+    def visit_caption(self, node):
+        self.out.append('\n\\caption{')
+
+    def depart_caption(self, node):
+        self.out.append('}\n')
+
+    def visit_title_reference(self, node):
+        if not self.fallback_stylesheet:
+            self.fallbacks['titlereference'] = PreambleCmds.titlereference
+        self.out.append(r'\DUroletitlereference{')
+        self.visit_inline(node)
+
+    def depart_title_reference(self, node):
+        self.depart_inline(node)
+        self.out.append('}')
+
+    def visit_citation(self, node):
+        if self.use_latex_citations:
+            self.push_output_collector([])
+        else:
+            # self.requirements['~fnt_floats'] = PreambleCmds.footnote_floats
+            self.out.append(r'\begin{figure}[b]')
+            self.append_hypertargets(node)
+
+    def depart_citation(self, node):
+        if self.use_latex_citations:
+            # TODO: normalize label
+            label = self.out[0]
+            text = ''.join(self.out[1:])
+            self._bibitems.append([label, text])
+            self.pop_output_collector()
+        else:
+            self.out.append('\\end{figure}\n')
+
+    def visit_citation_reference(self, node):
+        if self.bibtex:
+            self._bibitems.append([node.astext()])
+        if self.use_latex_citations:
+            if not self.inside_citation_reference_label:
+                self.out.append(r'\cite{')
+                self.inside_citation_reference_label = True
+            else:
+                assert self.out[-1] in (' ', '\n'),\
+                        'unexpected non-whitespace while in reference label'
+                del self.out[-1]
+        else:
+            href = ''
+            if 'refid' in node:
+                href = node['refid']
+            elif 'refname' in node:
+                href = self.document.nameids[node['refname']]
+            self.out.append('\\hyperlink{%s}{[' % href)
+
+    def depart_citation_reference(self, node):
+        # TODO: normalize labels
+        if self.use_latex_citations:
+            followup_citation = False
+            # check for a following citation separated by a space or newline
+            sibling = node.next_node(descend=False, siblings=True)
+            if (isinstance(sibling, nodes.Text)
+                and sibling.astext() in (' ', '\n')):
+                sibling2 = sibling.next_node(descend=False, siblings=True)
+                if isinstance(sibling2, nodes.citation_reference):
+                    followup_citation = True
+            if followup_citation:
+                self.out.append(',')
+            else:
+                self.out.append('}')
+                self.inside_citation_reference_label = False
+        else:
+            self.out.append(']}')
+
+    def visit_classifier(self, node):
+        self.out.append('(\\textbf{')
+
+    def depart_classifier(self, node):
+        self.out.append('})')
+        if node.next_node(nodes.term, descend=False, siblings=True):
+            self.out.append('\n')
+
+    def visit_colspec(self, node):
+        self.active_table.visit_colspec(node)
+
+    def depart_colspec(self, node):
+        pass
+
+    def visit_comment(self, node):
+        if not isinstance(node.parent, nodes.compound):
+            self.out.append('\n')
+        # Precede every line with a comment sign, wrap in newlines
+        self.out.append('%% %s\n' % node.astext().replace('\n', '\n% '))
+        raise nodes.SkipNode
+
+    def depart_comment(self, node):
+        pass
+
+    def visit_compound(self, node):
+        if isinstance(node.parent, nodes.compound):
+            self.out.append('\n')
+        node['classes'].insert(0, 'compound')
+        self.duclass_open(node)
+
+    def depart_compound(self, node):
+        self.duclass_close(node)
+
+    def visit_contact(self, node):
+        self.visit_docinfo_item(node, 'contact')
+
+    def depart_contact(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_container(self, node):
+        self.duclass_open(node)
+
+    def depart_container(self, node):
+        self.duclass_close(node)
+
+    def visit_copyright(self, node):
+        self.visit_docinfo_item(node, 'copyright')
+
+    def depart_copyright(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_date(self, node):
+        self.visit_docinfo_item(node, 'date')
+
+    def depart_date(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_decoration(self, node):
+        # header and footer
+        pass
+
+    def depart_decoration(self, node):
+        pass
+
+    def visit_definition(self, node):
+        pass
+
+    def depart_definition(self, node):
+        pass
+
+    def visit_definition_list(self, node):
+        self.duclass_open(node)
+        self.out.append('\\begin{description}\n')
+
+    def depart_definition_list(self, node):
+        self.out.append('\\end{description}\n')
+        self.duclass_close(node)
+
+    def visit_definition_list_item(self, node):
+        pass
+
+    def depart_definition_list_item(self, node):
+        if node.next_node(descend=False, siblings=True) is not None:
+            self.out.append('\n')                # TODO: just pass?
+
+    def visit_description(self, node):
+        self.out.append(' ')
+
+    def depart_description(self, node):
+        pass
+
+    def visit_docinfo(self, node):
+        self.push_output_collector(self.docinfo)
+
+    def depart_docinfo(self, node):
+        self.pop_output_collector()
+        # Some itmes (e.g. author) end up at other places
+        if self.docinfo:
+            # tabularx: automatic width of columns, no page breaks allowed.
+            self.requirements['tabularx'] = r'\usepackage{tabularx}'
+            if not self.fallback_stylesheet:
+                self.fallbacks['_providelength'] = PreambleCmds.providelength
+                self.fallbacks['docinfo'] = PreambleCmds.docinfo
+            #
+            self.docinfo.insert(0, '\n% Docinfo\n'
+                                '\\begin{center}\n'
+                                '\\begin{tabularx}{\\DUdocinfowidth}{lX}\n')
+            self.docinfo.append('\\end{tabularx}\n'
+                                '\\end{center}\n')
+
+    def visit_docinfo_item(self, node, name):
+        if self.use_latex_docinfo:
+            if name in ('author', 'organization', 'contact', 'address'):
+                # We attach these to the last author.  If any of them precedes
+                # the first author, put them in a separate "author" group
+                # (in lack of better semantics).
+                if name == 'author' or not self.author_stack:
+                    self.author_stack.append([])
+                if name == 'address':   # newlines are meaningful
+                    self.insert_newline = True
+                    text = self.encode(node.astext())
+                    self.insert_newline = False
+                else:
+                    text = self.attval(node.astext())
+                self.author_stack[-1].append(text)
+                raise nodes.SkipNode
+            elif name == 'date':
+                self.date.append(self.attval(node.astext()))
+                raise nodes.SkipNode
+        self.out.append('\\textbf{%s}: &\n\t' % self.language_label(name))
+        if name == 'address':
+            self.insert_newline = True
+            self.out.append('{\\raggedright\n')
+            self.context.append(' } \\\\\n')
+        else:
+            self.context.append(' \\\\\n')
+
+    def depart_docinfo_item(self, node):
+        self.out.append(self.context.pop())
+        # for address we did set insert_newline
+        self.insert_newline = False
+
+    def visit_doctest_block(self, node):
+        self.visit_literal_block(node)
+
+    def depart_doctest_block(self, node):
+        self.depart_literal_block(node)
+
+    def visit_document(self, node):
+        # titled document?
+        if (self.use_latex_docinfo or len(node)
+            and isinstance(node[0], nodes.title)):
+            protect = (self.settings.documentclass == 'memoir')
+            self.title_labels += self.ids_to_labels(node, set_anchor=False,
+                                                    protect=protect)
+
+    def depart_document(self, node):
+        # Complete "parts" with information gained from walkabout
+        # * language setup
+        if (self.babel.otherlanguages
+            or self.babel.language not in ('', 'english')):
+            self.requirements['babel'] = self.babel()
+        # * conditional requirements (before style sheet)
+        self.requirements = self.requirements.sortedvalues()
+        # * coditional fallback definitions (after style sheet)
+        self.fallbacks = self.fallbacks.sortedvalues()
+        # * PDF properties
+        self.pdfsetup.append(PreambleCmds.linking % self.hyperref_options)
+        if self.pdfauthor:
+            authors = self.author_separator.join(self.pdfauthor)
+            self.pdfinfo.append('  pdfauthor={%s}' % authors)
+        if self.pdfinfo:
+            self.pdfsetup += [r'\hypersetup{'] + self.pdfinfo + ['}']
+        # * title (including author(s) and date if using "latex_docinfo")
+        if self.title or (self.use_latex_docinfo
+                          and (self.author_stack or self.date)):
+            self.make_title()  # see below
+        # * bibliography
+        if self._bibitems:
+            self.append_bibliogaphy()  # see below
+        # * make sure to generate a toc file if needed for local contents:
+        if 'minitoc' in self.requirements and not self.has_latex_toc:
+            self.out.append('\n\\faketableofcontents % for local ToCs\n')
+
+    def make_title(self):
+        # Auxiliary function called by `self.depart_document()`.
+        #
+        # Append ``\title``, ``\author``, and ``\date`` to "titledata".
+        # (We need all three, even if empty, to prevent errors
+        # and/or automatic display of the current date by \maketitle.)
+        # Append ``\maketitle`` to "body_pre_docinfo" parts.
+        #
+        # \title
+        title_arg = [''.join(self.title)]  # ensure len == 1
+        if self.title:
+            title_arg += self.title_labels
+        if self.subtitle:
+            title_arg += [r'\\',
+                          r'\DUdocumentsubtitle{%s}' % ''.join(self.subtitle),
+                          ] + self.subtitle_labels
+        self.titledata.append(r'\title{%s}' % '%\n  '.join(title_arg))
+        # \author
+        author_arg = ['\\\\\n'.join(author_entry)
+                      for author_entry in self.author_stack]
+        self.titledata.append(r'\author{%s}' %
+                              ' \\and\n'.join(author_arg))
+        # \date
+        self.titledata.append(r'\date{%s}' % ', '.join(self.date))
+        # \maketitle
+        # Must be in the document body. We add it to `body_pre_docinfo`
+        # to allow templates to put `titledata` into the document preamble.
+        self.body_pre_docinfo.append('\\maketitle\n')
+
+    def append_bibliogaphy(self):
+        # Add bibliography at end of document.
+        # TODO insertion point should be configurable.
+        # Auxiliary function called by `depart_document`.
+        if self.bibtex:
+            self.out.append('\n\\bibliographystyle{%s}\n' % self.bibtex[0])
+            self.out.append('\\bibliography{%s}\n' % ','.join(self.bibtex[1:]))
+        elif self.use_latex_citations:
+            # TODO: insert citations at point of definition.
+            widest_label = ''
+            for bibitem in self._bibitems:
+                if len(widest_label) < len(bibitem[0]):
+                    widest_label = bibitem[0]
+            self.out.append('\n\\begin{thebibliography}{%s}\n' %
+                            widest_label)
+            for bibitem in self._bibitems:
+                # cite_key: underscores must not be escaped
+                cite_key = bibitem[0].replace(r'\_', '_')
+                self.out.append('\\bibitem[%s]{%s}{%s}\n' %
+                                (bibitem[0], cite_key, bibitem[1]))
+            self.out.append('\\end{thebibliography}\n')
+
+    def visit_emphasis(self, node):
+        self.out.append('\\emph{')
+        self.visit_inline(node)
+
+    def depart_emphasis(self, node):
+        self.depart_inline(node)
+        self.out.append('}')
+
+    # Append column delimiters and advance column counter,
+    # if the current cell is a multi-row continuation."""
+    def insert_additional_table_colum_delimiters(self):
+        while self.active_table.get_rowspan(
+                                self.active_table.get_entry_number()):
+            self.out.append(' & ')
+            self.active_table.visit_entry()  # increment cell count
+
+    def visit_entry(self, node):
+        # cell separation
+        if self.active_table.get_entry_number() == 0:
+            self.insert_additional_table_colum_delimiters()
+        else:
+            self.out.append(' & ')
+
+        # multirow, multicolumn
+        if 'morerows' in node and 'morecols' in node:
+            raise NotImplementedError('Cells that span multiple rows *and* '
+                                      'columns currently not supported '
+                                      'by the LaTeX writer')
+            # TODO: should be possible with LaTeX, see e.g.
+            # http://texblog.org/2012/12/21/multi-column-and-multi-row-cells-in-latex-tables/
+        # multirow in LaTeX simply will enlarge the cell over several rows
+        # (the following n if n is positive, the former if negative).
+        if 'morerows' in node:
+            self.requirements['multirow'] = r'\usepackage{multirow}'
+            mrows = node['morerows'] + 1
+            self.active_table.set_rowspan(
+                            self.active_table.get_entry_number(), mrows)
+            self.out.append('\\multirow{%d}{%s}{' %
+                            (mrows, self.active_table.get_column_width()))
+            self.context.append('}')
+        elif 'morecols' in node:
+            # the vertical bar before column is missing if it is the first
+            # column. the one after always.
+            if self.active_table.get_entry_number() == 0:
+                bar1 = self.active_table.get_vertical_bar()
+            else:
+                bar1 = ''
+            mcols = node['morecols'] + 1
+            self.out.append('\\multicolumn{%d}{%s%s%s}{' %
+                            (mcols,
+                             bar1,
+                             self.active_table.get_multicolumn_width(
+                                 self.active_table.get_entry_number(), mcols),
+                             self.active_table.get_vertical_bar()))
+            self.context.append('}')
+        else:
+            self.context.append('')
+
+        # bold header/stub-column
+        if len(node) and (isinstance(node.parent.parent, nodes.thead)
+                          or self.active_table.is_stub_column()):
+            self.out.append('\\textbf{')
+            self.context.append('}')
+        else:
+            self.context.append('')
+
+        # if line ends with '{', mask line break
+        if (not self.active_table.colwidths_auto
+            and self.out[-1].endswith("{")
+            and node.astext()):
+            self.out.append("%")
+
+        self.active_table.visit_entry()  # increment cell count
+
+    def depart_entry(self, node):
+        self.out.append(self.context.pop())  # header / not header
+        self.out.append(self.context.pop())  # multirow/column
+        # insert extra "&"s, if following rows are spanned from above:
+        self.insert_additional_table_colum_delimiters()
+
+    def visit_row(self, node):
+        self.active_table.visit_row()
+
+    def depart_row(self, node):
+        self.out.extend(self.active_table.depart_row())
+
+    def visit_enumerated_list(self, node):
+        # enumeration styles:
+        types = {'': '',
+                 'arabic': 'arabic',
+                 'loweralpha': 'alph',
+                 'upperalpha': 'Alph',
+                 'lowerroman': 'roman',
+                 'upperroman': 'Roman'}
+        # default LaTeX enumeration labels:
+        default_labels = [
+                          # (präfix, enumtype, suffix)
+                          ('',  'arabic', '.'),  # 1.
+                          ('(', 'alph',   ')'),  # (a)
+                          ('',  'roman',  '.'),  # i.
+                          ('',  'Alph',   '.')]  # A.
+
+        prefix = ''
+        if self.compound_enumerators:
+            if (self.section_prefix_for_enumerators and self.section_level
+                and not self._enumeration_counters):
+                prefix = '.'.join(str(n) for n in
+                                  self._section_number[:self.section_level]
+                                  ) + self.section_enumerator_separator
+            if self._enumeration_counters:
+                prefix += self._enumeration_counters[-1]
+        prefix += node.get('prefix', '')
+        enumtype = types[node.get('enumtype', 'arabic')]
+        suffix = node.get('suffix', '.')
+
+        enum_level = len(self._enumeration_counters)+1
+        counter_name = 'enum' + roman.toRoman(enum_level).lower()
+        label = r'%s\%s{%s}%s' % (prefix, enumtype, counter_name, suffix)
+        self._enumeration_counters.append(label)
+
+        self.duclass_open(node)
+        if enum_level <= 4:
+            self.out.append('\\begin{enumerate}')
+            if (prefix, enumtype, suffix) != default_labels[enum_level-1]:
+                self.out.append('\n\\renewcommand{\\label%s}{%s}' %
+                                (counter_name, label))
+        else:
+            self.fallbacks[counter_name] = '\\newcounter{%s}' % counter_name
+            self.out.append('\\begin{list}')
+            self.out.append('{%s}' % label)
+            self.out.append('{\\usecounter{%s}}' % counter_name)
+        if 'start' in node:
+            self.out.append('\n\\setcounter{%s}{%d}' %
+                            (counter_name, node['start']-1))
+
+    def depart_enumerated_list(self, node):
+        if len(self._enumeration_counters) <= 4:
+            self.out.append('\\end{enumerate}\n')
+        else:
+            self.out.append('\\end{list}\n')
+        self.duclass_close(node)
+        self._enumeration_counters.pop()
+
+    def visit_field(self, node):
+        # output is done in field_body, field_name
+        pass
+
+    def depart_field(self, node):
+        pass
+
+    def visit_field_body(self, node):
+        if not isinstance(node.parent.parent, nodes.docinfo):
+            self.out.append(self.term_postfix(node))
+
+    def depart_field_body(self, node):
+        if self.out is self.docinfo:
+            self.out.append(r'\\'+'\n')
+
+    def visit_field_list(self, node):
+        self.duclass_open(node)
+        if self.out is not self.docinfo:
+            if not self.fallback_stylesheet:
+                self.fallbacks['fieldlist'] = PreambleCmds.fieldlist
+            self.out.append('\\begin{DUfieldlist}')
+
+    def depart_field_list(self, node):
+        if self.out is not self.docinfo:
+            self.out.append('\\end{DUfieldlist}\n')
+        self.duclass_close(node)
+
+    def visit_field_name(self, node):
+        if self.out is self.docinfo:
+            self.out.append('\\textbf{')
+        else:
+            # Commands with optional args inside an optional arg must be put
+            # in a group, e.g. ``\item[{\hyperref[label]{text}}]``.
+            self.out.append('\n\\item[{')
+
+    def depart_field_name(self, node):
+        if self.out is self.docinfo:
+            self.out.append('}: &')
+        else:
+            self.out.append(':}]')
+
+    def visit_figure(self, node):
+        self.requirements['float'] = PreambleCmds.float
+        self.duclass_open(node)
+        # The 'align' attribute sets the "outer alignment",
+        # for "inner alignment" use LaTeX default alignment (similar to HTML)
+        alignment = node.attributes.get('align', 'center')
+        if alignment != 'center':
+            # The LaTeX "figure" environment always uses the full linewidth,
+            # so "outer alignment" is ignored. Just write a comment.
+            # TODO: use the wrapfigure environment?
+            self.out.append('\\begin{figure} %% align = "%s"\n' % alignment)
+        else:
+            self.out.append('\\begin{figure}\n')
+        self.out += self.ids_to_labels(node, newline=True)
+
+    def depart_figure(self, node):
+        self.out.append('\\end{figure}\n')
+        self.duclass_close(node)
+
+    def visit_footer(self, node):
+        self.push_output_collector([])
+        self.out.append(r'\newcommand{\DUfooter}{')
+
+    def depart_footer(self, node):
+        self.out.append('}')
+        self.requirements['~footer'] = ''.join(self.out)
+        self.pop_output_collector()
+
+    def visit_footnote(self, node):
+        try:
+            backref = node['backrefs'][0]
+        except IndexError:
+            backref = node['ids'][0]  # no backref, use self-ref instead
+        if self.docutils_footnotes:
+            if not self.fallback_stylesheet:
+                self.fallbacks['footnotes'] = PreambleCmds.footnotes
+            num = node[0].astext()
+            if self.settings.footnote_references == 'brackets':
+                num = '[%s]' % num
+            self.out.append('%%\n\\DUfootnotetext{%s}{%s}{%s}{' %
+                            (node['ids'][0], backref, self.encode(num)))
+            if node['ids'] == node['names']:
+                self.out += self.ids_to_labels(node)
+            # prevent spurious whitespace if footnote starts with paragraph:
+            if len(node) > 1 and isinstance(node[1], nodes.paragraph):
+                self.out.append('%')
+        # TODO: "real" LaTeX \footnote{}s (see visit_footnotes_reference())
+
+    def depart_footnote(self, node):
+        self.out.append('}\n')
+
+    def visit_footnote_reference(self, node):
+        href = ''
+        if 'refid' in node:
+            href = node['refid']
+        elif 'refname' in node:
+            href = self.document.nameids[node['refname']]
+        # if not self.docutils_footnotes:
+        #     # TODO: insert footnote content at (or near) this place
+        #     #       see also docs/dev/todo.txt
+        #     try:
+        #         referenced_node = self.document.ids[node['refid']]
+        #     except (AttributeError, KeyError):
+        #         self.document.reporter.error(
+        #             'unresolved footnote-reference %s' % node)
+        #     print('footnote-ref to %s' % referenced_node)
+        format = self.settings.footnote_references
+        if format == 'brackets':
+            self.append_hypertargets(node)
+            self.out.append('\\hyperlink{%s}{[' % href)
+            self.context.append(']}')
+        else:
+            if not self.fallback_stylesheet:
+                self.fallbacks['footnotes'] = PreambleCmds.footnotes
+            self.out.append(r'\DUfootnotemark{%s}{%s}{' %
+                            (node['ids'][0], href))
+            self.context.append('}')
+
+    def depart_footnote_reference(self, node):
+        self.out.append(self.context.pop())
+
+    # footnote/citation label
+    def label_delim(self, node, bracket, superscript):
+        if isinstance(node.parent, nodes.footnote):
+            raise nodes.SkipNode
+        else:
+            assert isinstance(node.parent, nodes.citation)
+            if not self.use_latex_citations:
+                self.out.append(bracket)
+
+    def visit_label(self, node):
+        """footnote or citation label: in brackets or as superscript"""
+        self.label_delim(node, '[', '\\textsuperscript{')
+
+    def depart_label(self, node):
+        self.label_delim(node, ']', '}')
+
+    # elements generated by the framework e.g. section numbers.
+    def visit_generated(self, node):
+        pass
+
+    def depart_generated(self, node):
+        pass
+
+    def visit_header(self, node):
+        self.push_output_collector([])
+        self.out.append(r'\newcommand{\DUheader}{')
+
+    def depart_header(self, node):
+        self.out.append('}')
+        self.requirements['~header'] = ''.join(self.out)
+        self.pop_output_collector()
+
+    def to_latex_length(self, length_str, pxunit=None):
+        """Convert `length_str` with rst length to LaTeX length
+        """
+        if pxunit is not None:
+            warnings.warn(
+                'The optional argument `pxunit` '
+                'of LaTeXTranslator.to_latex_length() is ignored '
+                'and will be removed in Docutils 0.21 or later',
+                DeprecationWarning, stacklevel=2)
+        match = re.match(r'(\d*\.?\d*)\s*(\S*)', length_str)
+        if not match:
+            return length_str
+        value, unit = match.groups()[:2]
+        # no unit or "DTP" points (called 'bp' in TeX):
+        if unit in ('', 'pt'):
+            length_str = '%sbp' % value
+        # percentage: relate to current line width
+        elif unit == '%':
+            length_str = '%.3f\\linewidth' % (float(value)/100.0)
+        elif self.is_xetex and unit == 'px':
+            # XeTeX does not know the length unit px.
+            # Use \pdfpxdimen, the macro to set the value of 1 px in pdftex.
+            # This way, configuring works the same for pdftex and xetex.
+            if not self.fallback_stylesheet:
+                self.fallbacks['_providelength'] = PreambleCmds.providelength
+            self.fallbacks['px'] = '\n\\DUprovidelength{\\pdfpxdimen}{1bp}\n'
+            length_str = r'%s\pdfpxdimen' % value
+        return length_str
+
+    def visit_image(self, node):
+        self.requirements['graphicx'] = self.graphicx_package
+        attrs = node.attributes
+        # Convert image URI to a local file path
+        imagepath = url2pathname(attrs['uri']).replace('\\', '/')
+        # alignment defaults:
+        if 'align' not in attrs:
+            # Set default align of image in a figure to 'center'
+            if isinstance(node.parent, nodes.figure):
+                attrs['align'] = 'center'
+            self.set_align_from_classes(node)
+        # pre- and postfix (prefix inserted in reverse order)
+        pre = []
+        post = []
+        include_graphics_options = []
+        align_codes = {
+            # inline images: by default latex aligns the bottom.
+            'bottom': ('', ''),
+            'middle': (r'\raisebox{-0.5\height}{', '}'),
+            'top':    (r'\raisebox{-\height}{', '}'),
+            # block level images:
+            'center': (r'\noindent\makebox[\linewidth][c]{', '}'),
+            'left':   (r'\noindent{', r'\hfill}'),
+            'right':  (r'\noindent{\hfill', '}'),
+            }
+        if 'align' in attrs:
+            # TODO: warn or ignore non-applicable alignment settings?
+            try:
+                align_code = align_codes[attrs['align']]
+                pre.append(align_code[0])
+                post.append(align_code[1])
+            except KeyError:
+                pass                    # TODO: warn?
+        if 'height' in attrs:
+            include_graphics_options.append(
+                'height=%s' % self.to_latex_length(attrs['height']))
+        if 'scale' in attrs:
+            include_graphics_options.append(
+                'scale=%f' % (attrs['scale'] / 100.0))
+        if 'width' in attrs:
+            include_graphics_options.append(
+                'width=%s' % self.to_latex_length(attrs['width']))
+        if not (self.is_inline(node)
+                or isinstance(node.parent, (nodes.figure, nodes.compound))):
+            pre.append('\n')
+        if not (self.is_inline(node)
+                or isinstance(node.parent, nodes.figure)):
+            post.append('\n')
+        pre.reverse()
+        self.out.extend(pre)
+        options = ''
+        if include_graphics_options:
+            options = '[%s]' % (','.join(include_graphics_options))
+        self.out.append('\\includegraphics%s{%s}' % (options, imagepath))
+        self.out.extend(post)
+
+    def depart_image(self, node):
+        self.out += self.ids_to_labels(node, newline=True)
+
+    def visit_inline(self, node):  # <span>, i.e. custom roles
+        for cls in node['classes']:
+            if cls.startswith('language-'):
+                language = self.babel.language_name(cls[9:])
+                if language:
+                    self.babel.otherlanguages[language] = True
+                    self.out.append(r'\foreignlanguage{%s}{' % language)
+            else:
+                if not self.fallback_stylesheet:
+                    self.fallbacks['inline'] = PreambleCmds.inline
+                self.out.append(r'\DUrole{%s}{' % cls)
+
+    def depart_inline(self, node):
+        self.out.append('}' * len(node['classes']))
+
+    def visit_legend(self, node):
+        if not self.fallback_stylesheet:
+            self.fallbacks['legend'] = PreambleCmds.legend
+        self.out.append('\\begin{DUlegend}')
+
+    def depart_legend(self, node):
+        self.out.append('\\end{DUlegend}\n')
+
+    def visit_line(self, node):
+        self.out.append(r'\item[] ')
+
+    def depart_line(self, node):
+        self.out.append('\n')
+
+    def visit_line_block(self, node):
+        if not self.fallback_stylesheet:
+            self.fallbacks['_providelength'] = PreambleCmds.providelength
+            self.fallbacks['lineblock'] = PreambleCmds.lineblock
+        self.set_align_from_classes(node)
+        if isinstance(node.parent, nodes.line_block):
+            self.out.append('\\item[]\n'
+                            '\\begin{DUlineblock}{\\DUlineblockindent}\n')
+            # nested line-blocks cannot be given class arguments
+        else:
+            self.duclass_open(node)
+            self.out.append('\\begin{DUlineblock}{0em}\n')
+            self.insert_align_declaration(node)
+
+    def depart_line_block(self, node):
+        self.out.append('\\end{DUlineblock}\n')
+        self.duclass_close(node)
+
+    def visit_list_item(self, node):
+        self.out.append('\n\\item ')
+
+    def depart_list_item(self, node):
+        pass
+
+    def visit_literal(self, node):
+        self.literal = True
+        if ('code' in node['classes']
+            and self.settings.syntax_highlight != 'none'):
+            self.requirements['color'] = PreambleCmds.color
+            if not self.fallback_stylesheet:
+                self.fallbacks['code'] = PreambleCmds.highlight_rules
+        self.out.append('\\texttt{')
+        self.visit_inline(node)
+
+    def depart_literal(self, node):
+        self.literal = False
+        self.depart_inline(node)
+        self.out.append('}')
+
+    # Literal blocks are used for '::'-prefixed literal-indented
+    # blocks of text, where the inline markup is not recognized,
+    # but are also the product of the "parsed-literal" directive,
+    # where the markup is respected.
+    #
+    # In both cases, we want to use a typewriter/monospaced typeface.
+    # For "real" literal-blocks, we can use \verbatim, while for all
+    # the others we must use \ttfamily and \raggedright.
+    #
+    # We can distinguish between the two kinds by the number of
+    # siblings that compose this node: if it is composed by a
+    # single element, it's either
+    # * a real one,
+    # * a parsed-literal that does not contain any markup, or
+    # * a parsed-literal containing just one markup construct.
+    def is_plaintext(self, node):
+        """Check whether a node can be typeset verbatim"""
+        return (len(node) == 1) and isinstance(node[0], nodes.Text)
+
+    def visit_literal_block(self, node):
+        """Render a literal block.
+
+        Corresponding rST elements: literal block, parsed-literal, code.
+        """
+        packages = {'lstlisting':  r'\usepackage{listings}' '\n'
+                                   r'\lstset{xleftmargin=\leftmargin}',
+                    'listing': r'\usepackage{moreverb}',
+                    'Verbatim': r'\usepackage{fancyvrb}',
+                    'verbatimtab': r'\usepackage{moreverb}'}
+
+        literal_env = self.literal_block_env
+
+        # Check, if it is possible to use a literal-block environment
+        _plaintext = self.is_plaintext(node)
+        _in_table = self.active_table.is_open()
+        # TODO: fails if normal text precedes the literal block.
+        #       Check parent node instead?
+        _autowidth_table = _in_table and self.active_table.colwidths_auto
+        _no_env_nodes = (nodes.footnote, nodes.sidebar)
+        if self.settings.legacy_class_functions:
+            _no_env_nodes += (nodes.admonition, nodes.system_message)
+        _use_env = _plaintext and not isinstance(node.parent, _no_env_nodes)
+        _use_listings = (literal_env == 'lstlisting') and _use_env
+
+        # Labels and classes:
+        self.duclass_open(node)
+        self.out += self.ids_to_labels(node, newline=True)
+        # Highlight code?
+        if (not _plaintext
+            and 'code' in node['classes']
+            and self.settings.syntax_highlight != 'none'):
+            self.requirements['color'] = PreambleCmds.color
+            if not self.fallback_stylesheet:
+                self.fallbacks['code'] = PreambleCmds.highlight_rules
+        # Wrap?
+        if _in_table and _use_env and not _autowidth_table:
+            # Wrap in minipage to prevent extra vertical space
+            # with alltt and verbatim-like environments:
+            self.fallbacks['ttem'] = PreambleCmds.ttem
+            self.out.append(
+                '\\begin{minipage}{%d\\ttemwidth}\n' %
+                (max(len(line) for line in node.astext().split('\n'))))
+            self.context.append('\n\\end{minipage}\n')
+        elif not _in_table and not _use_listings:
+            # Wrap in quote to set off vertically and indent
+            self.out.append('\\begin{quote}\n')
+            self.context.append('\n\\end{quote}\n')
+        else:
+            self.context.append('\n')
+
+        # Use verbatim-like environment, if defined and possible
+        # (in an auto-width table, only listings works):
+        if literal_env and _use_env and (not _autowidth_table
+                                         or _use_listings):
+            try:
+                self.requirements['literal_block'] = packages[literal_env]
+            except KeyError:
+                pass
+            self.verbatim = True
+            if _in_table and _use_listings:
+                self.out.append('\\lstset{xleftmargin=0pt}\n')
+            self.out.append('\\begin{%s}%s\n' %
+                            (literal_env, self.literal_block_options))
+            self.context.append('\n\\end{%s}' % literal_env)
+        elif _use_env and not _autowidth_table:
+            self.alltt = True
+            self.requirements['alltt'] = r'\usepackage{alltt}'
+            self.out.append('\\begin{alltt}\n')
+            self.context.append('\n\\end{alltt}')
+        else:
+            self.literal = True
+            self.insert_newline = True
+            self.insert_non_breaking_blanks = True
+            # \raggedright ensures leading blanks are respected but
+            # leads to additional leading vspace if the first line
+            # of the block is overfull :-(
+            self.out.append('\\ttfamily\\raggedright\n')
+            self.context.append('')
+
+    def depart_literal_block(self, node):
+        self.insert_non_breaking_blanks = False
+        self.insert_newline = False
+        self.literal = False
+        self.verbatim = False
+        self.alltt = False
+        self.out.append(self.context.pop())
+        self.out.append(self.context.pop())
+        self.duclass_close(node)
+
+    def visit_meta(self, node):
+        name = node.attributes.get('name')
+        content = node.attributes.get('content')
+        if not name or not content:
+            return
+        if name in ('author', 'creator', 'keywords', 'subject', 'title'):
+            # fields with dedicated hyperref options:
+            self.pdfinfo.append('  pdf%s={%s},'%(name, content))
+        elif name == 'producer':
+            self.pdfinfo.append('  addtopdfproducer={%s},'%content)
+        else:
+            # generic interface (case sensitive!)
+            # TODO: filter irrelevant nodes ("http-equiv", ...)?
+            self.pdfinfo.append('  pdfinfo={%s={%s}},'%(name, content))
+
+    def depart_meta(self, node):
+        pass
+
+    def visit_math(self, node, math_env='$'):
+        """math role"""
+        self.visit_inline(node)
+        self.requirements['amsmath'] = r'\usepackage{amsmath}'
+        math_code = node.astext().translate(unichar2tex.uni2tex_table)
+        if math_env == '$':
+            if self.alltt:
+                wrapper = ['\\(', '\\)']
+            else:
+                wrapper = ['$', '$']
+        else:
+            labels = self.ids_to_labels(node, set_anchor=False, newline=True)
+            wrapper = ['%%\n\\begin{%s}\n' % math_env,
+                       '\n',
+                       ''.join(labels),
+                       '\\end{%s}' % math_env]
+        wrapper.insert(1, math_code)
+        self.out.extend(wrapper)
+        self.depart_inline(node)
+        # Content already processed:
+        raise nodes.SkipNode
+
+    def depart_math(self, node):
+        pass  # never reached
+
+    def visit_math_block(self, node):
+        math_env = pick_math_environment(node.astext())
+        self.visit_math(node, math_env=math_env)
+
+    def depart_math_block(self, node):
+        pass  # never reached
+
+    def visit_option(self, node):
+        if self.context[-1]:
+            # this is not the first option
+            self.out.append(', ')
+
+    def depart_option(self, node):
+        # flag that the first option is done.
+        self.context[-1] += 1
+
+    def visit_option_argument(self, node):
+        """Append the delimiter between an option and its argument to body."""
+        self.out.append(node.get('delimiter', ' '))
+
+    def depart_option_argument(self, node):
+        pass
+
+    def visit_option_group(self, node):
+        self.out.append('\\item[')
+        # flag for first option
+        self.context.append(0)
+
+    def depart_option_group(self, node):
+        self.context.pop()  # the flag
+        self.out.append('] ')
+
+    def visit_option_list(self, node):
+        if not self.fallback_stylesheet:
+            self.fallbacks['_providelength'] = PreambleCmds.providelength
+            self.fallbacks['optionlist'] = PreambleCmds.optionlist
+        self.duclass_open(node)
+        self.out.append('\\begin{DUoptionlist}\n')
+
+    def depart_option_list(self, node):
+        self.out.append('\\end{DUoptionlist}\n')
+        self.duclass_close(node)
+
+    def visit_option_list_item(self, node):
+        pass
+
+    def depart_option_list_item(self, node):
+        pass
+
+    def visit_option_string(self, node):
+        ## self.out.append(self.starttag(node, 'span', '', CLASS='option'))
+        pass
+
+    def depart_option_string(self, node):
+        ## self.out.append('</span>')
+        pass
+
+    def visit_organization(self, node):
+        self.visit_docinfo_item(node, 'organization')
+
+    def depart_organization(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_paragraph(self, node):
+        # insert blank line, unless
+        # * the paragraph is first in a list item, compound, or container
+        # * follows a non-paragraph node in a compound,
+        # * is in a table with auto-width columns
+        index = node.parent.index(node)
+        if index == 0 and isinstance(node.parent,
+                                     (nodes.list_item, nodes.description,
+                                      nodes.compound, nodes.container)):
+            pass
+        elif (index > 0
+              and isinstance(node.parent, nodes.compound)
+              and not isinstance(node.parent[index - 1],
+                                 (nodes.paragraph, nodes.compound))):
+            pass
+        elif self.active_table.colwidths_auto:
+            if index == 1:  # second paragraph
+                self.warn('LaTeX merges paragraphs in tables '
+                          'with auto-sized columns!', base_node=node)
+            if index > 0:
+                self.out.append('\n')
+        else:
+            self.out.append('\n')
+        self.out += self.ids_to_labels(node, newline=True)
+        self.visit_inline(node)
+
+    def depart_paragraph(self, node):
+        self.depart_inline(node)
+        if not self.active_table.colwidths_auto:
+            self.out.append('\n')
+
+    def visit_problematic(self, node):
+        self.requirements['color'] = PreambleCmds.color
+        self.out.append('%\n')
+        self.append_hypertargets(node)
+        self.out.append(r'\hyperlink{%s}{\textbf{\color{red}' % node['refid'])
+
+    def depart_problematic(self, node):
+        self.out.append('}}')
+
+    def visit_raw(self, node):
+        if 'latex' not in node.get('format', '').split():
+            raise nodes.SkipNode
+        if not (self.is_inline(node)
+                or isinstance(node.parent, nodes.compound)):
+            self.out.append('\n')
+        self.visit_inline(node)
+        # append "as-is" skipping any LaTeX-encoding
+        self.verbatim = True
+
+    def depart_raw(self, node):
+        self.verbatim = False
+        self.depart_inline(node)
+        if not self.is_inline(node):
+            self.out.append('\n')
+
+    def has_unbalanced_braces(self, string):
+        """Test whether there are unmatched '{' or '}' characters."""
+        level = 0
+        for ch in string:
+            if ch == '{':
+                level += 1
+            if ch == '}':
+                level -= 1
+            if level < 0:
+                return True
+        return level != 0
+
+    def visit_reference(self, node):
+        # We need to escape #, \, and % if we use the URL in a command.
+        special_chars = {ord('#'): '\\#',
+                         ord('%'): '\\%',
+                         ord('\\'): '\\\\',
+                         }
+        # external reference (URL)
+        if 'refuri' in node:
+            href = str(node['refuri']).translate(special_chars)
+            # problematic chars double caret and unbalanced braces:
+            if href.find('^^') != -1 or self.has_unbalanced_braces(href):
+                self.error(
+                    f'External link "{href}" not supported by LaTeX.\n'
+                    ' (Must not contain "^^" or unbalanced braces.)')
+            if node['refuri'] == node.astext():
+                self.out.append(r'\url{%s}' % href)
+                raise nodes.SkipNode
+            self.out.append(r'\href{%s}{' % href)
+            return
+        # internal reference
+        if 'refid' in node:
+            href = node['refid']
+        elif 'refname' in node:
+            href = self.document.nameids[node['refname']]
+        else:
+            raise AssertionError('Unknown reference.')
+        if not self.is_inline(node):
+            self.out.append('\n')
+        self.out.append('\\hyperref[%s]{' % href)
+        if self.reference_label:
+            # TODO: don't use \hyperref if self.reference_label is True
+            self.out.append('\\%s{%s}}' %
+                            (self.reference_label, href.replace('#', '')))
+            raise nodes.SkipNode
+
+    def depart_reference(self, node):
+        self.out.append('}')
+        if not self.is_inline(node):
+            self.out.append('\n')
+
+    def visit_revision(self, node):
+        self.visit_docinfo_item(node, 'revision')
+
+    def depart_revision(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_rubric(self, node):
+        if not self.fallback_stylesheet:
+            self.fallbacks['rubric'] = PreambleCmds.rubric
+        # class wrapper would interfere with ``\section*"`` type commands
+        # (spacing/indent of first paragraph)
+        self.out.append('\n\\DUrubric{')
+
+    def depart_rubric(self, node):
+        self.out.append('}\n')
+
+    def visit_section(self, node):
+        self.section_level += 1
+        # Initialize counter for potential subsections:
+        self._section_number.append(0)
+        # Counter for this section's level (initialized by parent section):
+        self._section_number[self.section_level - 1] += 1
+
+    def depart_section(self, node):
+        # Remove counter for potential subsections:
+        self._section_number.pop()
+        self.section_level -= 1
+
+    def visit_sidebar(self, node):
+        self.duclass_open(node)
+        self.requirements['color'] = PreambleCmds.color
+        if not self.fallback_stylesheet:
+            self.fallbacks['sidebar'] = PreambleCmds.sidebar
+        self.out.append('\\DUsidebar{')
+
+    def depart_sidebar(self, node):
+        self.out.append('}\n')
+        self.duclass_close(node)
+
+    attribution_formats = {'dash': ('—', ''),  # EM DASH
+                           'parentheses': ('(', ')'),
+                           'parens': ('(', ')'),
+                           'none': ('', '')}
+
+    def visit_attribution(self, node):
+        prefix, suffix = self.attribution_formats[self.settings.attribution]
+        self.out.append('\\nopagebreak\n\n\\raggedleft ')
+        self.out.append(prefix)
+        self.context.append(suffix)
+
+    def depart_attribution(self, node):
+        self.out.append(self.context.pop() + '\n')
+
+    def visit_status(self, node):
+        self.visit_docinfo_item(node, 'status')
+
+    def depart_status(self, node):
+        self.depart_docinfo_item(node)
+
+    def visit_strong(self, node):
+        self.out.append('\\textbf{')
+        self.visit_inline(node)
+
+    def depart_strong(self, node):
+        self.depart_inline(node)
+        self.out.append('}')
+
+    def visit_substitution_definition(self, node):
+        raise nodes.SkipNode
+
+    def visit_substitution_reference(self, node):
+        self.unimplemented_visit(node)
+
+    def visit_subtitle(self, node):
+        if isinstance(node.parent, nodes.document):
+            self.push_output_collector(self.subtitle)
+            if not self.fallback_stylesheet:
+                self.fallbacks['documentsubtitle'] = PreambleCmds.documentsubtitle  # noqa:E501
+            protect = (self.settings.documentclass == 'memoir')
+            self.subtitle_labels += self.ids_to_labels(node, set_anchor=False,
+                                                       protect=protect)
+        # section subtitle: "starred" (no number, not in ToC)
+        elif isinstance(node.parent, nodes.section):
+            self.out.append(r'\%s*{' %
+                            self.d_class.section(self.section_level + 1))
+        else:
+            if not self.fallback_stylesheet:
+                self.fallbacks['subtitle'] = PreambleCmds.subtitle
+            self.out.append('\n\\DUsubtitle{')
+
+    def depart_subtitle(self, node):
+        if isinstance(node.parent, nodes.document):
+            self.pop_output_collector()
+        else:
+            self.out.append('}\n')
+
+    def visit_system_message(self, node):
+        self.requirements['color'] = PreambleCmds.color
+        if not self.fallback_stylesheet:
+            self.fallbacks['title'] = PreambleCmds.title
+        if self.settings.legacy_class_functions:
+            self.fallbacks['title'] = PreambleCmds.title_legacy
+        node['classes'] = ['system-message']
+        self.visit_admonition(node)
+        if self.settings.legacy_class_functions:
+            self.out.append('\n\\DUtitle[system-message]{system-message\n')
+        else:
+            self.out.append('\n\\DUtitle{system-message\n')
+        self.append_hypertargets(node)
+        try:
+            line = ', line~%s' % node['line']
+        except KeyError:
+            line = ''
+        self.out.append('}\n\n{\\color{red}%s/%s} in \\texttt{%s}%s\n' %
+                        (node['type'], node['level'],
+                         self.encode(node['source']), line))
+        if len(node['backrefs']) == 1:
+            self.out.append('\n\\hyperlink{%s}{' % node['backrefs'][0])
+            self.context.append('}')
+        else:
+            backrefs = ['\\hyperlink{%s}{%d}' % (href, i+1)
+                        for (i, href) in enumerate(node['backrefs'])]
+            self.context.append('backrefs: ' + ' '.join(backrefs))
+
+    def depart_system_message(self, node):
+        self.out.append(self.context.pop())
+        self.depart_admonition(node)
+
+    def visit_table(self, node):
+        self.duclass_open(node)
+        self.requirements['table'] = PreambleCmds.table
+        if not self.settings.legacy_column_widths:
+            self.requirements['table1'] = PreambleCmds.table_columnwidth
+        if self.active_table.is_open():
+            self.table_stack.append(self.active_table)
+            # nesting longtable does not work (e.g. 2007-04-18)
+            self.active_table = Table(self, 'tabular')
+        # A longtable moves before \paragraph and \subparagraph
+        # section titles if it immediately follows them:
+        if (self.active_table._latex_type == 'longtable'
+            and isinstance(node.parent, nodes.section)
+            and node.parent.index(node) == 1
+            and self.d_class.section(
+                   self.section_level).find('paragraph') != -1):
+            self.out.append('\\leavevmode')
+        self.active_table.open()
+        self.active_table.set_table_style(node, self.settings)
+        if self.active_table.borders == 'booktabs':
+            self.requirements['booktabs'] = r'\usepackage{booktabs}'
+        self.push_output_collector([])
+
+    def depart_table(self, node):
+        # wrap content in the right environment:
+        content = self.out
+        self.pop_output_collector()
+        try:
+            width = self.to_latex_length(node['width'])
+        except KeyError:
+            width = r'\linewidth'
+        # Insert hyperlabel and anchor before the table
+        # if it has no caption/title.
+        # See visit_thead() for tables with caption.
+        if not self.active_table.caption:
+            self.out.extend(self.ids_to_labels(
+                node, set_anchor=len(self.table_stack) != 1,
+                newline=True))
+        # TODO: Don't use a longtable or add \noindent before
+        #       the next paragraph, when in a "compound paragraph".
+        #       Start a new line or a new paragraph?
+        #       if (isinstance(node.parent, nodes.compound)
+        #       and self._latex_type != 'longtable')?
+        self.out.append(self.active_table.get_opening(width))
+        self.out += content
+        self.out.append(self.active_table.get_closing() + '\n')
+        self.active_table.close()
+        if len(self.table_stack) > 0:
+            self.active_table = self.table_stack.pop()
+        self.duclass_close(node)
+
+    def visit_target(self, node):
+        # Skip indirect targets:
+        if ('refuri' in node        # external hyperlink
+            or 'refid' in node      # resolved internal link
+            or 'refname' in node):  # unresolved internal link
+            ## self.out.append('%% %s\n' % node)   # for debugging
+            return
+        self.out.append('%\n')
+        # do we need an anchor (\phantomsection)?
+        set_anchor = not isinstance(node.parent, (nodes.caption, nodes.title))
+        # TODO: where else can/must we omit the \phantomsection?
+        self.out += self.ids_to_labels(node, set_anchor)
+
+    def depart_target(self, node):
+        pass
+
+    def visit_tbody(self, node):
+        # BUG write preamble if not yet done (colspecs not [])
+        # for tables without heads.
+        if not self.active_table.get('preamble written'):
+            self.visit_thead(node)
+            self.depart_thead(None)
+
+    def depart_tbody(self, node):
+        pass
+
+    def visit_term(self, node):
+        """definition list term"""
+        # Commands with optional args inside an optional arg must be put
+        # in a group, e.g. ``\item[{\hyperref[label]{text}}]``.
+        self.out.append('\\item[{')
+
+    def depart_term(self, node):
+        self.out.append('}] ')
+        # Do we need a \leavevmode (line break if the field body begins
+        # with a list or environment)?
+        next_node = node.next_node(descend=False, siblings=True)
+        if isinstance(next_node, nodes.term):
+            self.out.append('\n')
+        elif not isinstance(next_node, nodes.classifier):
+            self.out.append(self.term_postfix(next_node))
+
+    def visit_tgroup(self, node):
+        pass
+
+    def depart_tgroup(self, node):
+        pass
+
+    _thead_depth = 0
+
+    def thead_depth(self):
+        return self._thead_depth
+
+    def visit_thead(self, node):
+        self._thead_depth += 1
+        if 1 == self.thead_depth():
+            self.out.append('{%s}\n' % self.active_table.get_colspecs(node))
+            self.active_table.set('preamble written', 1)
+        if self.active_table.caption:
+            if self._thead_depth == 1:
+                pre = [r'\caption{']
+                post = self.ids_to_labels(node.parent.parent, False) + [r'}\\']
+            else:
+                pre = [r'\caption[]{']
+                post = [r' (... continued)}\\']
+            self.out.extend(pre + self.active_table.caption + post + ['\n'])
+        self.out.extend(self.active_table.visit_thead())
+
+    def depart_thead(self, node):
+        if node is not None:
+            self.out.extend(self.active_table.depart_thead())
+            if self.active_table.need_recurse():
+                node.walkabout(self)
+        self._thead_depth -= 1
+
+    def visit_title(self, node):
+        """Append section and other titles."""
+        # Document title
+        if isinstance(node.parent, nodes.document):
+            self.push_output_collector(self.title)
+            self.context.append('')
+            self.pdfinfo.append('  pdftitle={%s},' %
+                                self.encode(node.astext()))
+        # Topic titles (topic, admonition, sidebar)
+        elif (isinstance(node.parent, nodes.topic)
+              or isinstance(node.parent, nodes.admonition)
+              or isinstance(node.parent, nodes.sidebar)):
+            classes = node.parent['classes'] or [node.parent.tagname]
+            if self.settings.legacy_class_functions:
+                self.fallbacks['title'] = PreambleCmds.title_legacy
+                self.out.append('\n\\DUtitle[%s]{' % ','.join(classes))
+            else:
+                if not self.fallback_stylesheet:
+                    self.fallbacks['title'] = PreambleCmds.title
+                self.out.append('\n\\DUtitle{')
+            self.context.append('}\n')
+        # Table caption
+        elif isinstance(node.parent, nodes.table):
+            self.push_output_collector(self.active_table.caption)
+            self.context.append('')
+        # Section title
+        else:
+            if hasattr(PreambleCmds, 'secnumdepth'):
+                self.requirements['secnumdepth'] = PreambleCmds.secnumdepth
+            level = self.section_level
+            section_name = self.d_class.section(level)
+            self.out.append('\n\n')
+            if level > len(self.d_class.sections):
+                # section level not supported by LaTeX
+                if self.settings.legacy_class_functions:
+                    self.fallbacks['title'] = PreambleCmds.title_legacy
+                    section_name += '[section%s]' % roman.toRoman(level)
+                else:
+                    if not self.fallback_stylesheet:
+                        self.fallbacks['title'] = PreambleCmds.title
+                        self.fallbacks['DUclass'] = PreambleCmds.duclass
+                    self.out.append('\\begin{DUclass}{section%s}\n'
+                                    % roman.toRoman(level))
+
+            # System messages heading in red:
+            if 'system-messages' in node.parent['classes']:
+                self.requirements['color'] = PreambleCmds.color
+                section_title = self.encode(node.astext())
+                self.out.append(r'\%s[%s]{\color{red}' % (
+                                section_name, section_title))
+            else:
+                self.out.append(r'\%s{' % section_name)
+
+            # label and ToC entry:
+            bookmark = ['']
+            # add sections with unsupported level to toc and pdfbookmarks?
+            ## if level > len(self.d_class.sections):
+            ##     section_title = self.encode(node.astext())
+            ##     bookmark.append(r'\addcontentsline{toc}{%s}{%s}' %
+            ##               (section_name, section_title))
+            bookmark += self.ids_to_labels(node.parent, set_anchor=False)
+            self.context.append('%\n  '.join(bookmark) + '%\n}\n')
+            if (level > len(self.d_class.sections)
+                and not self.settings.legacy_class_functions):
+                self.context[-1] += '\\end{DUclass}\n'
+            # MAYBE postfix paragraph and subparagraph with \leavevmode to
+            # ensure floats stay in the section and text starts on a new line.
+
+    def depart_title(self, node):
+        self.out.append(self.context.pop())
+        if isinstance(node.parent, (nodes.table, nodes.document)):
+            self.pop_output_collector()
+
+    def visit_contents(self, node):
+        """Write the table of contents.
+
+        Called from visit_topic() for "contents" topics.
+        """
+        # requirements/setup for local ToC with package "minitoc",
+        if self.use_latex_toc and 'local' in node['classes']:
+            section_name = self.d_class.section(self.section_level)
+            # minitoc only supports "part" and toplevel sections
+            minitoc_names = {'part': 'part',
+                             'chapter': 'mini',
+                             'section': 'sect'}
+            if 'chapter' in self.d_class.sections:
+                del minitoc_names['section']
+            try:
+                mtc_name = minitoc_names[section_name]
+            except KeyError:
+                self.warn('Skipping local ToC at "%s" level.\n'
+                          '  Feature not supported with option "use-latex-toc"'
+                          % section_name, base_node=node)
+                raise nodes.SkipNode
+
+        # labels and PDF bookmark (sidebar entry)
+        self.out.append('\n')  # start new paragraph
+        if node['names']:  # don't add labels just for auto-ids
+            self.out += self.ids_to_labels(node, newline=True)
+        if (isinstance(node.next_node(), nodes.title)
+            and 'local' not in node['classes']
+            and self.settings.documentclass != 'memoir'):
+            self.out.append('\\pdfbookmark[%d]{%s}{%s}\n' %
+                            (self.section_level+1,
+                             node.next_node().astext(),
+                             node.get('ids', ['contents'])[0]))
+
+        # Docutils generated contents list (no page numbers)
+        if not self.use_latex_toc:
+            self.fallbacks['toc-list'] = PreambleCmds.toc_list
+            self.duclass_open(node)
+            return
+
+        # ToC by LaTeX
+        depth = node.get('depth', 0)
+        maxdepth = len(self.d_class.sections)
+        if isinstance(node.next_node(), nodes.title):
+            title = self.encode(node[0].astext())
+        else:
+            title = ''
+        if 'local' in node['classes']:
+            # use the "minitoc" package
+            self.requirements['minitoc'] = PreambleCmds.minitoc
+            self.requirements['minitoc-'+mtc_name] = r'\do%stoc'%mtc_name
+            self.requirements['minitoc-%s-depth' % mtc_name] = (
+                r'\mtcsetdepth{%stoc}{%d}' % (mtc_name, maxdepth))
+            # "depth" option: Docutils stores a relative depth while
+            # minitoc  expects an absolute depth!:
+            offset = {'sect': 1, 'mini': 0, 'part': 0}
+            if 'chapter' in self.d_class.sections:
+                offset['part'] = -1
+            if depth:
+                self.out.append('\\setcounter{%stocdepth}{%d}' %
+                                (mtc_name, depth + offset[mtc_name]))
+            # title:
+            self.out.append('\\mtcsettitle{%stoc}{%s}\n' % (mtc_name, title))
+            # the toc-generating command:
+            self.out.append('\\%stoc\n' % mtc_name)
+        else:
+            if depth:
+                self.out.append('\\setcounter{tocdepth}{%d}\n'
+                                % self.d_class.latex_section_depth(depth))
+            if title != 'Contents':
+                self.out.append('\\renewcommand{\\contentsname}{%s}\n' % title)
+            self.out.append('\\tableofcontents\n')
+            self.has_latex_toc = True
+        # ignore rest of node content
+        raise nodes.SkipNode
+
+    def visit_topic(self, node):
+        # Topic nodes can be generic topic, abstract, dedication, or ToC.
+        # table of contents:
+        if 'contents' in node['classes']:
+            self.visit_contents(node)
+        elif ('abstract' in node['classes']
+              and self.settings.use_latex_abstract):
+            self.push_output_collector(self.abstract)
+            self.out.append('\\begin{abstract}')
+            if isinstance(node.next_node(), nodes.title):
+                node.pop(0)  # LaTeX provides its own title
+        else:
+            # special topics:
+            if 'abstract' in node['classes']:
+                if not self.fallback_stylesheet:
+                    self.fallbacks['abstract'] = PreambleCmds.abstract
+                if self.settings.legacy_class_functions:
+                    self.fallbacks['abstract'] = PreambleCmds.abstract_legacy
+                self.push_output_collector(self.abstract)
+            elif 'dedication' in node['classes']:
+                if not self.fallback_stylesheet:
+                    self.fallbacks['dedication'] = PreambleCmds.dedication
+                self.push_output_collector(self.dedication)
+            else:
+                node['classes'].insert(0, 'topic')
+            self.visit_block_quote(node)
+
+    def depart_topic(self, node):
+        if ('abstract' in node['classes']
+            and self.settings.use_latex_abstract):
+            self.out.append('\\end{abstract}\n')
+        elif 'contents' in node['classes']:
+            self.duclass_close(node)
+        else:
+            self.depart_block_quote(node)
+        if ('abstract' in node['classes']
+            or 'dedication' in node['classes']):
+            self.pop_output_collector()
+
+    def visit_transition(self, node):
+        if not self.fallback_stylesheet:
+            self.fallbacks['transition'] = PreambleCmds.transition
+        self.out.append('\n%' + '_' * 75 + '\n')
+        self.out.append('\\DUtransition\n')
+
+    def depart_transition(self, node):
+        pass
+
+    def visit_version(self, node):
+        self.visit_docinfo_item(node, 'version')
+
+    def depart_version(self, node):
+        self.depart_docinfo_item(node)
+
+    def unimplemented_visit(self, node):
+        raise NotImplementedError('visiting unimplemented node type: %s' %
+                                  node.__class__.__name__)
+
+#    def unknown_visit(self, node):
+#    def default_visit(self, node):
+
+# vim: set ts=4 et ai :
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex
new file mode 100644
index 00000000..86552ab6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex
@@ -0,0 +1,14 @@
+$head_prefix% generated by Docutils <https://docutils.sourceforge.io/>
+\usepackage{cmap} % fix search and cut-and-paste in Acrobat
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks
+$pdfsetup
+%%% Body
+\begin{document}
+$titledata$body_pre_docinfo$docinfo$dedication$abstract$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty
new file mode 100644
index 00000000..52386bb9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty
@@ -0,0 +1,223 @@
+%% docutils.sty: macros for Docutils LaTeX output.
+%%
+%%   Copyright © 2020 Günter Milde
+%%   Released under the terms of the `2-Clause BSD license`, in short:
+%%
+%%      Copying and distribution of this file, with or without modification,
+%%      are permitted in any medium without royalty provided the copyright
+%%      notice and this notice are preserved.
+%%      This file is offered as-is, without any warranty.
+
+% .. include:: README.md
+%
+% Implementation
+% ==============
+%
+% ::
+
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{docutils}
+  [2021/05/18 macros for Docutils LaTeX output]
+
+% Helpers
+% -------
+%
+% duclass::
+
+% class handling for environments (block-level elements)
+% \begin{DUclass}{spam} tries \DUCLASSspam and
+% \end{DUclass}{spam} tries \endDUCLASSspam
+\ifx\DUclass\undefined % poor man's "provideenvironment"
+ \newenvironment{DUclass}[1]%
+  {% "#1" does not work in end-part of environment.
+   \def\DocutilsClassFunctionName{DUCLASS#1}
+     \csname \DocutilsClassFunctionName \endcsname}%
+  {\csname end\DocutilsClassFunctionName \endcsname}%
+\fi
+
+% providelength::
+
+% Provide a length variable and set default, if it is new
+\providecommand*{\DUprovidelength}[2]{
+  \ifthenelse{\isundefined{#1}}{\newlength{#1}\setlength{#1}{#2}}{}
+}
+
+
+% Configuration defaults
+% ----------------------
+%
+% See `Docutils LaTeX Writer`_ for details.
+%
+% abstract::
+
+\providecommand*{\DUCLASSabstract}{
+  \renewcommand{\DUtitle}[1]{\centerline{\textbf{##1}}}
+}
+
+% dedication::
+
+% special topic for dedications
+\providecommand*{\DUCLASSdedication}{%
+  \renewenvironment{quote}{\begin{center}}{\end{center}}%
+}
+
+% TODO: add \em to set dedication text in italics?
+%
+% docinfo::
+
+% width of docinfo table
+\DUprovidelength{\DUdocinfowidth}{0.9\linewidth}
+
+% error::
+
+\providecommand*{\DUCLASSerror}{\color{red}}
+
+% highlight_rules::
+
+% basic code highlight:
+\providecommand*\DUrolecomment[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
+\providecommand*\DUroledeleted[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
+\providecommand*\DUrolekeyword[1]{\textbf{#1}}
+\providecommand*\DUrolestring[1]{\textit{#1}}
+
+% Elements
+% --------
+%
+% Definitions for unknown or to-be-configured Docutils elements.
+%
+% admonition::
+
+% admonition environment (specially marked topic)
+\ifx\DUadmonition\undefined % poor man's "provideenvironment"
+ \newbox{\DUadmonitionbox}
+ \newenvironment{DUadmonition}%
+  {\begin{center}
+     \begin{lrbox}{\DUadmonitionbox}
+       \begin{minipage}{0.9\linewidth}
+  }%
+  {    \end{minipage}
+     \end{lrbox}
+     \fbox{\usebox{\DUadmonitionbox}}
+   \end{center}
+  }
+\fi
+
+% fieldlist::
+
+% field list environment (for separate configuration of `field lists`)
+\ifthenelse{\isundefined{\DUfieldlist}}{
+  \newenvironment{DUfieldlist}%
+    {\quote\description}
+    {\enddescription\endquote}
+}{}
+
+% footnotes::
+
+% numerical or symbol footnotes with hyperlinks and backlinks
+\providecommand*{\DUfootnotemark}[3]{%
+  \raisebox{1em}{\hypertarget{#1}{}}%
+  \hyperlink{#2}{\textsuperscript{#3}}%
+}
+\providecommand{\DUfootnotetext}[4]{%
+  \begingroup%
+  \renewcommand{\thefootnote}{%
+    \protect\raisebox{1em}{\protect\hypertarget{#1}{}}%
+    \protect\hyperlink{#2}{#3}}%
+  \footnotetext{#4}%
+  \endgroup%
+}
+
+% inline::
+
+% custom inline roles: \DUrole{#1}{#2} tries \DUrole#1{#2}
+\providecommand*{\DUrole}[2]{%
+  \ifcsname DUrole#1\endcsname%
+    \csname DUrole#1\endcsname{#2}%
+  \else%
+    #2%
+  \fi%
+}
+
+% legend::
+
+% legend environment (in figures and formal tables)
+\ifthenelse{\isundefined{\DUlegend}}{
+  \newenvironment{DUlegend}{\small}{}
+}{}
+
+% lineblock::
+
+% line block environment
+\DUprovidelength{\DUlineblockindent}{2.5em}
+\ifthenelse{\isundefined{\DUlineblock}}{
+  \newenvironment{DUlineblock}[1]{%
+    \list{}{\setlength{\partopsep}{\parskip}
+            \addtolength{\partopsep}{\baselineskip}
+            \setlength{\topsep}{0pt}
+            \setlength{\itemsep}{0.15\baselineskip}
+            \setlength{\parsep}{0pt}
+            \setlength{\leftmargin}{#1}}
+    \raggedright
+  }
+  {\endlist}
+}{}
+
+% optionlist::
+
+% list of command line options
+\providecommand*{\DUoptionlistlabel}[1]{\bfseries #1 \hfill}
+\DUprovidelength{\DUoptionlistindent}{3cm}
+\ifthenelse{\isundefined{\DUoptionlist}}{
+  \newenvironment{DUoptionlist}{%
+    \list{}{\setlength{\labelwidth}{\DUoptionlistindent}
+            \setlength{\rightmargin}{1cm}
+            \setlength{\leftmargin}{\rightmargin}
+            \addtolength{\leftmargin}{\labelwidth}
+            \addtolength{\leftmargin}{\labelsep}
+            \renewcommand{\makelabel}{\DUoptionlistlabel}}
+  }
+  {\endlist}
+}{}
+
+% rubric::
+
+% informal heading
+\providecommand*{\DUrubric}[1]{\subsubsection*{\emph{#1}}}
+
+% sidebar::
+
+% text outside the main text flow
+\providecommand{\DUsidebar}[1]{%
+  \begin{center}
+    \colorbox[gray]{0.80}{\parbox{0.9\linewidth}{#1}}
+  \end{center}
+}
+
+% title::
+
+% title for topics, admonitions, unsupported section levels, and sidebar
+\providecommand*{\DUtitle}[1]{%
+  \smallskip\noindent\textbf{#1}\smallskip}
+
+% subtitle::
+
+% subtitle (for sidebar)
+\providecommand*{\DUsubtitle}[1]{\par\emph{#1}\smallskip}
+
+% documentsubtitle::
+
+% subtitle (in document title)
+\providecommand*{\DUdocumentsubtitle}[1]{{\large #1}}
+
+% titlereference::
+
+% titlereference standard role
+\providecommand*{\DUroletitlereference}[1]{\textsl{#1}}
+
+% transition::
+
+% transition (break / fancybreak / anonymous section)
+\providecommand*{\DUtransition}{%
+  \hspace*{\fill}\hrulefill\hspace*{\fill}
+  \vskip 0.5\baselineskip
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex
new file mode 100644
index 00000000..278fba80
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex
@@ -0,0 +1,19 @@
+% generated by Docutils <https://docutils.sourceforge.io/>
+$head_prefix
+\usepackage{cmap} % fix search and cut-and-paste in Acrobat
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks$pdfsetup
+$titledata
+%%% Body
+\begin{document}
+\begin{titlepage}
+$body_pre_docinfo$docinfo$dedication$abstract
+\thispagestyle{empty}
+\end{titlepage}
+$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex
new file mode 100644
index 00000000..1e96806d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex
@@ -0,0 +1,18 @@
+% generated by Docutils <https://docutils.sourceforge.io/>
+$head_prefix
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks$pdfsetup
+$titledata
+%%% Body
+\begin{document}
+\begin{titlingpage}
+\thispagestyle{empty}
+$body_pre_docinfo$docinfo$dedication$abstract
+\end{titlingpage}
+$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex
new file mode 100644
index 00000000..4d802805
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex
@@ -0,0 +1,21 @@
+$head_prefix% generated by Docutils <https://docutils.sourceforge.io/>
+% rubber: set program xelatex
+\usepackage{fontspec}
+% \defaultfontfeatures{Scale=MatchLowercase}
+% straight double quotes (defined T1 but missing in TU):
+\ifdefined \UnicodeEncodingName
+  \DeclareTextCommand{\textquotedbl}{\UnicodeEncodingName}{%
+    {\addfontfeatures{RawFeature=-tlig,Mapping=}\char34}}%
+\fi
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks$pdfsetup
+$titledata
+%%% Body
+\begin{document}
+$body_pre_docinfo$docinfo$dedication$abstract$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/manpage.py b/.venv/lib/python3.12/site-packages/docutils/writers/manpage.py
new file mode 100644
index 00000000..9c0ab479
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/manpage.py
@@ -0,0 +1,1214 @@
+# $Id: manpage.py 9610 2024-04-03 17:29:36Z grubert $
+# Author: Engelbert Gruber <grubert@users.sourceforge.net>
+# Copyright: This module is put into the public domain.
+
+"""
+Simple man page writer for reStructuredText.
+
+Man pages (short for "manual pages") contain system documentation on unix-like
+systems. The pages are grouped in numbered sections:
+
+ 1 executable programs and shell commands
+ 2 system calls
+ 3 library functions
+ 4 special files
+ 5 file formats
+ 6 games
+ 7 miscellaneous
+ 8 system administration
+
+Man pages are written *troff*, a text file formatting system.
+
+See https://www.tldp.org/HOWTO/Man-Page for a start.
+
+Man pages have no subsection only parts.
+Standard parts
+
+  NAME ,
+  SYNOPSIS ,
+  DESCRIPTION ,
+  OPTIONS ,
+  FILES ,
+  SEE ALSO ,
+  BUGS ,
+
+and
+
+  AUTHOR .
+
+A unix-like system keeps an index of the DESCRIPTIONs, which is accessible
+by the command whatis or apropos.
+
+"""
+
+__docformat__ = 'reStructuredText'
+
+import re
+
+from docutils import nodes, writers, languages
+try:
+    import roman
+except ImportError:
+    import docutils.utils.roman as roman
+
+FIELD_LIST_INDENT = 7
+DEFINITION_LIST_INDENT = 7
+OPTION_LIST_INDENT = 7
+BLOCKQOUTE_INDENT = 3.5
+LITERAL_BLOCK_INDENT = 3.5
+
+# Define two macros so man/roff can calculate the
+# indent/unindent margins by itself
+MACRO_DEF = (r""".
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+""")
+
+
+class Writer(writers.Writer):
+
+    supported = ('manpage',)
+    """Formats this writer supports."""
+
+    output = None
+    """Final translated form of `document`."""
+
+    def __init__(self):
+        writers.Writer.__init__(self)
+        self.translator_class = Translator
+
+    def translate(self):
+        visitor = self.translator_class(self.document)
+        self.document.walkabout(visitor)
+        self.output = visitor.astext()
+
+
+class Table:
+    def __init__(self):
+        self._rows = []
+        self._options = ['box', 'center']
+        self._tab_char = '\t'
+        self._coldefs = []
+
+    def new_row(self):
+        self._rows.append([])
+
+    def append_separator(self, separator):
+        """Append the separator for table head."""
+        self._rows.append([separator])
+
+    def append_cell(self, cell_lines):
+        """cell_lines is an array of lines"""
+        start = 0
+        if len(cell_lines) > 0 and cell_lines[0] == '.sp\n':
+            start = 1
+        self._rows[-1].append(cell_lines[start:])
+        if len(self._coldefs) < len(self._rows[-1]):
+            self._coldefs.append('l')
+
+    def _minimize_cell(self, cell_lines):
+        """Remove leading and trailing blank and ``.sp`` lines"""
+        while cell_lines and cell_lines[0] in ('\n', '.sp\n'):
+            del cell_lines[0]
+        while cell_lines and cell_lines[-1] in ('\n', '.sp\n'):
+            del cell_lines[-1]
+
+    def as_list(self):
+        text = ['.TS\n']
+        text.append(' '.join(self._options) + ';\n')
+        text.append('%s.\n' % ('|'.join(self._coldefs)))
+        for row in self._rows:
+            # row = array of cells. cell = array of lines.
+            text.append('T{\n')
+            for i in range(len(row)):
+                cell = row[i]
+                self._minimize_cell(cell)
+                text.extend(cell)
+                if not text[-1].endswith('\n'):
+                    text[-1] += '\n'
+                if i < len(row)-1:
+                    text.append('T}'+self._tab_char+'T{\n')
+                else:
+                    text.append('T}\n')
+            text.append('_\n')       # line between rows
+        text.pop()     # pop last "line between"
+        text.append('.TE\n')
+        return text
+
+
+class Translator(nodes.NodeVisitor):
+    """"""
+
+    words_and_spaces = re.compile(r'\S+| +|\n')
+    possibly_a_roff_command = re.compile(r'\.\w')
+    document_start = """Man page generated from reStructuredText."""
+    # TODO add "from docutils 0.21rc1."
+
+    def __init__(self, document):
+        nodes.NodeVisitor.__init__(self, document)
+        self.settings = settings = document.settings
+        lcode = settings.language_code
+        self.language = languages.get_language(lcode, document.reporter)
+        self.head = []
+        self.body = []
+        self.foot = []
+        self.section_level = 0
+        self.context = []
+        self.topic_class = ''
+        self.colspecs = []
+        self.compact_p = 1
+        self.compact_simple = None
+        # the list style "*" bullet or "#" numbered
+        self._list_char = []
+        # writing the header .TH and .SH NAME is postboned after
+        # docinfo.
+        self._docinfo = {
+                "title": "", "title_upper": "",
+                "subtitle": "",
+                "manual_section": "", "manual_group": "",
+                "author": [],
+                "date": "",
+                "copyright": "",
+                "version": "",
+                    }
+        self._docinfo_keys = []     # a list to keep the sequence as in source.
+        self._docinfo_names = {}    # to get name from text not normalized.
+        self._in_docinfo = None
+        self._field_name = None
+        self._active_table = None
+        self._has_a_table = False   # is there a table in this document
+        self._in_literal = False
+        self.header_written = 0
+        self._line_block = 0
+        self.authors = []
+        self.section_level = 0
+        self._indent = [0]
+        # central definition of simple processing rules
+        # what to output on : visit, depart
+        # Do not use paragraph requests ``.PP`` because these set indentation.
+        # use ``.sp``. Remove superfluous ``.sp`` in ``astext``.
+        #
+        # Fonts are put on a stack, the top one is used.
+        # ``.ft P`` or ``\\fP`` pop from stack.
+        # But ``.BI`` seams to fill stack with BIBIBIBIB...
+        # ``B`` bold, ``I`` italic, ``R`` roman should be available.
+        #
+        # Requests start wit a dot ``.`` or the no-break control character,
+        # a neutral apostrophe ``'`` suppresses the break implied by some
+        # requests.
+        self.defs = {
+                'indent': ('.INDENT %.1f\n', '.UNINDENT\n'),
+                'definition_list_item': ('.TP', ''),  # par. with hanging tag
+                'field_name': ('.TP\n.B ', '\n'),
+                'literal': ('\\fB', '\\fP'),
+                'literal_block': ('.sp\n.EX\n', '\n.EE\n'),
+
+                'option_list_item': ('.TP\n', ''),
+
+                'reference': (r'\fI\%', r'\fP'),
+                'emphasis': ('\\fI', '\\fP'),
+                'strong': ('\\fB', '\\fP'),
+                'title_reference': ('\\fI', '\\fP'),
+
+                'topic-title': ('.SS ',),
+                'sidebar-title': ('.SS ',),
+
+                'problematic': ('\n.nf\n', '\n.fi\n'),
+                    }
+        # NOTE do not specify the newline before a dot-command, but ensure
+        # it is there.
+
+    def comment_begin(self, text):
+        """Return commented version of the passed text WITHOUT end of
+        line/comment."""
+        prefix = '.\\" '
+        out_text = ''.join([(prefix + in_line + '\n')
+                            for in_line in text.split('\n')])
+        return out_text
+
+    def comment(self, text):
+        """Return commented version of the passed text."""
+        return self.comment_begin(text)+'.\n'
+
+    def ensure_eol(self):
+        """Ensure the last line in body is terminated by new line."""
+        if len(self.body) > 0 and self.body[-1][-1] != '\n':
+            self.body.append('\n')
+
+    def astext(self):
+        """Return the final formatted document as a string."""
+        if not self.header_written:
+            # ensure we get a ".TH" as viewers require it.
+            self.append_header()
+        # filter body
+        for i in range(len(self.body)-1, 0, -1):
+            # remove superfluous vertical gaps.
+            if self.body[i] == '.sp\n':
+                if self.body[i - 1][:4] in ('.BI ', '.IP '):
+                    self.body[i] = '.\n'
+                elif (self.body[i - 1][:3] == '.B '
+                      and self.body[i - 2][:4] == '.TP\n'):
+                    self.body[i] = '.\n'
+                elif (self.body[i - 1] == '\n'
+                      and not self.possibly_a_roff_command.match(
+                                  self.body[i - 2])
+                      and (self.body[i - 3][:7] == '.TP\n.B '
+                           or self.body[i - 3][:4] == '\n.B ')
+                      ):
+                    self.body[i] = '.\n'
+        return ''.join(self.head + self.body + self.foot)
+
+    def deunicode(self, text):
+        text = text.replace('\xa0', '\\ ')
+        text = text.replace('\u2020', '\\(dg')
+        return text
+
+    def visit_Text(self, node):
+        text = node.astext()
+        text = text.replace('\\', '\\e')
+        replace_pairs = [
+            ('-', '\\-'),
+            ('\'', '\\(aq'),
+            ('´', "\\'"),
+            ('`', '\\(ga'),
+            ('"', '\\(dq'),  # double quotes are a problem on macro lines
+            ]
+        for (in_char, out_markup) in replace_pairs:
+            text = text.replace(in_char, out_markup)
+        # unicode
+        text = self.deunicode(text)
+        # prevent interpretation of "." at line start
+        if text.startswith('.'):
+            text = '\\&' + text
+        if self._in_literal:
+            text = text.replace('\n.', '\n\\&.')
+        self.body.append(text)
+
+    def depart_Text(self, node):
+        pass
+
+    def list_start(self, node):
+        class EnumChar:
+            enum_style = {
+                    'bullet': '\\(bu',
+                    'emdash': '\\(em',
+                     }
+
+            def __init__(self, style):
+                self._style = style
+                if 'start' in node:
+                    self._cnt = node['start'] - 1
+                else:
+                    self._cnt = 0
+                self._indent = 2
+                if style == 'arabic':
+                    # indentation depends on number of children
+                    # and start value.
+                    self._indent = len(str(len(node.children)))
+                    self._indent += len(str(self._cnt)) + 1
+                elif style == 'loweralpha':
+                    self._cnt += ord('a') - 1
+                    self._indent = 3
+                elif style == 'upperalpha':
+                    self._cnt += ord('A') - 1
+                    self._indent = 3
+                elif style.endswith('roman'):
+                    self._indent = 5
+
+            def __next__(self):
+                if self._style == 'bullet':
+                    return self.enum_style[self._style]
+                elif self._style == 'emdash':
+                    return self.enum_style[self._style]
+                self._cnt += 1
+                # TODO add prefix postfix
+                if self._style == 'arabic':
+                    return "%d." % self._cnt
+                elif self._style in ('loweralpha', 'upperalpha'):
+                    return "%c." % self._cnt
+                elif self._style.endswith('roman'):
+                    res = roman.toRoman(self._cnt) + '.'
+                    if self._style.startswith('upper'):
+                        return res.upper()
+                    return res.lower()
+                else:
+                    return "%d." % self._cnt
+
+            def get_width(self):
+                return self._indent
+
+            def __repr__(self):
+                return 'enum_style-%s' % list(self._style)
+
+        if 'enumtype' in node:
+            self._list_char.append(EnumChar(node['enumtype']))
+        else:
+            self._list_char.append(EnumChar('bullet'))
+        if len(self._list_char) > 1:
+            # indent nested lists
+            self.indent(self._list_char[-2].get_width())
+        else:
+            self.indent(self._list_char[-1].get_width())
+
+    def list_end(self):
+        self.dedent()
+        self._list_char.pop()
+
+    def header(self):
+        th = (".TH \"%(title_upper)s\" \"%(manual_section)s\""
+              " \"%(date)s\" \"%(version)s\"") % self._docinfo
+        if self._docinfo["manual_group"]:
+            th += " \"%(manual_group)s\"" % self._docinfo
+        th += "\n"
+        sh_tmpl = (".SH NAME\n"
+                   "%(title)s \\- %(subtitle)s\n")
+        return th + sh_tmpl % self._docinfo
+
+    def append_header(self):
+        """append header with .TH and .SH NAME"""
+        # NOTE before everything
+        # .TH title_upper section date source manual
+        # BUT macros before .TH for whatis database generators.
+        if self.header_written:
+            return
+        self.head.append(MACRO_DEF)
+        self.head.append(self.header())
+        self.header_written = 1
+
+    def visit_address(self, node):
+        self.visit_docinfo_item(node, 'address')
+
+    def depart_address(self, node):
+        pass
+
+    def visit_admonition(self, node, name=None):
+        #
+        # Make admonitions a simple block quote
+        # with a strong heading
+        #
+        # Using .IP/.RE doesn't preserve indentation
+        # when admonitions contain bullets, literal,
+        # and/or block quotes.
+        #
+        if name:
+            # .. admonition:: has no name
+            self.body.append('.sp\n')
+            name = '%s%s:%s\n' % (
+                self.defs['strong'][0],
+                self.language.labels.get(name, name).upper(),
+                self.defs['strong'][1],
+                )
+            self.body.append(name)
+        self.visit_block_quote(node)
+
+    def depart_admonition(self, node):
+        self.depart_block_quote(node)
+
+    def visit_attention(self, node):
+        self.visit_admonition(node, 'attention')
+
+    depart_attention = depart_admonition
+
+    def visit_docinfo_item(self, node, name):
+        if name == 'author':
+            self._docinfo[name].append(node.astext())
+        else:
+            self._docinfo[name] = node.astext()
+        self._docinfo_keys.append(name)
+        raise nodes.SkipNode
+
+    def depart_docinfo_item(self, node):
+        pass
+
+    def visit_author(self, node):
+        self.visit_docinfo_item(node, 'author')
+
+    depart_author = depart_docinfo_item
+
+    def visit_authors(self, node):
+        # _author is called anyway.
+        pass
+
+    def depart_authors(self, node):
+        pass
+
+    def visit_block_quote(self, node):
+        # BUG/HACK: indent always uses the _last_ indentation,
+        # thus we need two of them.
+        self.indent(BLOCKQOUTE_INDENT)
+        self.indent(0)
+
+    def depart_block_quote(self, node):
+        self.dedent()
+        self.dedent()
+
+    def visit_bullet_list(self, node):
+        self.list_start(node)
+
+    def depart_bullet_list(self, node):
+        self.list_end()
+
+    def visit_caption(self, node):
+        pass
+
+    def depart_caption(self, node):
+        pass
+
+    def visit_caution(self, node):
+        self.visit_admonition(node, 'caution')
+
+    depart_caution = depart_admonition
+
+    def visit_citation(self, node):
+        num = node.astext().split(None, 1)[0]
+        num = num.strip()
+        self.body.append('.IP [%s] 5\n' % num)
+
+    def depart_citation(self, node):
+        pass
+
+    def visit_citation_reference(self, node):
+        self.body.append('['+node.astext()+']')
+        raise nodes.SkipNode
+
+    def visit_classifier(self, node):
+        self.body.append('(')
+
+    def depart_classifier(self, node):
+        self.body.append(')')
+        self.depart_term(node)  # close the term element after last classifier
+
+    def visit_colspec(self, node):
+        self.colspecs.append(node)
+
+    def depart_colspec(self, node):
+        pass
+
+    def write_colspecs(self):
+        self.body.append("%s.\n" % ('L '*len(self.colspecs)))
+
+    def visit_comment(self, node,
+                      sub=re.compile('-(?=-)').sub):
+        self.body.append(self.comment(node.astext()))
+        raise nodes.SkipNode
+
+    def visit_contact(self, node):
+        self.visit_docinfo_item(node, 'contact')
+
+    depart_contact = depart_docinfo_item
+
+    def visit_container(self, node):
+        pass
+
+    def depart_container(self, node):
+        pass
+
+    def visit_compound(self, node):
+        pass
+
+    def depart_compound(self, node):
+        pass
+
+    def visit_copyright(self, node):
+        self.visit_docinfo_item(node, 'copyright')
+
+    def visit_danger(self, node):
+        self.visit_admonition(node, 'danger')
+
+    depart_danger = depart_admonition
+
+    def visit_date(self, node):
+        self.visit_docinfo_item(node, 'date')
+
+    def visit_decoration(self, node):
+        pass
+
+    def depart_decoration(self, node):
+        pass
+
+    def visit_definition(self, node):
+        pass
+
+    def depart_definition(self, node):
+        pass
+
+    def visit_definition_list(self, node):
+        self.indent(DEFINITION_LIST_INDENT)
+
+    def depart_definition_list(self, node):
+        self.dedent()
+
+    def visit_definition_list_item(self, node):
+        self.body.append(self.defs['definition_list_item'][0])
+
+    def depart_definition_list_item(self, node):
+        self.body.append(self.defs['definition_list_item'][1])
+
+    def visit_description(self, node):
+        pass
+
+    def depart_description(self, node):
+        pass
+
+    def visit_docinfo(self, node):
+        self._in_docinfo = 1
+
+    def depart_docinfo(self, node):
+        self._in_docinfo = None
+        # NOTE nothing should be written before this
+        self.append_header()
+
+    def visit_doctest_block(self, node):
+        self.body.append(self.defs['literal_block'][0])
+        self._in_literal = True
+
+    def depart_doctest_block(self, node):
+        self._in_literal = False
+        self.body.append(self.defs['literal_block'][1])
+
+    def visit_document(self, node):
+        # no blank line between comment and header.
+        self.head.append(self.comment(self.document_start).rstrip()+'\n')
+        # writing header is postponed
+        self.header_written = 0
+
+    def depart_document(self, node):
+        if self._docinfo['author']:
+            self.body.append('.SH AUTHOR\n%s\n'
+                             % ', '.join(self._docinfo['author']))
+        skip = ('author', 'copyright', 'date',
+                'manual_group', 'manual_section',
+                'subtitle',
+                'title', 'title_upper', 'version')
+        for name in self._docinfo_keys:
+            if name == 'address':
+                self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
+                                    self.language.labels.get(name, name),
+                                    self.defs['indent'][0] % 0,
+                                    self.defs['indent'][0] % BLOCKQOUTE_INDENT,
+                                    self._docinfo[name],
+                                    self.defs['indent'][1],
+                                    self.defs['indent'][1]))
+            elif name not in skip:
+                if name in self._docinfo_names:
+                    label = self._docinfo_names[name]
+                else:
+                    label = self.language.labels.get(name, name)
+                self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
+        if self._docinfo['copyright']:
+            self.body.append('.SH COPYRIGHT\n%s\n'
+                             % self._docinfo['copyright'])
+        self.body.append(self.comment('Generated by docutils manpage writer.'))
+
+    def visit_emphasis(self, node):
+        self.body.append(self.defs['emphasis'][0])
+
+    def depart_emphasis(self, node):
+        self.body.append(self.defs['emphasis'][1])
+
+    def visit_entry(self, node):
+        # a cell in a table row
+        if 'morerows' in node:
+            self.document.reporter.warning(
+                '"table row spanning" not supported', base_node=node)
+        if 'morecols' in node:
+            self.document.reporter.warning(
+                '"table cell spanning" not supported', base_node=node)
+        self.context.append(len(self.body))
+
+    def depart_entry(self, node):
+        start = self.context.pop()
+        self._active_table.append_cell(self.body[start:])
+        del self.body[start:]
+
+    def visit_enumerated_list(self, node):
+        self.list_start(node)
+
+    def depart_enumerated_list(self, node):
+        self.list_end()
+
+    def visit_error(self, node):
+        self.visit_admonition(node, 'error')
+
+    depart_error = depart_admonition
+
+    def visit_field(self, node):
+        pass
+
+    def depart_field(self, node):
+        pass
+
+    def visit_field_body(self, node):
+        if self._in_docinfo:
+            name_normalized = self._field_name.lower().replace(" ", "_")
+            self._docinfo_names[name_normalized] = self._field_name
+            self.visit_docinfo_item(node, name_normalized)
+            raise nodes.SkipNode
+
+    def depart_field_body(self, node):
+        pass
+
+    def visit_field_list(self, node):
+        self.indent(FIELD_LIST_INDENT)
+
+    def depart_field_list(self, node):
+        self.dedent()
+
+    def visit_field_name(self, node):
+        if self._in_docinfo:
+            self._field_name = node.astext()
+            raise nodes.SkipNode
+        else:
+            self.body.append(self.defs['field_name'][0])
+
+    def depart_field_name(self, node):
+        self.body.append(self.defs['field_name'][1])
+
+    def visit_figure(self, node):
+        self.indent(2.5)
+        self.indent(0)
+
+    def depart_figure(self, node):
+        self.dedent()
+        self.dedent()
+
+    def visit_footer(self, node):
+        self.document.reporter.warning('"footer" not supported',
+                                       base_node=node)
+        # avoid output the link to document source
+        raise nodes.SkipNode
+
+    def depart_footer(self, node):
+        pass
+
+    def visit_footnote(self, node):
+        num, text = node.astext().split(None, 1)
+        num = num.strip()
+        self.body.append('.IP [%s] 5\n' % self.deunicode(num))
+
+    def depart_footnote(self, node):
+        pass
+
+    def footnote_backrefs(self, node):
+        self.document.reporter.warning('"footnote_backrefs" not supported',
+                                       base_node=node)
+
+    def visit_footnote_reference(self, node):
+        self.body.append('['+self.deunicode(node.astext())+']')
+        raise nodes.SkipNode
+
+    def depart_footnote_reference(self, node):
+        pass
+
+    def visit_generated(self, node):
+        pass
+
+    def depart_generated(self, node):
+        pass
+
+    def visit_header(self, node):
+        raise NotImplementedError(node.astext())
+
+    def depart_header(self, node):
+        pass
+
+    def visit_hint(self, node):
+        self.visit_admonition(node, 'hint')
+
+    depart_hint = depart_admonition
+
+    def visit_subscript(self, node):
+        self.body.append('\\s-2\\d')
+
+    def depart_subscript(self, node):
+        self.body.append('\\u\\s0')
+
+    def visit_superscript(self, node):
+        self.body.append('\\s-2\\u')
+
+    def depart_superscript(self, node):
+        self.body.append('\\d\\s0')
+
+    def visit_attribution(self, node):
+        self.body.append('\\(em ')
+
+    def depart_attribution(self, node):
+        self.body.append('\n')
+
+    def visit_image(self, node):
+        self.document.reporter.warning('"image" not supported',
+                                       base_node=node)
+        text = []
+        if 'alt' in node.attributes:
+            text.append(node.attributes['alt'])
+        if 'uri' in node.attributes:
+            text.append(node.attributes['uri'])
+        self.body.append('[image: %s]\n' % ('/'.join(text)))
+        raise nodes.SkipNode
+
+    def visit_important(self, node):
+        self.visit_admonition(node, 'important')
+
+    depart_important = depart_admonition
+
+    def visit_inline(self, node):
+        pass
+
+    def depart_inline(self, node):
+        pass
+
+    def visit_label(self, node):
+        # footnote and citation
+        if (isinstance(node.parent, nodes.footnote)
+            or isinstance(node.parent, nodes.citation)):
+            raise nodes.SkipNode
+        self.document.reporter.warning('"unsupported "label"',
+                                       base_node=node)
+        self.body.append('[')
+
+    def depart_label(self, node):
+        self.body.append(']\n')
+
+    def visit_legend(self, node):
+        pass
+
+    def depart_legend(self, node):
+        pass
+
+    # WHAT should we use .INDENT, .UNINDENT ?
+    def visit_line_block(self, node):
+        self._line_block += 1
+        if self._line_block == 1:
+            # TODO: separate inline blocks from previous paragraphs
+            # see http://hg.intevation.org/mercurial/crew/rev/9c142ed9c405
+            # self.body.append('.sp\n')
+            # but it does not work for me.
+            self.body.append('.nf\n')
+        else:
+            self.body.append('.in +2\n')
+
+    def depart_line_block(self, node):
+        self._line_block -= 1
+        if self._line_block == 0:
+            self.body.append('.fi\n')
+            self.body.append('.sp\n')
+        else:
+            self.body.append('.in -2\n')
+
+    def visit_line(self, node):
+        pass
+
+    def depart_line(self, node):
+        self.body.append('\n')
+
+    def visit_list_item(self, node):
+        # man 7 man argues to use ".IP" instead of ".TP"
+        self.body.append('.IP %s %d\n' % (
+                next(self._list_char[-1]),
+                self._list_char[-1].get_width(),))
+
+    def depart_list_item(self, node):
+        pass
+
+    def visit_literal(self, node):
+        self.body.append(self.defs['literal'][0])
+
+    def depart_literal(self, node):
+        self.body.append(self.defs['literal'][1])
+
+    def visit_literal_block(self, node):
+        # BUG/HACK: indent always uses the _last_ indentation,
+        # thus we need two of them.
+        self.indent(LITERAL_BLOCK_INDENT)
+        self.indent(0)
+        self.body.append(self.defs['literal_block'][0])
+        self._in_literal = True
+
+    def depart_literal_block(self, node):
+        self._in_literal = False
+        self.body.append(self.defs['literal_block'][1])
+        self.dedent()
+        self.dedent()
+
+    def visit_math(self, node):
+        self.document.reporter.warning('"math" role not supported',
+                                       base_node=node)
+        self.visit_literal(node)
+
+    def depart_math(self, node):
+        self.depart_literal(node)
+
+    def visit_math_block(self, node):
+        self.document.reporter.warning('"math" directive not supported',
+                                       base_node=node)
+        self.visit_literal_block(node)
+
+    def depart_math_block(self, node):
+        self.depart_literal_block(node)
+
+    # <meta> shall become an optional standard node:
+    # def visit_meta(self, node):
+    #     raise NotImplementedError(node.astext())
+
+    # def depart_meta(self, node):
+    #     pass
+
+    def visit_note(self, node):
+        self.visit_admonition(node, 'note')
+
+    depart_note = depart_admonition
+
+    def indent(self, by=0.5):
+        # if we are in a section ".SH" there already is a .RS
+        step = self._indent[-1]
+        self._indent.append(by)
+        self.body.append(self.defs['indent'][0] % step)
+
+    def dedent(self):
+        self._indent.pop()
+        self.body.append(self.defs['indent'][1])
+
+    def visit_option_list(self, node):
+        self.indent(OPTION_LIST_INDENT)
+
+    def depart_option_list(self, node):
+        self.dedent()
+
+    def visit_option_list_item(self, node):
+        # one item of the list
+        self.body.append(self.defs['option_list_item'][0])
+
+    def depart_option_list_item(self, node):
+        self.body.append(self.defs['option_list_item'][1])
+
+    def visit_option_group(self, node):
+        # as one option could have several forms it is a group
+        # options without parameter bold only, .B, -v
+        # options with parameter bold italic, .BI, -f file
+        #
+        # we do not know if .B or .BI, blind guess:
+        self.context.append('.B ')  # Add blank for sphinx (docutils/bugs/380)
+        self.context.append(len(self.body))  # to be able to insert later
+        self.context.append(0)               # option counter
+
+    def depart_option_group(self, node):
+        self.context.pop()  # the counter
+        start_position = self.context.pop()
+        text = self.body[start_position:]
+        del self.body[start_position:]
+        self.body.append('%s%s\n' % (self.context.pop(), ''.join(text)))
+
+    def visit_option(self, node):
+        # each form of the option will be presented separately
+        if self.context[-1] > 0:
+            if self.context[-3] == '.BI':
+                self.body.append('\\fR,\\fB ')
+            else:
+                self.body.append('\\fP,\\fB ')
+        if self.context[-3] == '.BI':
+            self.body.append('\\')
+        self.body.append(' ')
+
+    def depart_option(self, node):
+        self.context[-1] += 1
+
+    def visit_option_string(self, node):
+        # do not know if .B or .BI
+        pass
+
+    def depart_option_string(self, node):
+        pass
+
+    def visit_option_argument(self, node):
+        self.context[-3] = '.BI'  # bold/italic alternate
+        if node['delimiter'] != ' ':
+            self.body.append('\\fB%s ' % node['delimiter'])
+        elif self.body[len(self.body)-1].endswith('='):
+            # a blank only means no blank in output, just changing font
+            self.body.append(' ')
+        else:
+            # blank backslash blank, switch font then a blank
+            self.body.append(' \\ ')
+
+    def depart_option_argument(self, node):
+        pass
+
+    def visit_organization(self, node):
+        self.visit_docinfo_item(node, 'organization')
+
+    def depart_organization(self, node):
+        pass
+
+    def first_child(self, node):
+        first = isinstance(node.parent[0], nodes.label)  # skip label
+        for child in node.parent.children[first:]:
+            if isinstance(child, nodes.Invisible):
+                continue
+            if child is node:
+                return 1
+            break
+        return 0
+
+    def visit_paragraph(self, node):
+        # ``.PP`` : Start standard indented paragraph.
+        # ``.LP`` : Start block paragraph, all except the first.
+        # ``.P [type]``  : Start paragraph type.
+        # NOTE do not use paragraph starts because they reset indentation.
+        # ``.sp`` is only vertical space
+        self.ensure_eol()
+        if not self.first_child(node):
+            self.body.append('.sp\n')
+        # set in literal to escape dots after a new-line-character
+        self._in_literal = True
+
+    def depart_paragraph(self, node):
+        self._in_literal = False
+        self.body.append('\n')
+
+    def visit_problematic(self, node):
+        self.body.append(self.defs['problematic'][0])
+
+    def depart_problematic(self, node):
+        self.body.append(self.defs['problematic'][1])
+
+    def visit_raw(self, node):
+        if 'manpage' in node.get('format', '').split():
+            self.body.append(node.astext() + "\n")
+        # Keep non-manpage raw text out of output:
+        raise nodes.SkipNode
+
+    def visit_reference(self, node):
+        """E.g. link or email address."""
+        # .UR and .UE macros in roff use OSC8 escape sequences
+        # which are not supported everywhere yet
+        # therefore make the markup ourself
+        if 'refuri' in node:
+            # if content has the "email" do not output "mailto:email"
+            if node['refuri'].endswith(node.astext()):
+                self.body.append(" <")
+        #TODO elif 'refid' in node:
+
+    def depart_reference(self, node):
+        if 'refuri' in node:
+            # if content has the "email" do not output "mailto:email"
+            if node['refuri'].endswith(node.astext()):
+                self.body.append("> ")
+            else:
+                self.body.append(" <%s>\n" % node['refuri'])
+        #TODO elif 'refid' in node:
+
+    def visit_revision(self, node):
+        self.visit_docinfo_item(node, 'revision')
+
+    depart_revision = depart_docinfo_item
+
+    def visit_row(self, node):
+        self._active_table.new_row()
+
+    def depart_row(self, node):
+        pass
+
+    def visit_section(self, node):
+        self.section_level += 1
+
+    def depart_section(self, node):
+        self.section_level -= 1
+
+    def visit_status(self, node):
+        self.visit_docinfo_item(node, 'status')
+
+    depart_status = depart_docinfo_item
+
+    def visit_strong(self, node):
+        self.body.append(self.defs['strong'][0])
+
+    def depart_strong(self, node):
+        self.body.append(self.defs['strong'][1])
+
+    def visit_substitution_definition(self, node):
+        """Internal only."""
+        raise nodes.SkipNode
+
+    def visit_substitution_reference(self, node):
+        self.document.reporter.warning(
+            '"substitution_reference" not supported', base_node=node)
+
+    def visit_subtitle(self, node):
+        if isinstance(node.parent, nodes.sidebar):
+            self.body.append(self.defs['strong'][0])
+        elif isinstance(node.parent, nodes.document):
+            self.visit_docinfo_item(node, 'subtitle')
+        elif isinstance(node.parent, nodes.section):
+            self.body.append(self.defs['strong'][0])
+
+    def depart_subtitle(self, node):
+        # document subtitle calls SkipNode
+        self.body.append(self.defs['strong'][1]+'\n.PP\n')
+
+    def visit_system_message(self, node):
+        # TODO add report_level
+        # if node['level'] < self.document.reporter['writer'].report_level:
+        #    Level is too low to display:
+        #    raise nodes.SkipNode
+        attr = {}
+        if node.hasattr('id'):
+            attr['name'] = node['id']
+        if node.hasattr('line'):
+            line = ', line %s' % node['line']
+        else:
+            line = ''
+        self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
+                         % (node['type'], node['level'], node['source'], line))
+
+    def depart_system_message(self, node):
+        pass
+
+    def visit_table(self, node):
+        self._active_table = Table()
+        if not self._has_a_table:
+            self._has_a_table = True
+            # the comment to hint that preprocessor tbl should be called
+            self.head.insert(0, "'\\\" t\n")  # ``'\" t`` + newline
+
+    def depart_table(self, node):
+        self.ensure_eol()
+        self.body.extend(self._active_table.as_list())
+        self._active_table = None
+
+    def visit_target(self, node):
+        # targets are in-document hyper targets, without any use for man-pages.
+        raise nodes.SkipNode
+
+    def visit_tbody(self, node):
+        pass
+
+    def depart_tbody(self, node):
+        pass
+
+    def visit_term(self, node):
+        self.body.append('\n.B ')
+
+    def depart_term(self, node):
+        _next = node.next_node(None, descend=False, siblings=True)
+        # Nest (optional) classifier(s) in the <term> element
+        if isinstance(_next, nodes.classifier):
+            self.body.append(' ')
+            return  # skip (depart_classifier() calls this function again)
+        if isinstance(_next, nodes.term):
+            self.body.append('\n.TQ')
+        else:
+            self.body.append('\n')
+
+    def visit_tgroup(self, node):
+        pass
+
+    def depart_tgroup(self, node):
+        pass
+
+    def visit_thead(self, node):
+        # MAYBE double line '='
+        pass
+
+    def depart_thead(self, node):
+        # MAYBE double line '='
+        pass
+
+    def visit_tip(self, node):
+        self.visit_admonition(node, 'tip')
+
+    depart_tip = depart_admonition
+
+    def visit_title(self, node):
+        if isinstance(node.parent, nodes.topic):
+            self.body.append(self.defs['topic-title'][0])
+        elif isinstance(node.parent, nodes.sidebar):
+            self.body.append(self.defs['sidebar-title'][0])
+        elif isinstance(node.parent, nodes.admonition):
+            self.body.append('.IP "')
+        elif self.section_level == 0:
+            self._docinfo['title'] = node.astext()
+            # document title for .TH
+            self._docinfo['title_upper'] = node.astext().upper()
+            raise nodes.SkipNode
+        elif self.section_level == 1:
+            self.body.append('.SH %s\n'%self.deunicode(node.astext().upper()))
+            raise nodes.SkipNode
+        else:
+            self.body.append('.SS ')
+
+    def depart_title(self, node):
+        if isinstance(node.parent, nodes.admonition):
+            self.body.append('"')
+        self.body.append('\n')
+
+    def visit_title_reference(self, node):
+        """inline citation reference"""
+        self.body.append(self.defs['title_reference'][0])
+
+    def depart_title_reference(self, node):
+        self.body.append(self.defs['title_reference'][1])
+
+    def visit_topic(self, node):
+        pass
+
+    def depart_topic(self, node):
+        pass
+
+    def visit_sidebar(self, node):
+        pass
+
+    def depart_sidebar(self, node):
+        pass
+
+    def visit_rubric(self, node):
+        pass
+
+    def depart_rubric(self, node):
+        self.body.append('\n')
+
+    def visit_transition(self, node):
+        # .PP      Begin a new paragraph and reset prevailing indent.
+        # .sp N    leaves N lines of blank space.
+        # .ce      centers the next line
+        self.body.append('\n.sp\n.ce\n----\n')
+
+    def depart_transition(self, node):
+        self.body.append('\n.ce 0\n.sp\n')
+
+    def visit_version(self, node):
+        self.visit_docinfo_item(node, 'version')
+
+    def visit_warning(self, node):
+        self.visit_admonition(node, 'warning')
+
+    depart_warning = depart_admonition
+
+    def unimplemented_visit(self, node):
+        raise NotImplementedError('visiting unimplemented node type: %s'
+                                  % node.__class__.__name__)
+
+# vim: set fileencoding=utf-8 et ts=4 ai :
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/null.py b/.venv/lib/python3.12/site-packages/docutils/writers/null.py
new file mode 100644
index 00000000..db4c6720
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/null.py
@@ -0,0 +1,25 @@
+# $Id: null.py 9352 2023-04-17 20:26:41Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+A do-nothing Writer.
+
+`self.output` will change from ``None`` to the empty string
+in Docutils 0.22.
+"""
+
+from docutils import writers
+
+
+class Writer(writers.UnfilteredWriter):
+
+    supported = ('null',)
+    """Formats this writer supports."""
+
+    config_section = 'null writer'
+    config_section_dependencies = ('writers',)
+
+    def translate(self):
+        # output = None   # TODO in 0.22
+        pass
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py
new file mode 100644
index 00000000..c538af34
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py
@@ -0,0 +1,3461 @@
+# $Id: __init__.py 9541 2024-02-17 10:37:13Z milde $
+# Author: Dave Kuhlman <dkuhlman@davekuhlman.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Open Document Format (ODF) Writer.
+
+This module is provisional:
+the API is not settled and may change with any minor Docutils version.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from configparser import ConfigParser
+import copy
+from io import StringIO
+import itertools
+import locale
+import os
+import os.path
+from pathlib import Path
+import re
+import subprocess
+import tempfile
+import time
+import urllib
+import weakref
+from xml.etree import ElementTree as etree
+from xml.dom import minidom
+import zipfile
+
+import docutils
+from docutils import frontend, nodes, utils, writers, languages
+from docutils.parsers.rst.directives.images import PIL  # optional
+from docutils.readers import standalone
+from docutils.transforms import references
+
+# Import pygments and odtwriter pygments formatters if possible.
+try:
+    import pygments
+    import pygments.lexers
+    from .pygmentsformatter import (OdtPygmentsProgFormatter,
+                                    OdtPygmentsLaTeXFormatter)
+except (ImportError, SyntaxError):
+    pygments = None
+
+# import warnings
+# warnings.warn('importing IPShellEmbed', UserWarning)
+# from IPython.Shell import IPShellEmbed
+# args = ['-pdb', '-pi1', 'In <\\#>: ', '-pi2', '   .\\D.: ',
+#         '-po', 'Out<\\#>: ', '-nosep']
+# ipshell = IPShellEmbed(args,
+#                        banner = 'Entering IPython.  Press Ctrl-D to exit.',
+#                        exit_msg = 'Leaving Interpreter, back to program.')
+
+VERSION = '1.0a'
+
+IMAGE_NAME_COUNTER = itertools.count()
+
+
+#
+# ElementTree does not support getparent method (lxml does).
+# This wrapper class and the following support functions provide
+#   that support for the ability to get the parent of an element.
+#
+_parents = weakref.WeakKeyDictionary()
+if isinstance(etree.Element, type):
+    _ElementInterface = etree.Element
+else:
+    _ElementInterface = etree._ElementInterface
+
+
+class _ElementInterfaceWrapper(_ElementInterface):
+    def __init__(self, tag, attrib=None):
+        _ElementInterface.__init__(self, tag, attrib)
+        _parents[self] = None
+
+    def setparent(self, parent):
+        _parents[self] = parent
+
+    def getparent(self):
+        return _parents[self]
+
+
+#
+# Constants and globals
+
+SPACES_PATTERN = re.compile(r'( +)')
+TABS_PATTERN = re.compile(r'(\t+)')
+FILL_PAT1 = re.compile(r'^ +')
+FILL_PAT2 = re.compile(r' {2,}')
+
+TABLESTYLEPREFIX = 'rststyle-table-'
+TABLENAMEDEFAULT = '%s0' % TABLESTYLEPREFIX
+TABLEPROPERTYNAMES = (
+    'border', 'border-top', 'border-left',
+    'border-right', 'border-bottom', )
+
+GENERATOR_DESC = 'Docutils.org/odf_odt'
+
+NAME_SPACE_1 = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'
+
+CONTENT_NAMESPACE_DICT = CNSD = {
+    # 'office:version': '1.0',
+    'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+    'dc': 'http://purl.org/dc/elements/1.1/',
+    'dom': 'http://www.w3.org/2001/xml-events',
+    'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+    'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+    'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+    'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+    'math': 'http://www.w3.org/1998/Math/MathML',
+    'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+    'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+    'office': NAME_SPACE_1,
+    'ooo': 'http://openoffice.org/2004/office',
+    'oooc': 'http://openoffice.org/2004/calc',
+    'ooow': 'http://openoffice.org/2004/writer',
+    'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+
+    'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+    'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+    'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+    'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+    'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+    'xforms': 'http://www.w3.org/2002/xforms',
+    'xlink': 'http://www.w3.org/1999/xlink',
+    'xsd': 'http://www.w3.org/2001/XMLSchema',
+    'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+}
+
+STYLES_NAMESPACE_DICT = SNSD = {
+    # 'office:version': '1.0',
+    'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+    'dc': 'http://purl.org/dc/elements/1.1/',
+    'dom': 'http://www.w3.org/2001/xml-events',
+    'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+    'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+    'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+    'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+    'math': 'http://www.w3.org/1998/Math/MathML',
+    'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+    'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+    'office': NAME_SPACE_1,
+    'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+    'ooo': 'http://openoffice.org/2004/office',
+    'oooc': 'http://openoffice.org/2004/calc',
+    'ooow': 'http://openoffice.org/2004/writer',
+    'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+    'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+    'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+    'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+    'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+    'xlink': 'http://www.w3.org/1999/xlink',
+}
+
+MANIFEST_NAMESPACE_DICT = MANNSD = {
+    'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
+}
+
+META_NAMESPACE_DICT = METNSD = {
+    # 'office:version': '1.0',
+    'dc': 'http://purl.org/dc/elements/1.1/',
+    'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+    'office': NAME_SPACE_1,
+    'ooo': 'http://openoffice.org/2004/office',
+    'xlink': 'http://www.w3.org/1999/xlink',
+}
+
+# Attribute dictionaries for use with ElementTree, which
+# does not support use of nsmap parameter on Element() and SubElement().
+
+CONTENT_NAMESPACE_ATTRIB = {
+    # 'office:version': '1.0',
+    'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+    'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
+    'xmlns:dom': 'http://www.w3.org/2001/xml-events',
+    'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+    'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+    'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+    'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+    'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
+    'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+    'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+    'xmlns:office': NAME_SPACE_1,
+    'xmlns:presentation':
+        'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+    'xmlns:ooo': 'http://openoffice.org/2004/office',
+    'xmlns:oooc': 'http://openoffice.org/2004/calc',
+    'xmlns:ooow': 'http://openoffice.org/2004/writer',
+    'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+    'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+    'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+    'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+    'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+    'xmlns:xforms': 'http://www.w3.org/2002/xforms',
+    'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+    'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
+    'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+}
+
+STYLES_NAMESPACE_ATTRIB = {
+    # 'office:version': '1.0',
+    'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+    'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
+    'xmlns:dom': 'http://www.w3.org/2001/xml-events',
+    'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+    'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+    'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+    'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+    'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
+    'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+    'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+    'xmlns:office': NAME_SPACE_1,
+    'xmlns:presentation':
+        'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+    'xmlns:ooo': 'http://openoffice.org/2004/office',
+    'xmlns:oooc': 'http://openoffice.org/2004/calc',
+    'xmlns:ooow': 'http://openoffice.org/2004/writer',
+    'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+    'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+    'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+    'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+    'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+    'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+}
+
+MANIFEST_NAMESPACE_ATTRIB = {
+    'xmlns:manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
+}
+
+META_NAMESPACE_ATTRIB = {
+    # 'office:version': '1.0',
+    'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
+    'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+    'xmlns:office': NAME_SPACE_1,
+    'xmlns:ooo': 'http://openoffice.org/2004/office',
+    'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+}
+
+
+#
+# Functions
+#
+
+#
+# ElementTree support functions.
+# In order to be able to get the parent of elements, must use these
+#   instead of the functions with same name provided by ElementTree.
+#
+def Element(tag, attrib=None, nsmap=None, nsdict=CNSD):
+    if attrib is None:
+        attrib = {}
+    tag, attrib = fix_ns(tag, attrib, nsdict)
+    return _ElementInterfaceWrapper(tag, attrib)
+
+
+def SubElement(parent, tag, attrib=None, nsmap=None, nsdict=CNSD):
+    if attrib is None:
+        attrib = {}
+    tag, attrib = fix_ns(tag, attrib, nsdict)
+    el = _ElementInterfaceWrapper(tag, attrib)
+    parent.append(el)
+    el.setparent(parent)
+    return el
+
+
+def fix_ns(tag, attrib, nsdict):
+    nstag = add_ns(tag, nsdict)
+    nsattrib = {}
+    for key, val in list(attrib.items()):
+        nskey = add_ns(key, nsdict)
+        nsattrib[nskey] = val
+    return nstag, nsattrib
+
+
+def add_ns(tag, nsdict=CNSD):
+    return tag
+
+
+def ToString(et):
+    outstream = StringIO()
+    et.write(outstream, encoding="unicode")
+    s1 = outstream.getvalue()
+    outstream.close()
+    return s1
+
+
+def escape_cdata(text):
+    text = text.replace("&", "&amp;")
+    text = text.replace("<", "&lt;")
+    text = text.replace(">", "&gt;")
+    ascii = ''
+    for char in text:
+        if ord(char) >= ord("\x7f"):
+            ascii += "&#x%X;" % (ord(char), )
+        else:
+            ascii += char
+    return ascii
+
+
+#
+# Classes
+#
+
+
+class TableStyle:
+    def __init__(self, border=None, backgroundcolor=None):
+        self.border = border
+        self.backgroundcolor = backgroundcolor
+
+    def get_border_(self):
+        return self.border_
+
+    def set_border_(self, border):
+        self.border_ = border
+
+    border = property(get_border_, set_border_)
+
+    def get_backgroundcolor_(self):
+        return self.backgroundcolor_
+
+    def set_backgroundcolor_(self, backgroundcolor):
+        self.backgroundcolor_ = backgroundcolor
+    backgroundcolor = property(get_backgroundcolor_, set_backgroundcolor_)
+
+
+BUILTIN_DEFAULT_TABLE_STYLE = TableStyle(
+    border='0.0007in solid #000000')
+
+
+#
+# Information about the indentation level for lists nested inside
+#   other contexts, e.g. dictionary lists.
+class ListLevel:
+    def __init__(self, level, sibling_level=True, nested_level=True):
+        self.level = level
+        self.sibling_level = sibling_level
+        self.nested_level = nested_level
+
+    def set_sibling(self, sibling_level):
+        self.sibling_level = sibling_level
+
+    def get_sibling(self):
+        return self.sibling_level
+
+    def set_nested(self, nested_level):
+        self.nested_level = nested_level
+
+    def get_nested(self):
+        return self.nested_level
+
+    def set_level(self, level):
+        self.level = level
+
+    def get_level(self):
+        return self.level
+
+
+class Writer(writers.Writer):
+
+    MIME_TYPE = 'application/vnd.oasis.opendocument.text'
+    EXTENSION = '.odt'
+
+    supported = ('odt', )
+    """Formats this writer supports."""
+
+    default_stylesheet = 'styles' + EXTENSION
+
+    default_stylesheet_path = utils.relative_path(
+        os.path.join(os.getcwd(), 'dummy'),
+        os.path.join(os.path.dirname(__file__), default_stylesheet))
+
+    default_template = 'template.txt'
+
+    default_template_path = utils.relative_path(
+        os.path.join(os.getcwd(), 'dummy'),
+        os.path.join(os.path.dirname(__file__), default_template))
+
+    settings_spec = (
+        'ODF-Specific Options.',
+        None,
+        (
+            ('Specify a stylesheet.  '
+                'Default: "%s"' % default_stylesheet_path,
+                ['--stylesheet'],
+                {
+                    'default': default_stylesheet_path,
+                    'dest': 'stylesheet'
+                }),
+            ('Specify an ODF-specific configuration/mapping file '
+                'relative to the current working directory.',
+                ['--odf-config-file'],
+                {'metavar': '<file>'}),
+            ('Obfuscate email addresses to confuse harvesters.',
+                ['--cloak-email-addresses'],
+                {'default': False,
+                    'action': 'store_true',
+                    'dest': 'cloak_email_addresses',
+                    'validator': frontend.validate_boolean}),
+            ('Do not obfuscate email addresses.',
+                ['--no-cloak-email-addresses'],
+                {'default': False,
+                    'action': 'store_false',
+                    'dest': 'cloak_email_addresses',
+                    'validator': frontend.validate_boolean}),
+            ('Specify the thickness of table borders in thousands of a cm. '
+                'Default is 35.',
+                ['--table-border-thickness'],
+                {'default': None,
+                    'metavar': '<int>',
+                    'validator': frontend.validate_nonnegative_int}),
+            ('Add syntax highlighting in literal code blocks.',
+                ['--add-syntax-highlighting'],
+                {'default': False,
+                    'action': 'store_true',
+                    'dest': 'add_syntax_highlighting',
+                    'validator': frontend.validate_boolean}),
+            ('Do not add syntax highlighting in '
+                'literal code blocks. (default)',
+                ['--no-syntax-highlighting'],
+                {'default': False,
+                    'action': 'store_false',
+                    'dest': 'add_syntax_highlighting',
+                    'validator': frontend.validate_boolean}),
+            ('Create sections for headers.  (default)',
+                ['--create-sections'],
+                {'default': True,
+                    'action': 'store_true',
+                    'dest': 'create_sections',
+                    'validator': frontend.validate_boolean}),
+            ('Do not create sections for headers.',
+                ['--no-sections'],
+                {'default': True,
+                    'action': 'store_false',
+                    'dest': 'create_sections',
+                    'validator': frontend.validate_boolean}),
+            ('Create links.',
+                ['--create-links'],
+                {'default': False,
+                    'action': 'store_true',
+                    'dest': 'create_links',
+                    'validator': frontend.validate_boolean}),
+            ('Do not create links.  (default)',
+                ['--no-links'],
+                {'default': False,
+                    'action': 'store_false',
+                    'dest': 'create_links',
+                    'validator': frontend.validate_boolean}),
+            ('Generate endnotes at end of document, not footnotes '
+                'at bottom of page.',
+                ['--endnotes-end-doc'],
+                {'default': False,
+                    'action': 'store_true',
+                    'dest': 'endnotes_end_doc',
+                    'validator': frontend.validate_boolean}),
+            ('Generate footnotes at bottom of page, not endnotes '
+                'at end of document. (default)',
+                ['--no-endnotes-end-doc'],
+                {'default': False,
+                    'action': 'store_false',
+                    'dest': 'endnotes_end_doc',
+                    'validator': frontend.validate_boolean}),
+            ('Generate a bullet list table of contents, '
+                'not a native ODF table of contents.',
+                ['--generate-list-toc'],
+                {'action': 'store_false',
+                    'dest': 'generate_oowriter_toc',
+                    'validator': frontend.validate_boolean}),
+            ('Generate a native ODF table of contents, '
+                'not a bullet list. (default)',
+                ['--generate-oowriter-toc'],
+                {'default': True,
+                    'action': 'store_true',
+                    'dest': 'generate_oowriter_toc',
+                    'validator': frontend.validate_boolean}),
+            ('Specify the contents of an custom header line.  '
+                'See ODF/ODT writer documentation for details '
+                'about special field character sequences.',
+                ['--custom-odt-header'],
+                {'default': '',
+                    'dest': 'custom_header',
+                    'metavar': '<custom header>'}),
+            ('Specify the contents of an custom footer line.  '
+                'See ODF/ODT writer documentation for details.',
+                ['--custom-odt-footer'],
+                {'default': '',
+                    'dest': 'custom_footer',
+                    'metavar': '<custom footer>'}),
+        )
+    )
+
+    settings_defaults = {
+        'output_encoding_error_handler': 'xmlcharrefreplace',
+    }
+
+    relative_path_settings = ('odf_config_file', 'stylesheet',)
+
+    config_section = 'odf_odt writer'
+    config_section_dependencies = ('writers',)
+
+    def __init__(self):
+        writers.Writer.__init__(self)
+        self.translator_class = ODFTranslator
+
+    def translate(self):
+        self.settings = self.document.settings
+        self.visitor = self.translator_class(self.document)
+        self.visitor.retrieve_styles(self.EXTENSION)
+        self.document.walkabout(self.visitor)
+        self.visitor.add_doc_title()
+        self.assemble_my_parts()
+        self.output = self.parts['whole']
+
+    def assemble_my_parts(self):
+        """Assemble the `self.parts` dictionary.  Extend in subclasses.
+        """
+        writers.Writer.assemble_parts(self)
+        f = tempfile.NamedTemporaryFile()
+        zfile = zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED)
+        self.write_zip_str(
+            zfile, 'mimetype', self.MIME_TYPE,
+            compress_type=zipfile.ZIP_STORED)
+        content = self.visitor.content_astext()
+        self.write_zip_str(zfile, 'content.xml', content)
+        s1 = self.create_manifest()
+        self.write_zip_str(zfile, 'META-INF/manifest.xml', s1)
+        s1 = self.create_meta()
+        self.write_zip_str(zfile, 'meta.xml', s1)
+        s1 = self.get_stylesheet()
+        # Set default language in document to be generated.
+        # Language is specified by the -l/--language command line option.
+        # The format is described in BCP 47.  If region is omitted, we use
+        # local.normalize(ll) to obtain a region.
+        language_code = None
+        region_code = None
+        if self.visitor.language_code:
+            language_ids = self.visitor.language_code.replace('_', '-')
+            language_ids = language_ids.split('-')
+            # first tag is primary language tag
+            language_code = language_ids[0].lower()
+            # 2-letter region subtag may follow in 2nd or 3rd position
+            for subtag in language_ids[1:]:
+                if len(subtag) == 2 and subtag.isalpha():
+                    region_code = subtag.upper()
+                    break
+                elif len(subtag) == 1:
+                    break   # 1-letter tag is never before valid region tag
+            if region_code is None:
+                try:
+                    rcode = locale.normalize(language_code)
+                except NameError:
+                    rcode = language_code
+                rcode = rcode.split('_')
+                if len(rcode) > 1:
+                    rcode = rcode[1].split('.')
+                    region_code = rcode[0]
+                if region_code is None:
+                    self.document.reporter.warning(
+                        'invalid language-region.\n'
+                        '  Could not find region with locale.normalize().\n'
+                        '  Please specify both language and region (ll-RR).\n'
+                        '  Examples: es-MX (Spanish, Mexico),\n'
+                        '  en-AU (English, Australia).')
+        # Update the style ElementTree with the language and region.
+        # Note that we keep a reference to the modified node because
+        # it is possible that ElementTree will throw away the Python
+        # representation of the updated node if we do not.
+        updated, new_dom_styles, updated_node = self.update_stylesheet(
+            self.visitor.get_dom_stylesheet(), language_code, region_code)
+        if updated:
+            s1 = etree.tostring(new_dom_styles)
+        self.write_zip_str(zfile, 'styles.xml', s1)
+        self.store_embedded_files(zfile)
+        self.copy_from_stylesheet(zfile)
+        zfile.close()
+        f.seek(0)
+        whole = f.read()
+        f.close()
+        self.parts['whole'] = whole
+        self.parts['encoding'] = self.document.settings.output_encoding
+        self.parts['version'] = docutils.__version__
+
+    def update_stylesheet(self, stylesheet_root, language_code, region_code):
+        """Update xml style sheet element with language and region/country."""
+        updated = False
+        modified_nodes = set()
+        if language_code is not None or region_code is not None:
+            n1 = stylesheet_root.find(
+                '{urn:oasis:names:tc:opendocument:xmlns:office:1.0}'
+                'styles')
+            if n1 is None:
+                raise RuntimeError(
+                    "Cannot find 'styles' element in styles.odt/styles.xml")
+            n2_nodes = n1.findall(
+                '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+                'default-style')
+            if not n2_nodes:
+                raise RuntimeError(
+                    "Cannot find 'default-style' "
+                    "element in styles.xml")
+            for node in n2_nodes:
+                family = node.attrib.get(
+                    '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+                    'family')
+                if family == 'paragraph' or family == 'graphic':
+                    n3 = node.find(
+                        '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+                        'text-properties')
+                    if n3 is None:
+                        raise RuntimeError(
+                            "Cannot find 'text-properties' "
+                            "element in styles.xml")
+                    if language_code is not None:
+                        n3.attrib[
+                            '{urn:oasis:names:tc:opendocument:xmlns:'
+                            'xsl-fo-compatible:1.0}language'] = language_code
+                        n3.attrib[
+                            '{urn:oasis:names:tc:opendocument:xmlns:'
+                            'style:1.0}language-complex'] = language_code
+                        updated = True
+                        modified_nodes.add(n3)
+                    if region_code is not None:
+                        n3.attrib[
+                            '{urn:oasis:names:tc:opendocument:xmlns:'
+                            'xsl-fo-compatible:1.0}country'] = region_code
+                        n3.attrib[
+                            '{urn:oasis:names:tc:opendocument:xmlns:'
+                            'style:1.0}country-complex'] = region_code
+                        updated = True
+                        modified_nodes.add(n3)
+        return updated, stylesheet_root, modified_nodes
+
+    def write_zip_str(
+            self, zfile, name, bytes, compress_type=zipfile.ZIP_DEFLATED):
+        localtime = time.localtime(time.time())
+        zinfo = zipfile.ZipInfo(name, localtime)
+        # Add some standard UNIX file access permissions (-rw-r--r--).
+        zinfo.external_attr = (0x81a4 & 0xFFFF) << 16
+        zinfo.compress_type = compress_type
+        zfile.writestr(zinfo, bytes)
+
+    def store_embedded_files(self, zfile):
+        embedded_files = self.visitor.get_embedded_file_list()
+        for source, destination in embedded_files:
+            if source is None:
+                continue
+            try:
+                zfile.write(source, destination)
+            except OSError:
+                self.document.reporter.warning(
+                    "Can't open file %s." % (source, ))
+
+    def get_settings(self):
+        """
+        modeled after get_stylesheet
+        """
+        stylespath = self.settings.stylesheet
+        zfile = zipfile.ZipFile(stylespath, 'r')
+        s1 = zfile.read('settings.xml')
+        zfile.close()
+        return s1
+
+    def get_stylesheet(self):
+        """Get the stylesheet from the visitor.
+        Ask the visitor to setup the page.
+        """
+        return self.visitor.setup_page()
+
+    def copy_from_stylesheet(self, outzipfile):
+        """Copy images, settings, etc from the stylesheet doc into target doc.
+        """
+        stylespath = self.settings.stylesheet
+        inzipfile = zipfile.ZipFile(stylespath, 'r')
+        # Copy the styles.
+        s1 = inzipfile.read('settings.xml')
+        self.write_zip_str(outzipfile, 'settings.xml', s1)
+        # Copy the images.
+        namelist = inzipfile.namelist()
+        for name in namelist:
+            if name.startswith('Pictures/'):
+                imageobj = inzipfile.read(name)
+                outzipfile.writestr(name, imageobj)
+        inzipfile.close()
+
+    def assemble_parts(self):
+        pass
+
+    def create_manifest(self):
+        root = Element(
+            'manifest:manifest',
+            attrib=MANIFEST_NAMESPACE_ATTRIB,
+            nsdict=MANIFEST_NAMESPACE_DICT,
+        )
+        doc = etree.ElementTree(root)
+        SubElement(root, 'manifest:file-entry', attrib={
+            'manifest:media-type': self.MIME_TYPE,
+            'manifest:full-path': '/',
+        }, nsdict=MANNSD)
+        SubElement(root, 'manifest:file-entry', attrib={
+            'manifest:media-type': 'text/xml',
+            'manifest:full-path': 'content.xml',
+        }, nsdict=MANNSD)
+        SubElement(root, 'manifest:file-entry', attrib={
+            'manifest:media-type': 'text/xml',
+            'manifest:full-path': 'styles.xml',
+        }, nsdict=MANNSD)
+        SubElement(root, 'manifest:file-entry', attrib={
+            'manifest:media-type': 'text/xml',
+            'manifest:full-path': 'settings.xml',
+        }, nsdict=MANNSD)
+        SubElement(root, 'manifest:file-entry', attrib={
+            'manifest:media-type': 'text/xml',
+            'manifest:full-path': 'meta.xml',
+        }, nsdict=MANNSD)
+        s1 = ToString(doc)
+        doc = minidom.parseString(s1)
+        return doc.toprettyxml('  ')
+
+    def create_meta(self):
+        root = Element(
+            'office:document-meta',
+            attrib=META_NAMESPACE_ATTRIB,
+            nsdict=META_NAMESPACE_DICT,
+        )
+        doc = etree.ElementTree(root)
+        root = SubElement(root, 'office:meta', nsdict=METNSD)
+        el1 = SubElement(root, 'meta:generator', nsdict=METNSD)
+        el1.text = 'Docutils/rst2odf.py/%s' % (VERSION, )
+        s1 = os.environ.get('USER', '')
+        el1 = SubElement(root, 'meta:initial-creator', nsdict=METNSD)
+        el1.text = s1
+        s2 = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime())
+        el1 = SubElement(root, 'meta:creation-date', nsdict=METNSD)
+        el1.text = s2
+        el1 = SubElement(root, 'dc:creator', nsdict=METNSD)
+        el1.text = s1
+        el1 = SubElement(root, 'dc:date', nsdict=METNSD)
+        el1.text = s2
+        el1 = SubElement(root, 'dc:language', nsdict=METNSD)
+        el1.text = 'en-US'
+        el1 = SubElement(root, 'meta:editing-cycles', nsdict=METNSD)
+        el1.text = '1'
+        el1 = SubElement(root, 'meta:editing-duration', nsdict=METNSD)
+        el1.text = 'PT00M01S'
+        title = self.visitor.get_title()
+        el1 = SubElement(root, 'dc:title', nsdict=METNSD)
+        if title:
+            el1.text = title
+        else:
+            el1.text = '[no title]'
+        for prop, value in self.visitor.get_meta_dict().items():
+            # 'keywords', 'description', and 'subject' have their own fields:
+            if prop == 'keywords':
+                keywords = re.split(', *', value)
+                for keyword in keywords:
+                    el1 = SubElement(root, 'meta:keyword', nsdict=METNSD)
+                    el1.text = keyword
+            elif prop == 'description':
+                el1 = SubElement(root, 'dc:description', nsdict=METNSD)
+                el1.text = value
+            elif prop == 'subject':
+                el1 = SubElement(root, 'dc:subject', nsdict=METNSD)
+                el1.text = value
+            else:  # Store remaining properties as custom/user-defined
+                el1 = SubElement(root, 'meta:user-defined',
+                                 attrib={'meta:name': prop}, nsdict=METNSD)
+                el1.text = value
+        s1 = ToString(doc)
+        # doc = minidom.parseString(s1)
+        # s1 = doc.toprettyxml('  ')
+        return s1
+
+
+# class ODFTranslator(nodes.SparseNodeVisitor):
+class ODFTranslator(nodes.GenericNodeVisitor):
+
+    used_styles = (
+        'attribution', 'blockindent', 'blockquote', 'blockquote-bulletitem',
+        'blockquote-bulletlist', 'blockquote-enumitem', 'blockquote-enumlist',
+        'bulletitem', 'bulletlist',
+        'caption', 'legend',
+        'centeredtextbody', 'codeblock', 'codeblock-indented',
+        'codeblock-classname', 'codeblock-comment', 'codeblock-functionname',
+        'codeblock-keyword', 'codeblock-name', 'codeblock-number',
+        'codeblock-operator', 'codeblock-string', 'emphasis', 'enumitem',
+        'enumlist', 'epigraph', 'epigraph-bulletitem', 'epigraph-bulletlist',
+        'epigraph-enumitem', 'epigraph-enumlist', 'footer',
+        'footnote', 'citation',
+        'header', 'highlights', 'highlights-bulletitem',
+        'highlights-bulletlist', 'highlights-enumitem', 'highlights-enumlist',
+        'horizontalline', 'inlineliteral', 'quotation', 'rubric',
+        'strong', 'table-title', 'textbody', 'tocbulletlist', 'tocenumlist',
+        'title',
+        'subtitle',
+        'heading1',
+        'heading2',
+        'heading3',
+        'heading4',
+        'heading5',
+        'heading6',
+        'heading7',
+        'admon-attention-hdr',
+        'admon-attention-body',
+        'admon-caution-hdr',
+        'admon-caution-body',
+        'admon-danger-hdr',
+        'admon-danger-body',
+        'admon-error-hdr',
+        'admon-error-body',
+        'admon-generic-hdr',
+        'admon-generic-body',
+        'admon-hint-hdr',
+        'admon-hint-body',
+        'admon-important-hdr',
+        'admon-important-body',
+        'admon-note-hdr',
+        'admon-note-body',
+        'admon-tip-hdr',
+        'admon-tip-body',
+        'admon-warning-hdr',
+        'admon-warning-body',
+        'tableoption',
+        'tableoption.%c', 'tableoption.%c%d', 'Table%d', 'Table%d.%c',
+        'Table%d.%c%d',
+        'lineblock1',
+        'lineblock2',
+        'lineblock3',
+        'lineblock4',
+        'lineblock5',
+        'lineblock6',
+        'image', 'figureframe',
+    )
+
+    def __init__(self, document):
+        # nodes.SparseNodeVisitor.__init__(self, document)
+        nodes.GenericNodeVisitor.__init__(self, document)
+        self.settings = document.settings
+        self.language_code = self.settings.language_code
+        self.language = languages.get_language(
+            self.language_code,
+            document.reporter)
+        self.format_map = {}
+        if self.settings.odf_config_file:
+            parser = ConfigParser()
+            parser.read(self.settings.odf_config_file)
+            for rststyle, format in parser.items("Formats"):
+                if rststyle not in self.used_styles:
+                    self.document.reporter.warning(
+                        'Style "%s" is not a style used by odtwriter.' % (
+                            rststyle, ))
+                self.format_map[rststyle] = format
+        self.section_level = 0
+        self.section_count = 0
+        # Create ElementTree content and styles documents.
+        root = Element(
+            'office:document-content',
+            attrib=CONTENT_NAMESPACE_ATTRIB,
+        )
+        self.content_tree = etree.ElementTree(element=root)
+        self.current_element = root
+        SubElement(root, 'office:scripts')
+        SubElement(root, 'office:font-face-decls')
+        el = SubElement(root, 'office:automatic-styles')
+        self.automatic_styles = el
+        el = SubElement(root, 'office:body')
+        el = self.generate_content_element(el)
+        self.current_element = el
+        self.body_text_element = el
+        self.paragraph_style_stack = [self.rststyle('textbody'), ]
+        self.list_style_stack = []
+        self.table_count = 0
+        self.column_count = ord('A') - 1
+        self.trace_level = -1
+        self.optiontablestyles_generated = False
+        self.field_name = None
+        self.field_element = None
+        self.title = None
+        self.image_count = 0
+        self.image_style_count = 0
+        self.image_dict = {}
+        self.embedded_file_list = []
+        self.syntaxhighlighting = 1
+        self.syntaxhighlight_lexer = 'python'
+        self.header_content = []
+        self.footer_content = []
+        self.in_header = False
+        self.in_footer = False
+        self.blockstyle = ''
+        self.in_table_of_contents = False
+        self.table_of_content_index_body = None
+        self.list_level = 0
+        self.def_list_level = 0
+        self.footnote_ref_dict = {}
+        self.footnote_list = []
+        self.footnote_chars_idx = 0
+        self.footnote_level = 0
+        self.pending_ids = []
+        self.in_paragraph = False
+        self.found_doc_title = False
+        self.bumped_list_level_stack = []
+        self.meta_dict = {}
+        self.line_block_level = 0
+        self.line_indent_level = 0
+        self.citation_id = None
+        self.style_index = 0        # use to form unique style names
+        self.str_stylesheet = ''
+        self.str_stylesheetcontent = ''
+        self.dom_stylesheet = None
+        self.table_styles = None
+        self.in_citation = False
+
+        # Keep track of nested styling classes
+        self.inline_style_count_stack = []
+
+    def get_str_stylesheet(self):
+        return self.str_stylesheet
+
+    def retrieve_styles(self, extension):
+        """Retrieve the stylesheet from either a .xml file or from
+        a .odt (zip) file.  Return the content as a string.
+        """
+        s2 = None
+        stylespath = self.settings.stylesheet
+        ext = os.path.splitext(stylespath)[1]
+        if ext == '.xml':
+            with open(stylespath, 'r', encoding='utf-8') as stylesfile:
+                s1 = stylesfile.read()
+        elif ext == extension:
+            zfile = zipfile.ZipFile(stylespath, 'r')
+            s1 = zfile.read('styles.xml')
+            s2 = zfile.read('content.xml')
+            zfile.close()
+        else:
+            raise RuntimeError('stylesheet path (%s) must be %s or '
+                               '.xml file' % (stylespath, extension))
+        self.str_stylesheet = s1
+        self.str_stylesheetcontent = s2
+        self.dom_stylesheet = etree.fromstring(self.str_stylesheet)
+        self.dom_stylesheetcontent = etree.fromstring(
+            self.str_stylesheetcontent)
+        self.table_styles = self.extract_table_styles(s2)
+
+    def extract_table_styles(self, styles_str):
+        root = etree.fromstring(styles_str)
+        table_styles = {}
+        auto_styles = root.find(
+            '{%s}automatic-styles' % (CNSD['office'], ))
+        for stylenode in auto_styles:
+            name = stylenode.get('{%s}name' % (CNSD['style'], ))
+            tablename = name.split('.')[0]
+            family = stylenode.get('{%s}family' % (CNSD['style'], ))
+            if name.startswith(TABLESTYLEPREFIX):
+                tablestyle = table_styles.get(tablename)
+                if tablestyle is None:
+                    tablestyle = TableStyle()
+                    table_styles[tablename] = tablestyle
+                if family == 'table':
+                    properties = stylenode.find(
+                        '{%s}table-properties' % (CNSD['style'], ))
+                    property = properties.get(
+                        '{%s}%s' % (CNSD['fo'], 'background-color', ))
+                    if property is not None and property != 'none':
+                        tablestyle.backgroundcolor = property
+                elif family == 'table-cell':
+                    properties = stylenode.find(
+                        '{%s}table-cell-properties' % (CNSD['style'], ))
+                    if properties is not None:
+                        border = self.get_property(properties)
+                        if border is not None:
+                            tablestyle.border = border
+        return table_styles
+
+    def get_property(self, stylenode):
+        border = None
+        for propertyname in TABLEPROPERTYNAMES:
+            border = stylenode.get('{%s}%s' % (CNSD['fo'], propertyname, ))
+            if border is not None and border != 'none':
+                return border
+        return border
+
+    def add_doc_title(self):
+        text = self.settings.title
+        if text:
+            self.title = text
+            if not self.found_doc_title:
+                el = Element('text:p', attrib={
+                    'text:style-name': self.rststyle('title'),
+                })
+                el.text = text
+                self.body_text_element.insert(0, el)
+        el = self.find_first_text_p(self.body_text_element)
+        if el is not None:
+            self.attach_page_style(el)
+
+    def find_first_text_p(self, el):
+        """Search the generated doc and return the first <text:p> element.
+        """
+        if el.tag == 'text:p' or el.tag == 'text:h':
+            return el
+        else:
+            for child in el:
+                el1 = self.find_first_text_p(child)
+                if el1 is not None:
+                    return el1
+            return None
+
+    def attach_page_style(self, el):
+        """Attach the default page style.
+
+        Create an automatic-style that refers to the current style
+        of this element and that refers to the default page style.
+        """
+        current_style = el.get('text:style-name')
+        style_name = 'P1003'
+        el1 = SubElement(
+            self.automatic_styles, 'style:style', attrib={
+                'style:name': style_name,
+                'style:master-page-name': "rststyle-pagedefault",
+                'style:family': "paragraph",
+            }, nsdict=SNSD)
+        if current_style:
+            el1.set('style:parent-style-name', current_style)
+        el.set('text:style-name', style_name)
+
+    def rststyle(self, name, parameters=()):
+        """
+        Returns the style name to use for the given style.
+
+        If `parameters` is given `name` must contain a matching number of
+        ``%`` and is used as a format expression with `parameters` as
+        the value.
+        """
+        name1 = name % parameters
+        return self.format_map.get(name1, 'rststyle-%s' % name1)
+
+    def generate_content_element(self, root):
+        return SubElement(root, 'office:text')
+
+    def setup_page(self):
+        self.setup_paper(self.dom_stylesheet)
+        if (len(self.header_content) > 0
+            or len(self.footer_content) > 0
+            or self.settings.custom_header
+            or self.settings.custom_footer):
+            self.add_header_footer(self.dom_stylesheet)
+        return etree.tostring(self.dom_stylesheet)
+
+    def get_dom_stylesheet(self):
+        return self.dom_stylesheet
+
+    def setup_paper(self, root_el):
+        # TODO: only call paperconf, if it is actually used
+        # (i.e. page size removed from "styles.odt" with rst2odt_prepstyles.py
+        # cf. conditional in walk() below)?
+        try:
+            dimensions = subprocess.check_output(('paperconf', '-s'),
+                                                 stderr=subprocess.STDOUT)
+            w, h = (float(s) for s in dimensions.split())
+        except (subprocess.CalledProcessError, FileNotFoundError, ValueError):
+            self.document.reporter.info(
+                'Cannot use `paperconf`, defaulting to Letter.')
+            w, h = 612, 792     # default to Letter
+
+        def walk(el):
+            if el.tag == "{%s}page-layout-properties" % SNSD["style"] and \
+                    "{%s}page-width" % SNSD["fo"] not in el.attrib:
+                el.attrib["{%s}page-width" % SNSD["fo"]] = "%.3fpt" % w
+                el.attrib["{%s}page-height" % SNSD["fo"]] = "%.3fpt" % h
+                el.attrib["{%s}margin-left" % SNSD["fo"]] = \
+                    el.attrib["{%s}margin-right" % SNSD["fo"]] = \
+                    "%.3fpt" % (.1 * w)
+                el.attrib["{%s}margin-top" % SNSD["fo"]] = \
+                    el.attrib["{%s}margin-bottom" % SNSD["fo"]] = \
+                    "%.3fpt" % (.1 * h)
+            else:
+                for subel in el:
+                    walk(subel)
+        walk(root_el)
+
+    def add_header_footer(self, root_el):
+        automatic_styles = root_el.find(
+            '{%s}automatic-styles' % SNSD['office'])
+        path = '{%s}master-styles' % (NAME_SPACE_1, )
+        master_el = root_el.find(path)
+        if master_el is None:
+            return
+        path = '{%s}master-page' % (SNSD['style'], )
+        master_el_container = master_el.findall(path)
+        master_el = None
+        target_attrib = '{%s}name' % (SNSD['style'], )
+        target_name = self.rststyle('pagedefault')
+        for el in master_el_container:
+            if el.get(target_attrib) == target_name:
+                master_el = el
+                break
+        if master_el is None:
+            return
+        el1 = master_el
+        if self.header_content or self.settings.custom_header:
+            el2 = SubElement(
+                el1, 'style:header',
+                attrib=STYLES_NAMESPACE_ATTRIB,
+                nsdict=STYLES_NAMESPACE_DICT,
+            )
+            for el in self.header_content:
+                attrkey = add_ns('text:style-name', nsdict=SNSD)
+                el.attrib[attrkey] = self.rststyle('header')
+                el2.append(el)
+            if self.settings.custom_header:
+                self.create_custom_headfoot(
+                    el2,
+                    self.settings.custom_header, 'header', automatic_styles)
+        if self.footer_content or self.settings.custom_footer:
+            el2 = SubElement(
+                el1, 'style:footer',
+                attrib=STYLES_NAMESPACE_ATTRIB,
+                nsdict=STYLES_NAMESPACE_DICT,
+            )
+            for el in self.footer_content:
+                attrkey = add_ns('text:style-name', nsdict=SNSD)
+                el.attrib[attrkey] = self.rststyle('footer')
+                el2.append(el)
+            if self.settings.custom_footer:
+                self.create_custom_headfoot(
+                    el2,
+                    self.settings.custom_footer, 'footer', automatic_styles)
+
+    code_none, code_field, code_text = list(range(3))
+    field_pat = re.compile(r'%(..?)%')
+
+    def create_custom_headfoot(
+            self, parent, text, style_name, automatic_styles):
+        parent = SubElement(parent, 'text:p', attrib={
+            'text:style-name': self.rststyle(style_name),
+        })
+        current_element = None
+        field_iter = self.split_field_specifiers_iter(text)
+        for item in field_iter:
+            if item[0] == ODFTranslator.code_field:
+                if item[1] not in (
+                        'p', 'P',
+                        't1', 't2', 't3', 't4',
+                        'd1', 'd2', 'd3', 'd4', 'd5',
+                        's', 't', 'a'):
+                    msg = 'bad field spec: %%%s%%' % (item[1], )
+                    raise RuntimeError(msg)
+                el1 = self.make_field_element(
+                    parent,
+                    item[1], style_name, automatic_styles)
+                if el1 is None:
+                    msg = 'bad field spec: %%%s%%' % (item[1], )
+                    raise RuntimeError(msg)
+                else:
+                    current_element = el1
+            else:
+                if current_element is None:
+                    parent.text = item[1]
+                else:
+                    current_element.tail = item[1]
+
+    def make_field_element(self, parent, text, style_name, automatic_styles):
+        if text == 'p':
+            el1 = SubElement(parent, 'text:page-number', attrib={
+                # 'text:style-name': self.rststyle(style_name),
+                'text:select-page': 'current',
+            })
+        elif text == 'P':
+            el1 = SubElement(parent, 'text:page-count', attrib={
+                # 'text:style-name': self.rststyle(style_name),
+            })
+        elif text == 't1':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:time', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'text:fixed': 'true',
+                'style:data-style-name':
+                    'rst-time-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+                'style:name': 'rst-time-style-%d' % self.style_index,
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:hours', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ':'
+            el3 = SubElement(el2, 'number:minutes', attrib={
+                'number:style': 'long',
+            })
+        elif text == 't2':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:time', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'text:fixed': 'true',
+                'style:data-style-name':
+                    'rst-time-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+                'style:name': 'rst-time-style-%d' % self.style_index,
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:hours', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ':'
+            el3 = SubElement(el2, 'number:minutes', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ':'
+            el3 = SubElement(el2, 'number:seconds', attrib={
+                'number:style': 'long',
+            })
+        elif text == 't3':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:time', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'text:fixed': 'true',
+                'style:data-style-name':
+                    'rst-time-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+                'style:name': 'rst-time-style-%d' % self.style_index,
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:hours', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ':'
+            el3 = SubElement(el2, 'number:minutes', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ' '
+            el3 = SubElement(el2, 'number:am-pm')
+        elif text == 't4':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:time', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'text:fixed': 'true',
+                'style:data-style-name':
+                    'rst-time-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+                'style:name': 'rst-time-style-%d' % self.style_index,
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:hours', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ':'
+            el3 = SubElement(el2, 'number:minutes', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ':'
+            el3 = SubElement(el2, 'number:seconds', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ' '
+            el3 = SubElement(el2, 'number:am-pm')
+        elif text == 'd1':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:date', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'style:data-style-name':
+                    'rst-date-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+                'style:name': 'rst-date-style-%d' % self.style_index,
+                'number:automatic-order': 'true',
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:month', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = '/'
+            el3 = SubElement(el2, 'number:day', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = '/'
+            el3 = SubElement(el2, 'number:year')
+        elif text == 'd2':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:date', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'style:data-style-name':
+                    'rst-date-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+                'style:name': 'rst-date-style-%d' % self.style_index,
+                'number:automatic-order': 'true',
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:month', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = '/'
+            el3 = SubElement(el2, 'number:day', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = '/'
+            el3 = SubElement(el2, 'number:year', attrib={
+                'number:style': 'long',
+            })
+        elif text == 'd3':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:date', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'style:data-style-name':
+                    'rst-date-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+                'style:name': 'rst-date-style-%d' % self.style_index,
+                'number:automatic-order': 'true',
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:month', attrib={
+                'number:textual': 'true',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ' '
+            el3 = SubElement(el2, 'number:day', attrib={})
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ', '
+            el3 = SubElement(el2, 'number:year', attrib={
+                'number:style': 'long',
+            })
+        elif text == 'd4':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:date', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'style:data-style-name':
+                    'rst-date-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+                'style:name': 'rst-date-style-%d' % self.style_index,
+                'number:automatic-order': 'true',
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:month', attrib={
+                'number:textual': 'true',
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ' '
+            el3 = SubElement(el2, 'number:day', attrib={})
+            el3 = SubElement(el2, 'number:text')
+            el3.text = ', '
+            el3 = SubElement(el2, 'number:year', attrib={
+                'number:style': 'long',
+            })
+        elif text == 'd5':
+            self.style_index += 1
+            el1 = SubElement(parent, 'text:date', attrib={
+                'text:style-name': self.rststyle(style_name),
+                'style:data-style-name':
+                    'rst-date-style-%d' % self.style_index,
+            })
+            el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+                'style:name': 'rst-date-style-%d' % self.style_index,
+                'xmlns:number': SNSD['number'],
+                'xmlns:style': SNSD['style'],
+            })
+            el3 = SubElement(el2, 'number:year', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = '-'
+            el3 = SubElement(el2, 'number:month', attrib={
+                'number:style': 'long',
+            })
+            el3 = SubElement(el2, 'number:text')
+            el3.text = '-'
+            el3 = SubElement(el2, 'number:day', attrib={
+                'number:style': 'long',
+            })
+        elif text == 's':
+            el1 = SubElement(parent, 'text:subject', attrib={
+                'text:style-name': self.rststyle(style_name),
+            })
+        elif text == 't':
+            el1 = SubElement(parent, 'text:title', attrib={
+                'text:style-name': self.rststyle(style_name),
+            })
+        elif text == 'a':
+            el1 = SubElement(parent, 'text:author-name', attrib={
+                'text:fixed': 'false',
+            })
+        else:
+            el1 = None
+        return el1
+
+    def split_field_specifiers_iter(self, text):
+        pos1 = 0
+        while True:
+            mo = ODFTranslator.field_pat.search(text, pos1)
+            if mo:
+                pos2 = mo.start()
+                if pos2 > pos1:
+                    yield ODFTranslator.code_text, text[pos1:pos2]
+                yield ODFTranslator.code_field, mo.group(1)
+                pos1 = mo.end()
+            else:
+                break
+        trailing = text[pos1:]
+        if trailing:
+            yield ODFTranslator.code_text, trailing
+
+    def astext(self):
+        root = self.content_tree.getroot()
+        et = etree.ElementTree(root)
+        return ToString(et)
+
+    def content_astext(self):
+        return self.astext()
+
+    def set_title(self, title):
+        self.title = title
+
+    def get_title(self):
+        return self.title
+
+    def set_embedded_file_list(self, embedded_file_list):
+        self.embedded_file_list = embedded_file_list
+
+    def get_embedded_file_list(self):
+        return self.embedded_file_list
+
+    def get_meta_dict(self):
+        return self.meta_dict
+
+    def process_footnotes(self):
+        for node, el1 in self.footnote_list:
+            backrefs = node.attributes.get('backrefs', [])
+            first = True
+            for ref in backrefs:
+                el2 = self.footnote_ref_dict.get(ref)
+                if el2 is not None:
+                    if first:
+                        first = False
+                        el3 = copy.deepcopy(el1)
+                        el2.append(el3)
+                    else:
+                        if len(el2) > 0:   # and 'id' in el2.attrib:
+                            child = el2[0]
+                            ref1 = child.text
+                            attribkey = add_ns('text:id', nsdict=SNSD)
+                            id1 = el2.get(attribkey, 'footnote-error')
+                            if id1 is None:
+                                id1 = ''
+                            tag = add_ns('text:note-ref', nsdict=SNSD)
+                            el2.tag = tag
+                            if self.settings.endnotes_end_doc:
+                                note_class = 'endnote'
+                            else:
+                                note_class = 'footnote'
+                            el2.attrib.clear()
+                            attribkey = add_ns('text:note-class', nsdict=SNSD)
+                            el2.attrib[attribkey] = note_class
+                            attribkey = add_ns('text:ref-name', nsdict=SNSD)
+                            el2.attrib[attribkey] = id1
+                            attribkey = add_ns(
+                                'text:reference-format', nsdict=SNSD)
+                            el2.attrib[attribkey] = 'page'
+                            el2.text = ref1
+
+    #
+    # Utility methods
+
+    def append_child(self, tag, attrib=None, parent=None):
+        if parent is None:
+            parent = self.current_element
+        return SubElement(parent, tag, attrib)
+
+    def append_p(self, style, text=None):
+        result = self.append_child('text:p', attrib={
+            'text:style-name': self.rststyle(style)})
+        self.append_pending_ids(result)
+        if text is not None:
+            result.text = text
+        return result
+
+    def append_pending_ids(self, el):
+        if self.settings.create_links:
+            for id in self.pending_ids:
+                SubElement(el, 'text:reference-mark', attrib={
+                    'text:name': id})
+        self.pending_ids = []
+
+    def set_current_element(self, el):
+        self.current_element = el
+
+    def set_to_parent(self):
+        self.current_element = self.current_element.getparent()
+
+    def generate_labeled_block(self, node, label):
+        label = '%s:' % (self.language.labels[label], )
+        el = self.append_p('textbody')
+        el1 = SubElement(
+            el, 'text:span',
+            attrib={'text:style-name': self.rststyle('strong')})
+        el1.text = label
+        return self.append_p('blockindent')
+
+    def generate_labeled_line(self, node, label):
+        label = '%s:' % (self.language.labels[label], )
+        el = self.append_p('textbody')
+        el1 = SubElement(
+            el, 'text:span',
+            attrib={'text:style-name': self.rststyle('strong')})
+        el1.text = label
+        el1.tail = node.astext()
+        return el
+
+    def encode(self, text):
+        return text.replace('\n', " ")
+
+    #
+    # Visitor functions
+    #
+    # In alphabetic order, more or less.
+    #   See docutils.docutils.nodes.node_class_names.
+    #
+
+    def dispatch_visit(self, node):
+        """Override to catch basic attributes which many nodes have."""
+        self.handle_basic_atts(node)
+        nodes.GenericNodeVisitor.dispatch_visit(self, node)
+
+    def handle_basic_atts(self, node):
+        if isinstance(node, nodes.Element) and node['ids']:
+            self.pending_ids += node['ids']
+
+    def default_visit(self, node):
+        self.document.reporter.warning('missing visit_%s' % (node.tagname, ))
+
+    def default_departure(self, node):
+        self.document.reporter.warning('missing depart_%s' % (node.tagname, ))
+
+    def visit_Text(self, node):
+        # Skip nodes whose text has been processed in parent nodes.
+        if isinstance(node.parent, docutils.nodes.literal_block):
+            return
+        text = node.astext()
+        # Are we in mixed content?  If so, add the text to the
+        #   etree tail of the previous sibling element.
+        if len(self.current_element) > 0:
+            if self.current_element[-1].tail:
+                self.current_element[-1].tail += text
+            else:
+                self.current_element[-1].tail = text
+        else:
+            if self.current_element.text:
+                self.current_element.text += text
+            else:
+                self.current_element.text = text
+
+    def depart_Text(self, node):
+        pass
+
+    #
+    # Pre-defined fields
+    #
+
+    def visit_address(self, node):
+        el = self.generate_labeled_block(node, 'address')
+        self.set_current_element(el)
+
+    def depart_address(self, node):
+        self.set_to_parent()
+
+    def visit_author(self, node):
+        if isinstance(node.parent, nodes.authors):
+            el = self.append_p('blockindent')
+        else:
+            el = self.generate_labeled_block(node, 'author')
+        self.set_current_element(el)
+
+    def depart_author(self, node):
+        self.set_to_parent()
+
+    def visit_authors(self, node):
+        label = '%s:' % (self.language.labels['authors'], )
+        el = self.append_p('textbody')
+        el1 = SubElement(
+            el, 'text:span',
+            attrib={'text:style-name': self.rststyle('strong')})
+        el1.text = label
+
+    def depart_authors(self, node):
+        pass
+
+    def visit_contact(self, node):
+        el = self.generate_labeled_block(node, 'contact')
+        self.set_current_element(el)
+
+    def depart_contact(self, node):
+        self.set_to_parent()
+
+    def visit_copyright(self, node):
+        el = self.generate_labeled_block(node, 'copyright')
+        self.set_current_element(el)
+
+    def depart_copyright(self, node):
+        self.set_to_parent()
+
+    def visit_date(self, node):
+        self.generate_labeled_line(node, 'date')
+
+    def depart_date(self, node):
+        pass
+
+    def visit_organization(self, node):
+        el = self.generate_labeled_block(node, 'organization')
+        self.set_current_element(el)
+
+    def depart_organization(self, node):
+        self.set_to_parent()
+
+    def visit_status(self, node):
+        el = self.generate_labeled_block(node, 'status')
+        self.set_current_element(el)
+
+    def depart_status(self, node):
+        self.set_to_parent()
+
+    def visit_revision(self, node):
+        self.generate_labeled_line(node, 'revision')
+
+    def depart_revision(self, node):
+        pass
+
+    def visit_version(self, node):
+        self.generate_labeled_line(node, 'version')
+        # self.set_current_element(el)
+
+    def depart_version(self, node):
+        # self.set_to_parent()
+        pass
+
+    def visit_attribution(self, node):
+        self.append_p('attribution', node.astext())
+
+    def depart_attribution(self, node):
+        pass
+
+    def visit_block_quote(self, node):
+        if 'epigraph' in node.attributes['classes']:
+            self.paragraph_style_stack.append(self.rststyle('epigraph'))
+            self.blockstyle = self.rststyle('epigraph')
+        elif 'highlights' in node.attributes['classes']:
+            self.paragraph_style_stack.append(self.rststyle('highlights'))
+            self.blockstyle = self.rststyle('highlights')
+        else:
+            self.paragraph_style_stack.append(self.rststyle('blockquote'))
+            self.blockstyle = self.rststyle('blockquote')
+        self.line_indent_level += 1
+
+    def depart_block_quote(self, node):
+        self.paragraph_style_stack.pop()
+        self.blockstyle = ''
+        self.line_indent_level -= 1
+
+    def visit_bullet_list(self, node):
+        self.list_level += 1
+        if self.in_table_of_contents:
+            if self.settings.generate_oowriter_toc:
+                pass
+            else:
+                if 'classes' in node and \
+                        'auto-toc' in node.attributes['classes']:
+                    el = SubElement(self.current_element, 'text:list', attrib={
+                        'text:style-name': self.rststyle('tocenumlist'),
+                    })
+                    self.list_style_stack.append(self.rststyle('enumitem'))
+                else:
+                    el = SubElement(self.current_element, 'text:list', attrib={
+                        'text:style-name': self.rststyle('tocbulletlist'),
+                    })
+                    self.list_style_stack.append(self.rststyle('bulletitem'))
+                self.set_current_element(el)
+        else:
+            if self.blockstyle == self.rststyle('blockquote'):
+                el = SubElement(self.current_element, 'text:list', attrib={
+                    'text:style-name': self.rststyle('blockquote-bulletlist'),
+                })
+                self.list_style_stack.append(
+                    self.rststyle('blockquote-bulletitem'))
+            elif self.blockstyle == self.rststyle('highlights'):
+                el = SubElement(self.current_element, 'text:list', attrib={
+                    'text:style-name': self.rststyle('highlights-bulletlist'),
+                })
+                self.list_style_stack.append(
+                    self.rststyle('highlights-bulletitem'))
+            elif self.blockstyle == self.rststyle('epigraph'):
+                el = SubElement(self.current_element, 'text:list', attrib={
+                    'text:style-name': self.rststyle('epigraph-bulletlist'),
+                })
+                self.list_style_stack.append(
+                    self.rststyle('epigraph-bulletitem'))
+            else:
+                el = SubElement(self.current_element, 'text:list', attrib={
+                    'text:style-name': self.rststyle('bulletlist'),
+                })
+                self.list_style_stack.append(self.rststyle('bulletitem'))
+            self.set_current_element(el)
+
+    def depart_bullet_list(self, node):
+        if self.in_table_of_contents:
+            if self.settings.generate_oowriter_toc:
+                pass
+            else:
+                self.set_to_parent()
+                self.list_style_stack.pop()
+        else:
+            self.set_to_parent()
+            self.list_style_stack.pop()
+        self.list_level -= 1
+
+    def visit_caption(self, node):
+        raise nodes.SkipChildren()
+
+    def depart_caption(self, node):
+        pass
+
+    def visit_comment(self, node):
+        el = self.append_p('textbody')
+        el1 = SubElement(el, 'office:annotation', attrib={})
+        el2 = SubElement(el1, 'dc:creator', attrib={})
+        s1 = os.environ.get('USER', '')
+        el2.text = s1
+        el2 = SubElement(el1, 'text:p', attrib={})
+        el2.text = node.astext()
+
+    def depart_comment(self, node):
+        pass
+
+    def visit_compound(self, node):
+        # The compound directive currently receives no special treatment.
+        pass
+
+    def depart_compound(self, node):
+        pass
+
+    def visit_container(self, node):
+        styles = node.attributes.get('classes', ())
+        if len(styles) > 0:
+            self.paragraph_style_stack.append(self.rststyle(styles[0]))
+
+    def depart_container(self, node):
+        styles = node.attributes.get('classes', ())
+        if len(styles) > 0:
+            self.paragraph_style_stack.pop()
+
+    def visit_decoration(self, node):
+        pass
+
+    def depart_decoration(self, node):
+        pass
+
+    def visit_definition_list(self, node):
+        self.def_list_level += 1
+        if self.list_level > 5:
+            raise RuntimeError(
+                'max definition list nesting level exceeded')
+
+    def depart_definition_list(self, node):
+        self.def_list_level -= 1
+
+    def visit_definition_list_item(self, node):
+        pass
+
+    def depart_definition_list_item(self, node):
+        pass
+
+    def visit_term(self, node):
+        el = self.append_p('deflist-term-%d' % self.def_list_level)
+        el.text = node.astext()
+        self.set_current_element(el)
+        raise nodes.SkipChildren()
+
+    def depart_term(self, node):
+        self.set_to_parent()
+
+    def visit_definition(self, node):
+        self.paragraph_style_stack.append(
+            self.rststyle('deflist-def-%d' % self.def_list_level))
+        self.bumped_list_level_stack.append(ListLevel(1))
+
+    def depart_definition(self, node):
+        self.paragraph_style_stack.pop()
+        self.bumped_list_level_stack.pop()
+
+    def visit_classifier(self, node):
+        if len(self.current_element) > 0:
+            el = self.current_element[-1]
+            el1 = SubElement(
+                el, 'text:span',
+                attrib={'text:style-name': self.rststyle('emphasis')})
+            el1.text = ' (%s)' % (node.astext(), )
+
+    def depart_classifier(self, node):
+        pass
+
+    def visit_document(self, node):
+        pass
+
+    def depart_document(self, node):
+        self.process_footnotes()
+
+    def visit_docinfo(self, node):
+        self.section_level += 1
+        self.section_count += 1
+        if self.settings.create_sections:
+            el = self.append_child(
+                'text:section', attrib={
+                    'text:name': 'Section%d' % self.section_count,
+                    'text:style-name': 'Sect%d' % self.section_level,
+                }
+            )
+            self.set_current_element(el)
+
+    def depart_docinfo(self, node):
+        self.section_level -= 1
+        if self.settings.create_sections:
+            self.set_to_parent()
+
+    def visit_emphasis(self, node):
+        el = SubElement(
+            self.current_element, 'text:span',
+            attrib={'text:style-name': self.rststyle('emphasis')})
+        self.set_current_element(el)
+
+    def depart_emphasis(self, node):
+        self.set_to_parent()
+
+    def visit_enumerated_list(self, node):
+        el1 = self.current_element
+        if self.blockstyle == self.rststyle('blockquote'):
+            el2 = SubElement(el1, 'text:list', attrib={
+                'text:style-name': self.rststyle('blockquote-enumlist'),
+            })
+            self.list_style_stack.append(self.rststyle('blockquote-enumitem'))
+        elif self.blockstyle == self.rststyle('highlights'):
+            el2 = SubElement(el1, 'text:list', attrib={
+                'text:style-name': self.rststyle('highlights-enumlist'),
+            })
+            self.list_style_stack.append(self.rststyle('highlights-enumitem'))
+        elif self.blockstyle == self.rststyle('epigraph'):
+            el2 = SubElement(el1, 'text:list', attrib={
+                'text:style-name': self.rststyle('epigraph-enumlist'),
+            })
+            self.list_style_stack.append(self.rststyle('epigraph-enumitem'))
+        else:
+            liststylename = 'enumlist-%s' % (node.get('enumtype', 'arabic'), )
+            el2 = SubElement(el1, 'text:list', attrib={
+                'text:style-name': self.rststyle(liststylename),
+            })
+            self.list_style_stack.append(self.rststyle('enumitem'))
+        self.set_current_element(el2)
+
+    def depart_enumerated_list(self, node):
+        self.set_to_parent()
+        self.list_style_stack.pop()
+
+    def visit_list_item(self, node):
+        # If we are in a "bumped" list level, then wrap this
+        #   list in an outer lists in order to increase the
+        #   indentation level.
+        if self.in_table_of_contents:
+            if self.settings.generate_oowriter_toc:
+                self.paragraph_style_stack.append(
+                    self.rststyle('contents-%d' % (self.list_level, )))
+            else:
+                el1 = self.append_child('text:list-item')
+                self.set_current_element(el1)
+        else:
+            el1 = self.append_child('text:list-item')
+            el3 = el1
+            if len(self.bumped_list_level_stack) > 0:
+                level_obj = self.bumped_list_level_stack[-1]
+                if level_obj.get_sibling():
+                    level_obj.set_nested(False)
+                    for level_obj1 in self.bumped_list_level_stack:
+                        for idx in range(level_obj1.get_level()):
+                            el2 = self.append_child('text:list', parent=el3)
+                            el3 = self.append_child(
+                                'text:list-item', parent=el2)
+            self.paragraph_style_stack.append(self.list_style_stack[-1])
+            self.set_current_element(el3)
+
+    def depart_list_item(self, node):
+        if self.in_table_of_contents:
+            if self.settings.generate_oowriter_toc:
+                self.paragraph_style_stack.pop()
+            else:
+                self.set_to_parent()
+        else:
+            if len(self.bumped_list_level_stack) > 0:
+                level_obj = self.bumped_list_level_stack[-1]
+                if level_obj.get_sibling():
+                    level_obj.set_nested(True)
+                    for level_obj1 in self.bumped_list_level_stack:
+                        for idx in range(level_obj1.get_level()):
+                            self.set_to_parent()
+                            self.set_to_parent()
+            self.paragraph_style_stack.pop()
+            self.set_to_parent()
+
+    def visit_header(self, node):
+        self.in_header = True
+
+    def depart_header(self, node):
+        self.in_header = False
+
+    def visit_footer(self, node):
+        self.in_footer = True
+
+    def depart_footer(self, node):
+        self.in_footer = False
+
+    def visit_field(self, node):
+        pass
+
+    def depart_field(self, node):
+        pass
+
+    def visit_field_list(self, node):
+        pass
+
+    def depart_field_list(self, node):
+        pass
+
+    def visit_field_name(self, node):
+        el = self.append_p('textbody')
+        el1 = SubElement(
+            el, 'text:span',
+            attrib={'text:style-name': self.rststyle('strong')})
+        el1.text = node.astext()
+
+    def depart_field_name(self, node):
+        pass
+
+    def visit_field_body(self, node):
+        self.paragraph_style_stack.append(self.rststyle('blockindent'))
+
+    def depart_field_body(self, node):
+        self.paragraph_style_stack.pop()
+
+    def visit_figure(self, node):
+        pass
+
+    def depart_figure(self, node):
+        pass
+
+    def visit_footnote(self, node):
+        self.footnote_level += 1
+        self.save_footnote_current = self.current_element
+        el1 = Element('text:note-body')
+        self.current_element = el1
+        self.footnote_list.append((node, el1))
+        if isinstance(node, docutils.nodes.citation):
+            self.paragraph_style_stack.append(self.rststyle('citation'))
+        else:
+            self.paragraph_style_stack.append(self.rststyle('footnote'))
+
+    def depart_footnote(self, node):
+        self.paragraph_style_stack.pop()
+        self.current_element = self.save_footnote_current
+        self.footnote_level -= 1
+
+    footnote_chars = [
+        '*', '**', '***',
+        '++', '+++',
+        '##', '###',
+        '@@', '@@@',
+    ]
+
+    def visit_footnote_reference(self, node):
+        if self.footnote_level <= 0:
+            id = node.attributes['ids'][0]
+            refid = node.attributes.get('refid')
+            if refid is None:
+                refid = ''
+            if self.settings.endnotes_end_doc:
+                note_class = 'endnote'
+            else:
+                note_class = 'footnote'
+            el1 = self.append_child('text:note', attrib={
+                'text:id': '%s' % (refid, ),
+                'text:note-class': note_class,
+            })
+            note_auto = str(node.attributes.get('auto', 1))
+            if isinstance(node, docutils.nodes.citation_reference):
+                citation = '[%s]' % node.astext()
+                el2 = SubElement(el1, 'text:note-citation', attrib={
+                    'text:label': citation,
+                })
+                el2.text = citation
+            elif note_auto == '1':
+                el2 = SubElement(el1, 'text:note-citation', attrib={
+                    'text:label': node.astext(),
+                })
+                el2.text = node.astext()
+            elif note_auto == '*':
+                if self.footnote_chars_idx >= len(
+                        ODFTranslator.footnote_chars):
+                    self.footnote_chars_idx = 0
+                footnote_char = ODFTranslator.footnote_chars[
+                    self.footnote_chars_idx]
+                self.footnote_chars_idx += 1
+                el2 = SubElement(el1, 'text:note-citation', attrib={
+                    'text:label': footnote_char,
+                })
+                el2.text = footnote_char
+            self.footnote_ref_dict[id] = el1
+        raise nodes.SkipChildren()
+
+    def depart_footnote_reference(self, node):
+        pass
+
+    def visit_citation(self, node):
+        self.in_citation = True
+        for id in node.attributes['ids']:
+            self.citation_id = id
+            break
+        self.paragraph_style_stack.append(self.rststyle('blockindent'))
+        self.bumped_list_level_stack.append(ListLevel(1))
+
+    def depart_citation(self, node):
+        self.citation_id = None
+        self.paragraph_style_stack.pop()
+        self.bumped_list_level_stack.pop()
+        self.in_citation = False
+
+    def visit_citation_reference(self, node):
+        if self.settings.create_links:
+            id = node.attributes['refid']
+            el = self.append_child('text:reference-ref', attrib={
+                'text:ref-name': '%s' % (id, ),
+                'text:reference-format': 'text',
+            })
+            el.text = '['
+            self.set_current_element(el)
+        elif self.current_element.text is None:
+            self.current_element.text = '['
+        else:
+            self.current_element.text += '['
+
+    def depart_citation_reference(self, node):
+        self.current_element.text += ']'
+        if self.settings.create_links:
+            self.set_to_parent()
+
+    def visit_label(self, node):
+        if isinstance(node.parent, docutils.nodes.footnote):
+            raise nodes.SkipChildren()
+        elif self.citation_id is not None:
+            el = self.append_p('textbody')
+            self.set_current_element(el)
+            if self.settings.create_links:
+                el0 = SubElement(el, 'text:span')
+                el0.text = '['
+                self.append_child('text:reference-mark-start', attrib={
+                    'text:name': '%s' % (self.citation_id, ),
+                })
+            else:
+                el.text = '['
+
+    def depart_label(self, node):
+        if isinstance(node.parent, docutils.nodes.footnote):
+            pass
+        elif self.citation_id is not None:
+            if self.settings.create_links:
+                self.append_child('text:reference-mark-end', attrib={
+                    'text:name': '%s' % (self.citation_id, ),
+                })
+                el0 = SubElement(self.current_element, 'text:span')
+                el0.text = ']'
+            else:
+                self.current_element.text += ']'
+            self.set_to_parent()
+
+    def visit_generated(self, node):
+        pass
+
+    def depart_generated(self, node):
+        pass
+
+    def check_file_exists(self, path):
+        if os.path.exists(path):
+            return 1
+        else:
+            return 0
+
+    def visit_image(self, node):
+        # Capture the image file.
+        source = node['uri']
+        uri_parts = urllib.parse.urlparse(source)
+        if uri_parts.scheme in ('', 'file'):
+            source = urllib.parse.unquote(uri_parts.path)
+            if source.startswith('/'):
+                root_prefix = Path(self.settings.root_prefix)
+                source = (root_prefix/source[1:]).as_posix()
+            else:
+                # adapt relative paths
+                docsource, line = utils.get_source_line(node)
+                if docsource:
+                    dirname = os.path.dirname(docsource)
+                    if dirname:
+                        source = os.path.join(dirname, source)
+            if not self.check_file_exists(source):
+                self.document.reporter.warning(
+                    f'Cannot find image file "{source}".')
+                return
+        if source in self.image_dict:
+            filename, destination = self.image_dict[source]
+        else:
+            self.image_count += 1
+            filename = os.path.split(source)[1]
+            destination = 'Pictures/1%08x%s' % (self.image_count, filename)
+            if uri_parts.scheme in ('', 'file'):
+                spec = (os.path.abspath(source), destination,)
+            else:
+                try:
+                    with urllib.request.urlopen(source) as imgfile:
+                        content = imgfile.read()
+                except urllib.error.URLError as err:
+                    self.document.reporter.warning(
+                        f'Cannot open image URL "{source}". {err}')
+                    return
+                with tempfile.NamedTemporaryFile('wb',
+                                                 delete=False) as imgfile2:
+                    imgfile2.write(content)
+                source = imgfile2.name
+                spec = (source, destination,)
+            self.embedded_file_list.append(spec)
+            self.image_dict[source] = (source, destination,)
+        # Is this a figure (containing an image) or just a plain image?
+        if self.in_paragraph:
+            el1 = self.current_element
+        else:
+            el1 = SubElement(
+                self.current_element, 'text:p',
+                attrib={'text:style-name': self.rststyle('textbody')})
+        el2 = el1
+        if isinstance(node.parent, docutils.nodes.figure):
+            el3, el4, el5, caption = self.generate_figure(
+                node, source,
+                destination, el2)
+            attrib = {}
+            el6, width = self.generate_image(
+                node, source, destination,
+                el5, attrib)
+            if caption is not None:
+                el6.tail = caption
+        else:   # if isinstance(node.parent, docutils.nodes.image):
+            self.generate_image(node, source, destination, el2)
+
+    def depart_image(self, node):
+        pass
+
+    def get_image_width_height(self, node, attr):
+        size = None
+        unit = None
+        if attr in node.attributes:
+            size = node.attributes[attr]
+            size = size.strip()
+            # For conversion factors, see:
+            # http://www.unitconversion.org/unit_converter/typography-ex.html
+            try:
+                if size.endswith('%'):
+                    if attr == 'height':
+                        # Percentage allowed for width but not height.
+                        raise ValueError('percentage not allowed for height')
+                    size = size.rstrip(' %')
+                    size = float(size) / 100.0
+                    unit = '%'
+                else:
+                    size, unit = self.convert_to_cm(size)
+            except ValueError as exp:
+                self.document.reporter.warning(
+                    'Invalid %s for image: "%s".  '
+                    'Error: "%s".' % (
+                        attr, node.attributes[attr], exp))
+        return size, unit
+
+    def convert_to_cm(self, size):
+        """Convert various units to centimeters.
+
+        Note that a call to this method should be wrapped in:
+            try: except ValueError:
+        """
+        size = size.strip()
+        if size.endswith('px'):
+            size = float(size[:-2]) * 0.026     # convert px to cm
+        elif size.endswith('in'):
+            size = float(size[:-2]) * 2.54      # convert in to cm
+        elif size.endswith('pt'):
+            size = float(size[:-2]) * 0.035     # convert pt to cm
+        elif size.endswith('pc'):
+            size = float(size[:-2]) * 2.371     # convert pc to cm
+        elif size.endswith('mm'):
+            size = float(size[:-2]) * 0.1       # convert mm to cm
+        elif size.endswith('cm'):
+            size = float(size[:-2])
+        else:
+            raise ValueError('unknown unit type')
+        unit = 'cm'
+        return size, unit
+
+    def get_image_scale(self, node):
+        if 'scale' in node.attributes:
+            scale = node.attributes['scale']
+            try:
+                scale = int(scale)
+            except ValueError:
+                self.document.reporter.warning(
+                    'Invalid scale for image: "%s"' % (
+                        node.attributes['scale'], ))
+            if scale < 1:       # or scale > 100:
+                self.document.reporter.warning(
+                    'scale out of range (%s), using 1.' % (scale, ))
+                scale = 1
+            scale = scale * 0.01
+        else:
+            scale = 1.0
+        return scale
+
+    def get_image_scaled_width_height(self, node, source):
+        """Return the image size in centimeters adjusted by image attrs."""
+        scale = self.get_image_scale(node)
+        width, width_unit = self.get_image_width_height(node, 'width')
+        height, _ = self.get_image_width_height(node, 'height')
+        dpi = (72, 72)
+        if PIL is not None and source in self.image_dict:
+            filename, destination = self.image_dict[source]
+            with PIL.Image.open(filename, 'r') as img:
+                img_size = img.size
+                dpi = img.info.get('dpi', dpi)
+            # dpi information can be (xdpi, ydpi) or xydpi
+            try:
+                iter(dpi)
+            except TypeError:
+                dpi = (dpi, dpi)
+        else:
+            img_size = None
+        if width is None or height is None:
+            if img_size is None:
+                raise RuntimeError(
+                    'image size not fully specified and PIL not installed')
+            if width is None:
+                width = img_size[0]
+                width = float(width) * 0.026        # convert px to cm
+            if height is None:
+                height = img_size[1]
+                height = float(height) * 0.026      # convert px to cm
+            if width_unit == '%':
+                factor = width
+                image_width = img_size[0]
+                image_width = float(image_width) * 0.026    # convert px to cm
+                image_height = img_size[1]
+                image_height = float(image_height) * 0.026  # convert px to cm
+                line_width = self.get_page_width()
+                width = factor * line_width
+                factor = (factor * line_width) / image_width
+                height = factor * image_height
+        width *= scale
+        height *= scale
+        width = '%.2fcm' % width
+        height = '%.2fcm' % height
+        return width, height
+
+    def get_page_width(self):
+        """Return the document's page width in centimeters."""
+        root = self.get_dom_stylesheet()
+        nodes = root.iterfind(
+            './/{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+            'page-layout/'
+            '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+            'page-layout-properties')
+        width = None
+        for node in nodes:
+            page_width = node.get(
+                '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
+                'page-width')
+            margin_left = node.get(
+                '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
+                'margin-left')
+            margin_right = node.get(
+                '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
+                'margin-right')
+            if (page_width is None
+                or margin_left is None
+                or margin_right is None):
+                continue
+            try:
+                page_width, _ = self.convert_to_cm(page_width)
+                margin_left, _ = self.convert_to_cm(margin_left)
+                margin_right, _ = self.convert_to_cm(margin_right)
+            except ValueError:
+                self.document.reporter.warning(
+                    'Stylesheet file contains invalid page width '
+                    'or margin size.')
+            width = page_width - margin_left - margin_right
+        if width is None:
+            # We can't find the width in styles, so we make a guess.
+            # Use a width of 6 in = 15.24 cm.
+            width = 15.24
+        return width
+
+    def generate_figure(self, node, source, destination, current_element):
+        caption = None
+        width, height = self.get_image_scaled_width_height(node, source)
+        for node1 in node.parent.children:
+            if node1.tagname == 'caption':
+                caption = node1.astext()
+        self.image_style_count += 1
+        #
+        # Add the style for the caption.
+        if caption is not None:
+            attrib = {
+                'style:class': 'extra',
+                'style:family': 'paragraph',
+                'style:name': 'Caption',
+                'style:parent-style-name': 'Standard',
+            }
+            el1 = SubElement(self.automatic_styles, 'style:style',
+                             attrib=attrib, nsdict=SNSD)
+            attrib = {
+                'fo:margin-bottom': '0.0835in',
+                'fo:margin-top': '0.0835in',
+                'text:line-number': '0',
+                'text:number-lines': 'false',
+            }
+            SubElement(el1, 'style:paragraph-properties',
+                       attrib=attrib, nsdict=SNSD)
+            attrib = {
+                'fo:font-size': '12pt',
+                'fo:font-style': 'italic',
+                'style:font-name': 'Times',
+                'style:font-name-complex': 'Lucidasans1',
+                'style:font-size-asian': '12pt',
+                'style:font-size-complex': '12pt',
+                'style:font-style-asian': 'italic',
+                'style:font-style-complex': 'italic',
+            }
+            SubElement(el1, 'style:text-properties',
+                       attrib=attrib, nsdict=SNSD)
+        style_name = 'rstframestyle%d' % self.image_style_count
+        draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER)
+        # Add the styles
+        attrib = {
+            'style:name': style_name,
+            'style:family': 'graphic',
+            'style:parent-style-name': self.rststyle('figureframe'),
+        }
+        el1 = SubElement(self.automatic_styles,
+                         'style:style', attrib=attrib, nsdict=SNSD)
+        attrib = {}
+        wrap = False
+        classes = node.parent.attributes.get('classes')
+        if classes and 'wrap' in classes:
+            wrap = True
+        if wrap:
+            attrib['style:wrap'] = 'dynamic'
+        else:
+            attrib['style:wrap'] = 'none'
+        SubElement(el1, 'style:graphic-properties',
+                   attrib=attrib, nsdict=SNSD)
+        attrib = {
+            'draw:style-name': style_name,
+            'draw:name': draw_name,
+            'text:anchor-type': 'paragraph',
+            'draw:z-index': '0',
+        }
+        attrib['svg:width'] = width
+        el3 = SubElement(current_element, 'draw:frame', attrib=attrib)
+        attrib = {}
+        el4 = SubElement(el3, 'draw:text-box', attrib=attrib)
+        attrib = {
+            'text:style-name': self.rststyle('caption'),
+        }
+        el5 = SubElement(el4, 'text:p', attrib=attrib)
+        return el3, el4, el5, caption
+
+    def generate_image(self, node, source, destination, current_element,
+                       frame_attrs=None):
+        width, height = self.get_image_scaled_width_height(node, source)
+        self.image_style_count += 1
+        style_name = 'rstframestyle%d' % self.image_style_count
+        # Add the style.
+        attrib = {
+            'style:name': style_name,
+            'style:family': 'graphic',
+            'style:parent-style-name': self.rststyle('image'),
+        }
+        el1 = SubElement(self.automatic_styles,
+                         'style:style', attrib=attrib, nsdict=SNSD)
+        halign = None
+        valign = None
+        if 'align' in node.attributes:
+            align = node.attributes['align'].split()
+            for val in align:
+                if val in ('left', 'center', 'right'):
+                    halign = val
+                elif val in ('top', 'middle', 'bottom'):
+                    valign = val
+        if frame_attrs is None:
+            attrib = {
+                'style:vertical-pos': 'top',
+                'style:vertical-rel': 'paragraph',
+                'style:horizontal-rel': 'paragraph',
+                'style:mirror': 'none',
+                'fo:clip': 'rect(0cm 0cm 0cm 0cm)',
+                'draw:luminance': '0%',
+                'draw:contrast': '0%',
+                'draw:red': '0%',
+                'draw:green': '0%',
+                'draw:blue': '0%',
+                'draw:gamma': '100%',
+                'draw:color-inversion': 'false',
+                'draw:image-opacity': '100%',
+                'draw:color-mode': 'standard',
+            }
+        else:
+            attrib = frame_attrs
+        if halign is not None:
+            attrib['style:horizontal-pos'] = halign
+        if valign is not None:
+            attrib['style:vertical-pos'] = valign
+        # If there is a classes/wrap directive or we are
+        #   inside a table, add a no-wrap style.
+        wrap = False
+        classes = node.attributes.get('classes')
+        if classes and 'wrap' in classes:
+            wrap = True
+        if wrap:
+            attrib['style:wrap'] = 'dynamic'
+        else:
+            attrib['style:wrap'] = 'none'
+        # If we are inside a table, add a no-wrap style.
+        if self.is_in_table(node):
+            attrib['style:wrap'] = 'none'
+        SubElement(el1, 'style:graphic-properties',
+                   attrib=attrib, nsdict=SNSD)
+        draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER)
+        # Add the content.
+        # el = SubElement(current_element, 'text:p',
+        #     attrib={'text:style-name': self.rststyle('textbody')})
+        attrib = {
+            'draw:style-name': style_name,
+            'draw:name': draw_name,
+            'draw:z-index': '1',
+        }
+        if isinstance(node.parent, nodes.TextElement):
+            attrib['text:anchor-type'] = 'as-char'      # vds
+        else:
+            attrib['text:anchor-type'] = 'paragraph'
+        attrib['svg:width'] = width
+        attrib['svg:height'] = height
+        el1 = SubElement(current_element, 'draw:frame', attrib=attrib)
+        SubElement(el1, 'draw:image', attrib={
+            'xlink:href': '%s' % (destination, ),
+            'xlink:type': 'simple',
+            'xlink:show': 'embed',
+            'xlink:actuate': 'onLoad',
+        })
+        return el1, width
+
+    def is_in_table(self, node):
+        node1 = node.parent
+        while node1:
+            if isinstance(node1, docutils.nodes.entry):
+                return True
+            node1 = node1.parent
+        return False
+
+    def visit_legend(self, node):
+        if isinstance(node.parent, docutils.nodes.figure):
+            el1 = self.current_element[-1]
+            el1 = el1[0][0]
+            self.current_element = el1
+            self.paragraph_style_stack.append(self.rststyle('legend'))
+
+    def depart_legend(self, node):
+        if isinstance(node.parent, docutils.nodes.figure):
+            self.paragraph_style_stack.pop()
+            self.set_to_parent()
+            self.set_to_parent()
+            self.set_to_parent()
+
+    def visit_line_block(self, node):
+        self.line_indent_level += 1
+        self.line_block_level += 1
+
+    def depart_line_block(self, node):
+        self.line_indent_level -= 1
+        self.line_block_level -= 1
+
+    def visit_line(self, node):
+        style = 'lineblock%d' % self.line_indent_level
+        el1 = SubElement(self.current_element, 'text:p',
+                         attrib={'text:style-name': self.rststyle(style), })
+        self.current_element = el1
+
+    def depart_line(self, node):
+        self.set_to_parent()
+
+    def visit_literal(self, node):
+        el = SubElement(
+            self.current_element, 'text:span',
+            attrib={'text:style-name': self.rststyle('inlineliteral')})
+        self.set_current_element(el)
+
+    def depart_literal(self, node):
+        self.set_to_parent()
+
+    def visit_inline(self, node):
+        styles = node.attributes.get('classes', ())
+        if styles:
+            el = self.current_element
+            for inline_style in styles:
+                el = SubElement(el, 'text:span',
+                                attrib={'text:style-name':
+                                        self.rststyle(inline_style)})
+            count = len(styles)
+        else:
+            # No style was specified so use a default style (old code
+            # crashed if no style was given)
+            el = SubElement(self.current_element, 'text:span')
+            count = 1
+
+        self.set_current_element(el)
+        self.inline_style_count_stack.append(count)
+
+    def depart_inline(self, node):
+        count = self.inline_style_count_stack.pop()
+        for x in range(count):
+            self.set_to_parent()
+
+    def _calculate_code_block_padding(self, line):
+        count = 0
+        matchobj = SPACES_PATTERN.match(line)
+        if matchobj:
+            pad = matchobj.group()
+            count = len(pad)
+        else:
+            matchobj = TABS_PATTERN.match(line)
+            if matchobj:
+                pad = matchobj.group()
+                count = len(pad) * 8
+        return count
+
+    def _add_syntax_highlighting(self, insource, language):
+        lexer = pygments.lexers.get_lexer_by_name(language, stripall=True)
+        if language in ('latex', 'tex'):
+            fmtr = OdtPygmentsLaTeXFormatter(
+                lambda name, parameters=():
+                self.rststyle(name, parameters),
+                escape_function=escape_cdata)
+        else:
+            fmtr = OdtPygmentsProgFormatter(
+                lambda name, parameters=():
+                self.rststyle(name, parameters),
+                escape_function=escape_cdata)
+        return pygments.highlight(insource, lexer, fmtr)
+
+    def fill_line(self, line):
+        line = FILL_PAT1.sub(self.fill_func1, line)
+        return FILL_PAT2.sub(self.fill_func2, line)
+
+    def fill_func1(self, matchobj):
+        spaces = matchobj.group(0)
+        return '<text:s text:c="%d"/>' % (len(spaces), )
+
+    def fill_func2(self, matchobj):
+        spaces = matchobj.group(0)
+        return ' <text:s text:c="%d"/>' % (len(spaces) - 1, )
+
+    def visit_literal_block(self, node):
+        if len(self.paragraph_style_stack) > 1:
+            wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % (
+                self.rststyle('codeblock-indented'), )
+        else:
+            wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % (
+                self.rststyle('codeblock'), )
+        source = node.astext()
+        if (pygments and self.settings.add_syntax_highlighting):
+            language = node.get('language', 'python')
+            source = self._add_syntax_highlighting(source, language)
+        else:
+            source = escape_cdata(source)
+        lines = source.split('\n')
+        # If there is an empty last line, remove it.
+        if lines[-1] == '':
+            del lines[-1]
+        lines1 = ['<wrappertag1 xmlns:text="urn:oasis:names:tc:'
+                  'opendocument:xmlns:text:1.0">']
+        my_lines = []
+        for my_line in lines:
+            my_line = self.fill_line(my_line)
+            my_line = my_line.replace("&#10;", "\n")
+            my_lines.append(my_line)
+        my_lines_str = '<text:line-break/>'.join(my_lines)
+        my_lines_str2 = wrapper1 % (my_lines_str, )
+        lines1.append(my_lines_str2)
+        lines1.append('</wrappertag1>')
+        s1 = ''.join(lines1)
+        s1 = s1.encode("utf-8")
+        el1 = etree.fromstring(s1)
+        for child in el1:
+            self.current_element.append(child)
+
+    def depart_literal_block(self, node):
+        pass
+
+    visit_doctest_block = visit_literal_block
+    depart_doctest_block = depart_literal_block
+
+    # placeholder for math (see docs/dev/todo.txt)
+    def visit_math(self, node):
+        self.document.reporter.warning('"math" role not supported',
+                                       base_node=node)
+        self.visit_literal(node)
+
+    def depart_math(self, node):
+        self.depart_literal(node)
+
+    def visit_math_block(self, node):
+        self.document.reporter.warning('"math" directive not supported',
+                                       base_node=node)
+        self.visit_literal_block(node)
+
+    def depart_math_block(self, node):
+        self.depart_literal_block(node)
+
+    def visit_meta(self, node):
+        name = node.attributes.get('name')
+        content = node.attributes.get('content')
+        if name is not None and content is not None:
+            self.meta_dict[name] = content
+
+    def depart_meta(self, node):
+        pass
+
+    def visit_option_list(self, node):
+        table_name = 'tableoption'
+        #
+        # Generate automatic styles
+        if not self.optiontablestyles_generated:
+            self.optiontablestyles_generated = True
+            el = SubElement(self.automatic_styles, 'style:style', attrib={
+                'style:name': self.rststyle(table_name),
+                'style:family': 'table'}, nsdict=SNSD)
+            el1 = SubElement(el, 'style:table-properties', attrib={
+                'style:width': '17.59cm',
+                'table:align': 'left',
+                'style:shadow': 'none'}, nsdict=SNSD)
+            el = SubElement(self.automatic_styles, 'style:style', attrib={
+                'style:name': self.rststyle('%s.%%c' % table_name, ('A', )),
+                'style:family': 'table-column'}, nsdict=SNSD)
+            el1 = SubElement(el, 'style:table-column-properties', attrib={
+                'style:column-width': '4.999cm'}, nsdict=SNSD)
+            el = SubElement(self.automatic_styles, 'style:style', attrib={
+                'style:name': self.rststyle('%s.%%c' % table_name, ('B', )),
+                'style:family': 'table-column'}, nsdict=SNSD)
+            el1 = SubElement(el, 'style:table-column-properties', attrib={
+                'style:column-width': '12.587cm'}, nsdict=SNSD)
+            el = SubElement(self.automatic_styles, 'style:style', attrib={
+                'style:name': self.rststyle(
+                    '%s.%%c%%d' % table_name, ('A', 1, )),
+                'style:family': 'table-cell'}, nsdict=SNSD)
+            el1 = SubElement(el, 'style:table-cell-properties', attrib={
+                'fo:background-color': 'transparent',
+                'fo:padding': '0.097cm',
+                'fo:border-left': '0.035cm solid #000000',
+                'fo:border-right': 'none',
+                'fo:border-top': '0.035cm solid #000000',
+                'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
+            el2 = SubElement(el1, 'style:background-image', nsdict=SNSD)
+            el = SubElement(self.automatic_styles, 'style:style', attrib={
+                'style:name': self.rststyle(
+                    '%s.%%c%%d' % table_name, ('B', 1, )),
+                'style:family': 'table-cell'}, nsdict=SNSD)
+            el1 = SubElement(el, 'style:table-cell-properties', attrib={
+                'fo:padding': '0.097cm',
+                'fo:border': '0.035cm solid #000000'}, nsdict=SNSD)
+            el = SubElement(self.automatic_styles, 'style:style', attrib={
+                'style:name': self.rststyle(
+                    '%s.%%c%%d' % table_name, ('A', 2, )),
+                'style:family': 'table-cell'}, nsdict=SNSD)
+            el1 = SubElement(el, 'style:table-cell-properties', attrib={
+                'fo:padding': '0.097cm',
+                'fo:border-left': '0.035cm solid #000000',
+                'fo:border-right': 'none',
+                'fo:border-top': 'none',
+                'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
+            el = SubElement(self.automatic_styles, 'style:style', attrib={
+                'style:name': self.rststyle(
+                    '%s.%%c%%d' % table_name, ('B', 2, )),
+                'style:family': 'table-cell'}, nsdict=SNSD)
+            el1 = SubElement(el, 'style:table-cell-properties', attrib={
+                'fo:padding': '0.097cm',
+                'fo:border-left': '0.035cm solid #000000',
+                'fo:border-right': '0.035cm solid #000000',
+                'fo:border-top': 'none',
+                'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
+        #
+        # Generate table data
+        el = self.append_child('table:table', attrib={
+            'table:name': self.rststyle(table_name),
+            'table:style-name': self.rststyle(table_name),
+        })
+        el1 = SubElement(el, 'table:table-column', attrib={
+            'table:style-name': self.rststyle(
+                '%s.%%c' % table_name, ('A', ))})
+        el1 = SubElement(el, 'table:table-column', attrib={
+            'table:style-name': self.rststyle(
+                '%s.%%c' % table_name, ('B', ))})
+        el1 = SubElement(el, 'table:table-header-rows')
+        el2 = SubElement(el1, 'table:table-row')
+        el3 = SubElement(el2, 'table:table-cell', attrib={
+            'table:style-name': self.rststyle(
+                '%s.%%c%%d' % table_name, ('A', 1, )),
+            'office:value-type': 'string'})
+        el4 = SubElement(el3, 'text:p', attrib={
+            'text:style-name': 'Table_20_Heading'})
+        el4.text = 'Option'
+        el3 = SubElement(el2, 'table:table-cell', attrib={
+            'table:style-name': self.rststyle(
+                '%s.%%c%%d' % table_name, ('B', 1, )),
+            'office:value-type': 'string'})
+        el4 = SubElement(el3, 'text:p', attrib={
+            'text:style-name': 'Table_20_Heading'})
+        el4.text = 'Description'
+        self.set_current_element(el)
+
+    def depart_option_list(self, node):
+        self.set_to_parent()
+
+    def visit_option_list_item(self, node):
+        el = self.append_child('table:table-row')
+        self.set_current_element(el)
+
+    def depart_option_list_item(self, node):
+        self.set_to_parent()
+
+    def visit_option_group(self, node):
+        el = self.append_child('table:table-cell', attrib={
+            'table:style-name': 'Table%d.A2' % self.table_count,
+            'office:value-type': 'string',
+        })
+        self.set_current_element(el)
+
+    def depart_option_group(self, node):
+        self.set_to_parent()
+
+    def visit_option(self, node):
+        el = self.append_child('text:p', attrib={
+            'text:style-name': 'Table_20_Contents'})
+        el.text = node.astext()
+
+    def depart_option(self, node):
+        pass
+
+    def visit_option_string(self, node):
+        pass
+
+    def depart_option_string(self, node):
+        pass
+
+    def visit_option_argument(self, node):
+        pass
+
+    def depart_option_argument(self, node):
+        pass
+
+    def visit_description(self, node):
+        el = self.append_child('table:table-cell', attrib={
+            'table:style-name': 'Table%d.B2' % self.table_count,
+            'office:value-type': 'string',
+        })
+        el1 = SubElement(el, 'text:p', attrib={
+            'text:style-name': 'Table_20_Contents'})
+        el1.text = node.astext()
+        raise nodes.SkipChildren()
+
+    def depart_description(self, node):
+        pass
+
+    def visit_paragraph(self, node):
+        self.in_paragraph = True
+        if self.in_header:
+            el = self.append_p('header')
+        elif self.in_footer:
+            el = self.append_p('footer')
+        else:
+            style_name = self.paragraph_style_stack[-1]
+            el = self.append_child(
+                'text:p',
+                attrib={'text:style-name': style_name})
+            self.append_pending_ids(el)
+        self.set_current_element(el)
+
+    def depart_paragraph(self, node):
+        self.in_paragraph = False
+        self.set_to_parent()
+        if self.in_header:
+            self.header_content.append(self.current_element[-1])
+            self.current_element.remove(self.current_element[-1])
+        elif self.in_footer:
+            self.footer_content.append(self.current_element[-1])
+            self.current_element.remove(self.current_element[-1])
+
+    def visit_problematic(self, node):
+        pass
+
+    def depart_problematic(self, node):
+        pass
+
+    def visit_raw(self, node):
+        if 'format' in node.attributes:
+            formats = node.attributes['format']
+            formatlist = formats.split()
+            if 'odt' in formatlist:
+                rawstr = node.astext()
+                attrstr = ' '.join(
+                    '%s="%s"' % (k, v, )
+                    for k, v in list(CONTENT_NAMESPACE_ATTRIB.items()))
+                contentstr = '<stuff %s>%s</stuff>' % (attrstr, rawstr, )
+                contentstr = contentstr.encode("utf-8")
+                content = etree.fromstring(contentstr)
+                if len(content) > 0:
+                    el1 = content[0]
+                    if self.in_header:
+                        pass
+                    elif self.in_footer:
+                        pass
+                    else:
+                        self.current_element.append(el1)
+        raise nodes.SkipChildren()
+
+    def depart_raw(self, node):
+        if self.in_header:
+            pass
+        elif self.in_footer:
+            pass
+        else:
+            pass
+
+    def visit_reference(self, node):
+        # text = node.astext()
+        if self.settings.create_links:
+            if 'refuri' in node:
+                href = node['refuri']
+                if (self.settings.cloak_email_addresses
+                    and href.startswith('mailto:')):
+                    href = self.cloak_mailto(href)
+                el = self.append_child('text:a', attrib={
+                    'xlink:href': '%s' % href,
+                    'xlink:type': 'simple',
+                })
+                self.set_current_element(el)
+            elif 'refid' in node:
+                if self.settings.create_links:
+                    href = node['refid']
+                    el = self.append_child('text:reference-ref', attrib={
+                        'text:ref-name': '%s' % href,
+                        'text:reference-format': 'text',
+                    })
+            else:
+                self.document.reporter.warning(
+                    'References must have "refuri" or "refid" attribute.')
+        if (self.in_table_of_contents
+            and len(node.children) >= 1
+            and isinstance(node.children[0], docutils.nodes.generated)):
+            node.remove(node.children[0])
+
+    def depart_reference(self, node):
+        if self.settings.create_links:
+            if 'refuri' in node:
+                self.set_to_parent()
+
+    def visit_rubric(self, node):
+        style_name = self.rststyle('rubric')
+        classes = node.get('classes')
+        if classes:
+            class1 = classes[0]
+            if class1:
+                style_name = class1
+        el = SubElement(self.current_element, 'text:h', attrib={
+            # 'text:outline-level': '%d' % section_level,
+            # 'text:style-name': 'Heading_20_%d' % section_level,
+            'text:style-name': style_name,
+        })
+        text = node.astext()
+        el.text = self.encode(text)
+
+    def depart_rubric(self, node):
+        pass
+
+    def visit_section(self, node, move_ids=1):
+        self.section_level += 1
+        self.section_count += 1
+        if self.settings.create_sections:
+            el = self.append_child('text:section', attrib={
+                'text:name': 'Section%d' % self.section_count,
+                'text:style-name': 'Sect%d' % self.section_level,
+            })
+            self.set_current_element(el)
+
+    def depart_section(self, node):
+        self.section_level -= 1
+        if self.settings.create_sections:
+            self.set_to_parent()
+
+    def visit_strong(self, node):
+        el = SubElement(self.current_element, 'text:span',
+                        attrib={'text:style-name': self.rststyle('strong')})
+        self.set_current_element(el)
+
+    def depart_strong(self, node):
+        self.set_to_parent()
+
+    def visit_substitution_definition(self, node):
+        raise nodes.SkipChildren()
+
+    def depart_substitution_definition(self, node):
+        pass
+
+    def visit_system_message(self, node):
+        pass
+
+    def depart_system_message(self, node):
+        pass
+
+    def get_table_style(self, node):
+        table_style = None
+        table_name = None
+        str_classes = node.get('classes')
+        if str_classes is not None:
+            for str_class in str_classes:
+                if str_class.startswith(TABLESTYLEPREFIX):
+                    table_name = str_class
+                    break
+        if table_name is not None:
+            table_style = self.table_styles.get(table_name)
+            if table_style is None:
+                # If we can't find the table style, issue warning
+                #   and use the default table style.
+                self.document.reporter.warning(
+                    'Can\'t find table style "%s".  Using default.' % (
+                        table_name, ))
+                table_name = TABLENAMEDEFAULT
+                table_style = self.table_styles.get(table_name)
+                if table_style is None:
+                    # If we can't find the default table style, issue a warning
+                    #   and use a built-in default style.
+                    self.document.reporter.warning(
+                        'Can\'t find default table style "%s".  '
+                        'Using built-in default.' % (
+                            table_name, ))
+                    table_style = BUILTIN_DEFAULT_TABLE_STYLE
+        else:
+            table_name = TABLENAMEDEFAULT
+            table_style = self.table_styles.get(table_name)
+            if table_style is None:
+                # If we can't find the default table style, issue a warning
+                #   and use a built-in default style.
+                self.document.reporter.warning(
+                    'Can\'t find default table style "%s".  '
+                    'Using built-in default.' % (
+                        table_name, ))
+                table_style = BUILTIN_DEFAULT_TABLE_STYLE
+        return table_style
+
+    def visit_table(self, node):
+        self.table_count += 1
+        table_style = self.get_table_style(node)
+        table_name = '%s%%d' % TABLESTYLEPREFIX
+        el1 = SubElement(self.automatic_styles, 'style:style', attrib={
+            'style:name': self.rststyle(
+                '%s' % table_name, (self.table_count, )),
+            'style:family': 'table',
+        }, nsdict=SNSD)
+        if table_style.backgroundcolor is None:
+            SubElement(el1, 'style:table-properties', attrib={
+                # 'style:width': '17.59cm',
+                # 'table:align': 'margins',
+                'table:align': 'left',
+                'fo:margin-top': '0in',
+                'fo:margin-bottom': '0.10in',
+            }, nsdict=SNSD)
+        else:
+            SubElement(el1, 'style:table-properties', attrib={
+                # 'style:width': '17.59cm',
+                'table:align': 'margins',
+                'fo:margin-top': '0in',
+                'fo:margin-bottom': '0.10in',
+                'fo:background-color': table_style.backgroundcolor,
+            }, nsdict=SNSD)
+        # We use a single cell style for all cells in this table.
+        # That's probably not correct, but seems to work.
+        el2 = SubElement(self.automatic_styles, 'style:style', attrib={
+            'style:name': self.rststyle(
+                '%s.%%c%%d' % table_name, (self.table_count, 'A', 1, )),
+            'style:family': 'table-cell',
+        }, nsdict=SNSD)
+        thickness = self.settings.table_border_thickness
+        if thickness is None:
+            line_style1 = table_style.border
+        else:
+            line_style1 = '0.%03dcm solid #000000' % (thickness, )
+        SubElement(el2, 'style:table-cell-properties', attrib={
+            'fo:padding': '0.049cm',
+            'fo:border-left': line_style1,
+            'fo:border-right': line_style1,
+            'fo:border-top': line_style1,
+            'fo:border-bottom': line_style1,
+        }, nsdict=SNSD)
+        title = None
+        for child in node.children:
+            if child.tagname == 'title':
+                title = child.astext()
+                break
+        if title is not None:
+            self.append_p('table-title', title)
+        else:
+            pass
+        el4 = SubElement(self.current_element, 'table:table', attrib={
+            'table:name': self.rststyle(
+                '%s' % table_name, (self.table_count, )),
+            'table:style-name': self.rststyle(
+                '%s' % table_name, (self.table_count, )),
+        })
+        self.set_current_element(el4)
+        self.current_table_style = el1
+        self.table_width = 0.0
+
+    def depart_table(self, node):
+        attribkey = add_ns('style:width', nsdict=SNSD)
+        attribval = '%.4fin' % (self.table_width, )
+        el1 = self.current_table_style
+        el2 = el1[0]
+        el2.attrib[attribkey] = attribval
+        self.set_to_parent()
+
+    def visit_tgroup(self, node):
+        self.column_count = ord('A') - 1
+
+    def depart_tgroup(self, node):
+        pass
+
+    def visit_colspec(self, node):
+        self.column_count += 1
+        colspec_name = self.rststyle(
+            '%s%%d.%%s' % TABLESTYLEPREFIX,
+            (self.table_count, chr(self.column_count), )
+        )
+        colwidth = node['colwidth'] / 12.0
+        el1 = SubElement(self.automatic_styles, 'style:style', attrib={
+            'style:name': colspec_name,
+            'style:family': 'table-column',
+        }, nsdict=SNSD)
+        SubElement(el1, 'style:table-column-properties',
+                   attrib={'style:column-width': '%.4fin' % colwidth},
+                   nsdict=SNSD)
+        self.append_child('table:table-column',
+                          attrib={'table:style-name': colspec_name, })
+        self.table_width += colwidth
+
+    def depart_colspec(self, node):
+        pass
+
+    def visit_thead(self, node):
+        el = self.append_child('table:table-header-rows')
+        self.set_current_element(el)
+        self.in_thead = True
+        self.paragraph_style_stack.append('Table_20_Heading')
+
+    def depart_thead(self, node):
+        self.set_to_parent()
+        self.in_thead = False
+        self.paragraph_style_stack.pop()
+
+    def visit_row(self, node):
+        self.column_count = ord('A') - 1
+        el = self.append_child('table:table-row')
+        self.set_current_element(el)
+
+    def depart_row(self, node):
+        self.set_to_parent()
+
+    def visit_entry(self, node):
+        self.column_count += 1
+        cellspec_name = self.rststyle(
+            '%s%%d.%%c%%d' % TABLESTYLEPREFIX,
+            (self.table_count, 'A', 1, )
+        )
+        attrib = {
+            'table:style-name': cellspec_name,
+            'office:value-type': 'string',
+        }
+        morecols = node.get('morecols', 0)
+        if morecols > 0:
+            attrib['table:number-columns-spanned'] = '%d' % (morecols + 1,)
+            self.column_count += morecols
+        morerows = node.get('morerows', 0)
+        if morerows > 0:
+            attrib['table:number-rows-spanned'] = '%d' % (morerows + 1,)
+        el1 = self.append_child('table:table-cell', attrib=attrib)
+        self.set_current_element(el1)
+
+    def depart_entry(self, node):
+        self.set_to_parent()
+
+    def visit_tbody(self, node):
+        pass
+
+    def depart_tbody(self, node):
+        pass
+
+    def visit_target(self, node):
+        #
+        # I don't know how to implement targets in ODF.
+        # How do we create a target in oowriter?  A cross-reference?
+        if ('refuri' not in node
+                and 'refid' not in node
+                and 'refname' not in node):
+            pass
+        else:
+            pass
+
+    def depart_target(self, node):
+        pass
+
+    def visit_title(self, node, move_ids=1, title_type='title'):
+        if isinstance(node.parent, docutils.nodes.section):
+            section_level = self.section_level
+            if section_level > 7:
+                self.document.reporter.warning(
+                    'Heading/section levels greater than 7 not supported.')
+                self.document.reporter.warning(
+                    '    Reducing to heading level 7 for heading: "%s"' % (
+                        node.astext(), ))
+                section_level = 7
+            el1 = self.append_child(
+                'text:h', attrib={
+                    'text:outline-level': '%d' % section_level,
+                    # 'text:style-name': 'Heading_20_%d' % section_level,
+                    'text:style-name': self.rststyle(
+                        'heading%d', (section_level, )),
+                })
+            self.append_pending_ids(el1)
+            self.set_current_element(el1)
+        elif isinstance(node.parent, docutils.nodes.document):
+            # text = self.settings.title
+            # else:
+            #     text = node.astext()
+            el1 = SubElement(self.current_element, 'text:p', attrib={
+                'text:style-name': self.rststyle(title_type),
+            })
+            self.append_pending_ids(el1)
+            text = node.astext()
+            self.title = text
+            self.found_doc_title = True
+            self.set_current_element(el1)
+
+    def depart_title(self, node):
+        if (isinstance(node.parent, docutils.nodes.section)
+            or isinstance(node.parent, docutils.nodes.document)):
+            self.set_to_parent()
+
+    def visit_subtitle(self, node, move_ids=1):
+        self.visit_title(node, move_ids, title_type='subtitle')
+
+    def depart_subtitle(self, node):
+        self.depart_title(node)
+
+    def visit_title_reference(self, node):
+        el = self.append_child('text:span', attrib={
+            'text:style-name': self.rststyle('quotation')})
+        el.text = self.encode(node.astext())
+        raise nodes.SkipChildren()
+
+    def depart_title_reference(self, node):
+        pass
+
+    def generate_table_of_content_entry_template(self, el1):
+        for idx in range(1, 11):
+            el2 = SubElement(
+                el1,
+                'text:table-of-content-entry-template',
+                attrib={
+                    'text:outline-level': "%d" % (idx, ),
+                    'text:style-name': self.rststyle('contents-%d' % (idx, )),
+                })
+            SubElement(el2, 'text:index-entry-chapter')
+            SubElement(el2, 'text:index-entry-text')
+            SubElement(el2, 'text:index-entry-tab-stop', attrib={
+                'style:leader-char': ".",
+                'style:type': "right",
+            })
+            SubElement(el2, 'text:index-entry-page-number')
+
+    def find_title_label(self, node, class_type, label_key):
+        label = ''
+        title_node = None
+        for child in node.children:
+            if isinstance(child, class_type):
+                title_node = child
+                break
+        if title_node is not None:
+            label = title_node.astext()
+        else:
+            label = self.language.labels[label_key]
+        return label
+
+    def visit_topic(self, node):
+        if 'classes' in node.attributes:
+            if 'contents' in node.attributes['classes']:
+                label = self.find_title_label(
+                    node, docutils.nodes.title, 'contents')
+                if self.settings.generate_oowriter_toc:
+                    el1 = self.append_child('text:table-of-content', attrib={
+                        'text:name': 'Table of Contents1',
+                        'text:protected': 'true',
+                        'text:style-name': 'Sect1',
+                    })
+                    el2 = SubElement(
+                        el1,
+                        'text:table-of-content-source',
+                        attrib={
+                            'text:outline-level': '10',
+                        })
+                    el3 = SubElement(el2, 'text:index-title-template', attrib={
+                        'text:style-name': 'Contents_20_Heading',
+                    })
+                    el3.text = label
+                    self.generate_table_of_content_entry_template(el2)
+                    el4 = SubElement(el1, 'text:index-body')
+                    el5 = SubElement(el4, 'text:index-title')
+                    el6 = SubElement(el5, 'text:p', attrib={
+                        'text:style-name': self.rststyle('contents-heading'),
+                    })
+                    el6.text = label
+                    self.save_current_element = self.current_element
+                    self.table_of_content_index_body = el4
+                    self.set_current_element(el4)
+                else:
+                    el = self.append_p('horizontalline')
+                    el = self.append_p('centeredtextbody')
+                    el1 = SubElement(
+                        el, 'text:span',
+                        attrib={'text:style-name': self.rststyle('strong')})
+                    el1.text = label
+                self.in_table_of_contents = True
+            elif 'abstract' in node.attributes['classes']:
+                el = self.append_p('horizontalline')
+                el = self.append_p('centeredtextbody')
+                el1 = SubElement(
+                    el, 'text:span',
+                    attrib={'text:style-name': self.rststyle('strong')})
+                label = self.find_title_label(
+                    node, docutils.nodes.title,
+                    'abstract')
+                el1.text = label
+            elif 'dedication' in node.attributes['classes']:
+                el = self.append_p('horizontalline')
+                el = self.append_p('centeredtextbody')
+                el1 = SubElement(
+                    el, 'text:span',
+                    attrib={'text:style-name': self.rststyle('strong')})
+                label = self.find_title_label(
+                    node, docutils.nodes.title,
+                    'dedication')
+                el1.text = label
+
+    def depart_topic(self, node):
+        if 'classes' in node.attributes:
+            if 'contents' in node.attributes['classes']:
+                if self.settings.generate_oowriter_toc:
+                    self.update_toc_page_numbers(
+                        self.table_of_content_index_body)
+                    self.set_current_element(self.save_current_element)
+                else:
+                    self.append_p('horizontalline')
+                self.in_table_of_contents = False
+
+    def update_toc_page_numbers(self, el):
+        collection = []
+        self.update_toc_collect(el, 0, collection)
+        self.update_toc_add_numbers(collection)
+
+    def update_toc_collect(self, el, level, collection):
+        collection.append((level, el))
+        level += 1
+        for child_el in el:
+            if child_el.tag != 'text:index-body':
+                self.update_toc_collect(child_el, level, collection)
+
+    def update_toc_add_numbers(self, collection):
+        for level, el1 in collection:
+            if (el1.tag == 'text:p'
+                and el1.text != 'Table of Contents'):
+                el2 = SubElement(el1, 'text:tab')
+                el2.tail = '9999'
+
+    def visit_transition(self, node):
+        self.append_p('horizontalline')
+
+    def depart_transition(self, node):
+        pass
+
+    #
+    # Admonitions
+    #
+    def visit_warning(self, node):
+        self.generate_admonition(node, 'warning')
+
+    def depart_warning(self, node):
+        self.paragraph_style_stack.pop()
+
+    def visit_attention(self, node):
+        self.generate_admonition(node, 'attention')
+
+    depart_attention = depart_warning
+
+    def visit_caution(self, node):
+        self.generate_admonition(node, 'caution')
+
+    depart_caution = depart_warning
+
+    def visit_danger(self, node):
+        self.generate_admonition(node, 'danger')
+
+    depart_danger = depart_warning
+
+    def visit_error(self, node):
+        self.generate_admonition(node, 'error')
+
+    depart_error = depart_warning
+
+    def visit_hint(self, node):
+        self.generate_admonition(node, 'hint')
+
+    depart_hint = depart_warning
+
+    def visit_important(self, node):
+        self.generate_admonition(node, 'important')
+
+    depart_important = depart_warning
+
+    def visit_note(self, node):
+        self.generate_admonition(node, 'note')
+
+    depart_note = depart_warning
+
+    def visit_tip(self, node):
+        self.generate_admonition(node, 'tip')
+
+    depart_tip = depart_warning
+
+    def visit_admonition(self, node):
+        title = None
+        for child in node.children:
+            if child.tagname == 'title':
+                title = child.astext()
+        if title is None:
+            classes1 = node.get('classes')
+            if classes1:
+                title = classes1[0]
+        self.generate_admonition(node, 'generic', title)
+
+    depart_admonition = depart_warning
+
+    def generate_admonition(self, node, label, title=None):
+        if hasattr(self.language, 'labels'):
+            translated_label = self.language.labels.get(label, label)
+        else:
+            translated_label = label
+        el1 = SubElement(self.current_element, 'text:p', attrib={
+            'text:style-name': self.rststyle(
+                'admon-%s-hdr', (label, )),
+        })
+        if title:
+            el1.text = title
+        else:
+            el1.text = '%s!' % (translated_label.capitalize(), )
+        s1 = self.rststyle('admon-%s-body', (label, ))
+        self.paragraph_style_stack.append(s1)
+
+    #
+    # Roles (e.g. subscript, superscript, strong, ...
+    #
+    def visit_subscript(self, node):
+        el = self.append_child('text:span', attrib={
+            'text:style-name': 'rststyle-subscript',
+        })
+        self.set_current_element(el)
+
+    def depart_subscript(self, node):
+        self.set_to_parent()
+
+    def visit_superscript(self, node):
+        el = self.append_child('text:span', attrib={
+            'text:style-name': 'rststyle-superscript',
+        })
+        self.set_current_element(el)
+
+    def depart_superscript(self, node):
+        self.set_to_parent()
+
+    def visit_abbreviation(self, node):
+        pass
+
+    def depart_abbreviation(self, node):
+        pass
+
+    def visit_acronym(self, node):
+        pass
+
+    def depart_acronym(self, node):
+        pass
+
+    def visit_sidebar(self, node):
+        pass
+
+    def depart_sidebar(self, node):
+        pass
+
+
+# Use an own reader to modify transformations done.
+class Reader(standalone.Reader):
+
+    def get_transforms(self):
+        transforms = super().get_transforms()
+        if not self.settings.create_links:
+            transforms.remove(references.DanglingReferences)
+        return transforms
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py
new file mode 100755
index 00000000..b59490f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+
+# $Id: prepstyles.py 9386 2023-05-16 14:49:31Z milde $
+# Author: Dave Kuhlman <dkuhlman@rexx.com>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Adapt a word-processor-generated styles.odt for odtwriter use:
+
+Drop page size specifications from styles.xml in STYLE_FILE.odt.
+See https://docutils.sourceforge.io/docs/user/odt.html#page-size
+"""
+
+# Author: Michael Schutte <michi@uiae.at>
+
+from xml.etree import ElementTree as etree
+
+import sys
+import zipfile
+from tempfile import mkstemp
+import shutil
+import os
+
+NAMESPACES = {
+    "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+    "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
+}
+
+
+def prepstyle(filename):
+
+    zin = zipfile.ZipFile(filename)
+    styles = zin.open("styles.xml")
+
+    root = None
+    # some extra effort to preserve namespace prefixes
+    for event, elem in etree.iterparse(styles, events=("start", "start-ns")):
+        if event == "start-ns":
+            etree.register_namespace(elem[0], elem[1])
+        elif event == "start":
+            if root is None:
+                root = elem
+
+    styles.close()
+
+    for el in root.findall(".//style:page-layout-properties",
+                           namespaces=NAMESPACES):
+        for attr in list(el.attrib):
+            if attr.startswith("{%s}" % NAMESPACES["fo"]):
+                del el.attrib[attr]
+
+    tempname = mkstemp()
+    zout = zipfile.ZipFile(os.fdopen(tempname[0], "wb"), "w",
+                           zipfile.ZIP_DEFLATED)
+
+    for item in zin.infolist():
+        if item.filename == "styles.xml":
+            zout.writestr(item, etree.tostring(root, encoding="UTF-8"))
+        else:
+            zout.writestr(item, zin.read(item.filename))
+
+    zout.close()
+    zin.close()
+    shutil.move(tempname[1], filename)
+
+
+def main():
+    args = sys.argv[1:]
+    if len(args) != 1 or args[0] in ('-h', '--help'):
+        print(__doc__, file=sys.stderr)
+        print("Usage: %s STYLE_FILE.odt\n" % sys.argv[0], file=sys.stderr)
+        sys.exit(1)
+    filename = args[0]
+    prepstyle(filename)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py
new file mode 100644
index 00000000..7880651b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py
@@ -0,0 +1,109 @@
+# $Id: pygmentsformatter.py 9015 2022-03-03 22:15:00Z milde $
+# Author: Dave Kuhlman <dkuhlman@rexx.com>
+# Copyright: This module has been placed in the public domain.
+
+"""
+
+Additional support for Pygments formatter.
+
+"""
+
+
+import pygments
+import pygments.formatter
+
+
+class OdtPygmentsFormatter(pygments.formatter.Formatter):
+    def __init__(self, rststyle_function, escape_function):
+        pygments.formatter.Formatter.__init__(self)
+        self.rststyle_function = rststyle_function
+        self.escape_function = escape_function
+
+    def rststyle(self, name, parameters=()):
+        return self.rststyle_function(name, parameters)
+
+
+class OdtPygmentsProgFormatter(OdtPygmentsFormatter):
+    def format(self, tokensource, outfile):
+        tokenclass = pygments.token.Token
+        for ttype, value in tokensource:
+            value = self.escape_function(value)
+            if ttype == tokenclass.Keyword:
+                s2 = self.rststyle('codeblock-keyword')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Literal.String:
+                s2 = self.rststyle('codeblock-string')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype in (
+                    tokenclass.Literal.Number.Integer,
+                    tokenclass.Literal.Number.Integer.Long,
+                    tokenclass.Literal.Number.Float,
+                    tokenclass.Literal.Number.Hex,
+                    tokenclass.Literal.Number.Oct,
+                    tokenclass.Literal.Number,
+                    ):
+                s2 = self.rststyle('codeblock-number')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Operator:
+                s2 = self.rststyle('codeblock-operator')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Comment:
+                s2 = self.rststyle('codeblock-comment')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Name.Class:
+                s2 = self.rststyle('codeblock-classname')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Name.Function:
+                s2 = self.rststyle('codeblock-functionname')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Name:
+                s2 = self.rststyle('codeblock-name')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            else:
+                s1 = value
+            outfile.write(s1)
+
+
+class OdtPygmentsLaTeXFormatter(OdtPygmentsFormatter):
+    def format(self, tokensource, outfile):
+        tokenclass = pygments.token.Token
+        for ttype, value in tokensource:
+            value = self.escape_function(value)
+            if ttype == tokenclass.Keyword:
+                s2 = self.rststyle('codeblock-keyword')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype in (tokenclass.Literal.String,
+                           tokenclass.Literal.String.Backtick,
+                           ):
+                s2 = self.rststyle('codeblock-string')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Name.Attribute:
+                s2 = self.rststyle('codeblock-operator')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            elif ttype == tokenclass.Comment:
+                if value[-1] == '\n':
+                    s2 = self.rststyle('codeblock-comment')
+                    s1 = '<text:span text:style-name="%s">%s</text:span>\n' % \
+                        (s2, value[:-1], )
+                else:
+                    s2 = self.rststyle('codeblock-comment')
+                    s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                        (s2, value, )
+            elif ttype == tokenclass.Name.Builtin:
+                s2 = self.rststyle('codeblock-name')
+                s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+                    (s2, value, )
+            else:
+                s1 = value
+            outfile.write(s1)
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odt b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odt
new file mode 100644
index 00000000..e17b0072
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odt
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py
new file mode 100644
index 00000000..dfde2e47
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py
@@ -0,0 +1,101 @@
+# $Id: __init__.py 9541 2024-02-17 10:37:13Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+PEP HTML Writer.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import os
+import os.path
+
+from docutils import frontend, nodes, utils
+from docutils.writers import html4css1
+
+
+class Writer(html4css1.Writer):
+
+    default_stylesheet = 'pep.css'
+
+    default_stylesheet_path = utils.relative_path(
+        os.path.join(os.getcwd(), 'dummy'),
+        os.path.join(os.path.dirname(__file__), default_stylesheet))
+
+    default_template = 'template.txt'
+
+    default_template_path = utils.relative_path(
+        os.path.join(os.getcwd(), 'dummy'),
+        os.path.join(os.path.dirname(__file__), default_template))
+
+    settings_spec = html4css1.Writer.settings_spec + (
+        'PEP/HTML Writer Options',
+        'For the PEP/HTML writer, the default value for the --stylesheet-path '
+        'option is "%s", and the default value for --template is "%s". '
+        'See HTML Writer Options above.'
+        % (default_stylesheet_path, default_template_path),
+        (('Python\'s home URL.  Default is "https://www.python.org".',
+          ['--python-home'],
+          {'default': 'https://www.python.org', 'metavar': '<URL>'}),
+         ('Home URL prefix for PEPs.  Default is "." (current directory).',
+          ['--pep-home'],
+          {'default': '.', 'metavar': '<URL>'}),
+         # For testing.
+         (frontend.SUPPRESS_HELP,
+          ['--no-random'],
+          {'action': 'store_true', 'validator': frontend.validate_boolean}),))
+
+    settings_default_overrides = {'stylesheet_path': default_stylesheet_path,
+                                  'template': default_template_path}
+    relative_path_settings = ('template',)
+    config_section = 'pep_html writer'
+    config_section_dependencies = ('writers', 'html writers',
+                                   'html4css1 writer')
+
+    def __init__(self):
+        html4css1.Writer.__init__(self)
+        self.translator_class = HTMLTranslator
+
+    def interpolation_dict(self):
+        subs = html4css1.Writer.interpolation_dict(self)
+        settings = self.document.settings
+        pyhome = settings.python_home
+        subs['pyhome'] = pyhome
+        subs['pephome'] = settings.pep_home
+        if pyhome == '..':
+            subs['pepindex'] = '.'
+        else:
+            subs['pepindex'] = pyhome + '/dev/peps'
+        index = self.document.first_child_matching_class(nodes.field_list)
+        header = self.document[index]
+        self.pepnum = header[0][1].astext()
+        subs['pep'] = self.pepnum
+        if settings.no_random:
+            subs['banner'] = 0
+        else:
+            import random
+            subs['banner'] = random.randrange(64)
+        try:
+            subs['pepnum'] = '%04i' % int(self.pepnum)
+        except ValueError:
+            subs['pepnum'] = self.pepnum
+        self.title = header[1][1].astext()
+        subs['title'] = self.title
+        subs['body'] = ''.join(
+            self.body_pre_docinfo + self.docinfo + self.body)
+        return subs
+
+    def assemble_parts(self):
+        html4css1.Writer.assemble_parts(self)
+        self.parts['title'] = [self.title]
+        self.parts['pepnum'] = self.pepnum
+
+
+class HTMLTranslator(html4css1.HTMLTranslator):
+
+    def depart_field_list(self, node):
+        html4css1.HTMLTranslator.depart_field_list(self, node)
+        if 'rfc2822' in node['classes']:
+            self.body.append('<hr />\n')
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css
new file mode 100644
index 00000000..5231cd1a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css
@@ -0,0 +1,344 @@
+/*
+:Author: David Goodger
+:Contact: goodger@python.org
+:date: $Date: 2022-01-29 23:26:10 +0100 (Sa, 29. Jän 2022) $
+:version: $Revision: 8995 $
+:copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the PEP HTML output of Docutils.
+*/
+
+/* "! important" is used here to override other ``margin-top`` and
+   ``margin-bottom`` styles that are later in the stylesheet or
+   more specific.  See http://www.w3.org/TR/CSS1#the-cascade */
+.first {
+  margin-top: 0 ! important }
+
+.last, .with-subtitle {
+  margin-bottom: 0 ! important }
+
+.hidden {
+  display: none }
+
+.navigation {
+  width: 100% ;
+  background: #99ccff ;
+  margin-top: 0px ;
+  margin-bottom: 0px }
+
+.navigation .navicon {
+  width: 150px ;
+  height: 35px }
+
+.navigation .textlinks {
+  padding-left: 1em ;
+  text-align: left }
+
+.navigation td, .navigation th {
+  padding-left: 0em ;
+  padding-right: 0em ;
+  vertical-align: middle }
+
+.rfc2822 {
+  margin-top: 0.5em ;
+  margin-left: 0.5em ;
+  margin-right: 0.5em ;
+  margin-bottom: 0em }
+
+.rfc2822 td {
+  text-align: left }
+
+.rfc2822 th.field-name {
+  text-align: right ;
+  font-family: sans-serif ;
+  padding-right: 0.5em ;
+  font-weight: bold ;
+  margin-bottom: 0em }
+
+a.toc-backref {
+  text-decoration: none ;
+  color: black }
+
+blockquote.epigraph {
+  margin: 2em 5em ; }
+
+body {
+  margin: 0px ;
+  margin-bottom: 1em ;
+  padding: 0px }
+
+dl.docutils dd {
+  margin-bottom: 0.5em }
+
+div.section {
+  margin-left: 1em ;
+  margin-right: 1em ;
+  margin-bottom: 1.5em }
+
+div.section div.section {
+  margin-left: 0em ;
+  margin-right: 0em ;
+  margin-top: 1.5em }
+
+div.abstract {
+  margin: 2em 5em }
+
+div.abstract p.topic-title {
+  font-weight: bold ;
+  text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+  margin: 2em ;
+  border: medium outset ;
+  padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+  font-weight: bold ;
+  font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title {
+  color: red ;
+  font-weight: bold ;
+  font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+   compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+  margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+  margin-top: 0.5em }
+*/
+
+div.dedication {
+  margin: 2em 5em ;
+  text-align: center ;
+  font-style: italic }
+
+div.dedication p.topic-title {
+  font-weight: bold ;
+  font-style: normal }
+
+div.figure {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+div.footer, div.header {
+  clear: both;
+  font-size: smaller }
+
+div.footer {
+  margin-left: 1em ;
+  margin-right: 1em }
+
+div.line-block {
+  display: block ;
+  margin-top: 1em ;
+  margin-bottom: 1em }
+
+div.line-block div.line-block {
+  margin-top: 0 ;
+  margin-bottom: 0 ;
+  margin-left: 1.5em }
+
+div.sidebar {
+  margin-left: 1em ;
+  border: medium outset ;
+  padding: 1em ;
+  background-color: #ffffee ;
+  width: 40% ;
+  float: right ;
+  clear: right }
+
+div.sidebar p.rubric {
+  font-family: sans-serif ;
+  font-size: medium }
+
+div.system-messages {
+  margin: 5em }
+
+div.system-messages h1 {
+  color: red }
+
+div.system-message {
+  border: medium outset ;
+  padding: 1em }
+
+div.system-message p.system-message-title {
+  color: red ;
+  font-weight: bold }
+
+div.topic {
+  margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+  margin-top: 0.4em }
+
+h1 {
+  font-family: sans-serif ;
+  font-size: large }
+
+h2 {
+  font-family: sans-serif ;
+  font-size: medium }
+
+h3 {
+  font-family: sans-serif ;
+  font-size: small }
+
+h4 {
+  font-family: sans-serif ;
+  font-style: italic ;
+  font-size: small }
+
+h5 {
+  font-family: sans-serif;
+  font-size: x-small }
+
+h6 {
+  font-family: sans-serif;
+  font-style: italic ;
+  font-size: x-small }
+
+hr.docutils {
+  width: 75% }
+
+img.align-left {
+  clear: left }
+
+img.align-right {
+  clear: right }
+
+img.borderless {
+  border: 0 }
+
+ol.simple, ul.simple {
+  margin-bottom: 1em }
+
+ol.arabic {
+  list-style: decimal }
+
+ol.loweralpha {
+  list-style: lower-alpha }
+
+ol.upperalpha {
+  list-style: upper-alpha }
+
+ol.lowerroman {
+  list-style: lower-roman }
+
+ol.upperroman {
+  list-style: upper-roman }
+
+p.attribution {
+  text-align: right ;
+  margin-left: 50% }
+
+p.caption {
+  font-style: italic }
+
+p.credits {
+  font-style: italic ;
+  font-size: smaller }
+
+p.label {
+  white-space: nowrap }
+
+p.rubric {
+  font-weight: bold ;
+  font-size: larger ;
+  color: maroon ;
+  text-align: center }
+
+p.sidebar-title {
+  font-family: sans-serif ;
+  font-weight: bold ;
+  font-size: larger }
+
+p.sidebar-subtitle {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+p.topic-title {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+pre.address {
+  margin-bottom: 0 ;
+  margin-top: 0 ;
+  font-family: serif ;
+  font-size: 100% }
+
+pre.literal-block, pre.doctest-block {
+  margin-left: 2em ;
+  margin-right: 2em }
+
+span.classifier {
+  font-family: sans-serif ;
+  font-style: oblique }
+
+span.classifier-delimiter {
+  font-family: sans-serif ;
+  font-weight: bold }
+
+span.interpreted {
+  font-family: sans-serif }
+
+span.option {
+  white-space: nowrap }
+
+span.option-argument {
+  font-style: italic }
+
+span.pre {
+  white-space: pre }
+
+span.problematic {
+  color: red }
+
+span.section-subtitle {
+  /* font-size relative to parent (h1..h6 element) */
+  font-size: 80% }
+
+table.citation {
+  border-left: solid 1px gray;
+  margin-left: 1px }
+
+table.docinfo {
+  margin: 2em 4em }
+
+table.docutils {
+  margin-top: 0.5em ;
+  margin-bottom: 0.5em }
+
+table.footnote {
+  border-left: solid 1px black;
+  margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+  padding-left: 0.5em ;
+  padding-right: 0.5em ;
+  vertical-align: top }
+
+td.num {
+  text-align: right }
+
+th.field-name {
+  font-weight: bold ;
+  text-align: left ;
+  white-space: nowrap ;
+  padding-left: 0 }
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+  font-size: 100% }
+
+ul.auto-toc {
+  list-style-type: none }
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt
new file mode 100644
index 00000000..e8cd351c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="%(encoding)s"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<!--
+This HTML is auto-generated.  DO NOT EDIT THIS FILE!  If you are writing a new
+PEP, see http://peps.python.org/pep-0001 for instructions and links
+to templates.  DO NOT USE THIS HTML FILE AS YOUR TEMPLATE!
+-->
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
+  <meta name="generator" content="Docutils %(version)s: https://docutils.sourceforge.io/" />
+  <title>PEP %(pep)s - %(title)s</title>
+  %(stylesheet)s
+</head>
+<body bgcolor="white">
+<div class="header">
+<strong>Python Enhancement Proposals</strong>
+| <a href="%(pyhome)s/">Python</a>
+&raquo; <a href="https://peps.python.org/pep-0000/">PEP Index</a>
+&raquo; PEP %(pep)s &ndash; %(title)s
+<hr class="header"/>
+</div>
+<div class="document">
+%(body)s
+%(body_suffix)s
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py b/.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py
new file mode 100644
index 00000000..0e238a88
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py
@@ -0,0 +1,40 @@
+# $Id: pseudoxml.py 9043 2022-03-11 12:09:16Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Simple internal document tree Writer, writes indented pseudo-XML.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils import writers, frontend
+
+
+class Writer(writers.Writer):
+
+    supported = ('pseudoxml', 'pprint', 'pformat')
+    """Formats this writer supports."""
+
+    settings_spec = (
+        '"Docutils pseudo-XML" Writer Options',
+        None,
+        (('Pretty-print <#text> nodes.',
+          ['--detailed'],
+          {'action': 'store_true', 'validator': frontend.validate_boolean}),
+         )
+        )
+
+    config_section = 'pseudoxml writer'
+    config_section_dependencies = ('writers',)
+
+    output = None
+    """Final translated form of `document`."""
+
+    def translate(self):
+        self.output = self.document.pformat()
+
+    def supports(self, format):
+        """This writer supports all format-specific elements."""
+        return True
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py
new file mode 100644
index 00000000..7014de33
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py
@@ -0,0 +1,353 @@
+# $Id: __init__.py 9542 2024-02-17 10:37:23Z milde $
+# Authors: Chris Liechti <cliechti@gmx.net>;
+#          David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+S5/HTML Slideshow Writer.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import sys
+import os
+import re
+import docutils
+from docutils import frontend, nodes, utils
+from docutils.writers import html4css1
+
+themes_dir_path = utils.relative_path(
+    os.path.join(os.getcwd(), 'dummy'),
+    os.path.join(os.path.dirname(__file__), 'themes'))
+
+
+def find_theme(name):
+    # Where else to look for a theme?
+    # Check working dir?  Destination dir?  Config dir?  Plugins dir?
+    path = os.path.join(themes_dir_path, name)
+    if not os.path.isdir(path):
+        raise docutils.ApplicationError(
+            'Theme directory not found: %r (path: %r)' % (name, path))
+    return path
+
+
+class Writer(html4css1.Writer):
+
+    settings_spec = html4css1.Writer.settings_spec + (
+        'S5 Slideshow Specific Options',
+        'For the S5/HTML writer, the --no-toc-backlinks option '
+        '(defined in General Docutils Options above) is the default, '
+        'and should not be changed.',
+        (('Specify an installed S5 theme by name.  Overrides --theme-url.  '
+          'The default theme name is "default".  The theme files will be '
+          'copied into a "ui/<theme>" directory, in the same directory as the '
+          'destination file (output HTML).  Note that existing theme files '
+          'will not be overwritten (unless --overwrite-theme-files is used).',
+          ['--theme'],
+          {'default': 'default', 'metavar': '<name>',
+           'overrides': 'theme_url'}),
+         ('Specify an S5 theme URL.  The destination file (output HTML) will '
+          'link to this theme; nothing will be copied.  Overrides --theme.',
+          ['--theme-url'],
+          {'metavar': '<URL>', 'overrides': 'theme'}),
+         ('Allow existing theme files in the ``ui/<theme>`` directory to be '
+          'overwritten.  The default is not to overwrite theme files.',
+          ['--overwrite-theme-files'],
+          {'action': 'store_true', 'validator': frontend.validate_boolean}),
+         ('Keep existing theme files in the ``ui/<theme>`` directory; do not '
+          'overwrite any.  This is the default.',
+          ['--keep-theme-files'],
+          {'dest': 'overwrite_theme_files', 'action': 'store_false'}),
+         ('Set the initial view mode to "slideshow" [default] or "outline".',
+          ['--view-mode'],
+          {'choices': ['slideshow', 'outline'], 'default': 'slideshow',
+           'metavar': '<mode>'}),
+         ('Normally hide the presentation controls in slideshow mode. '
+          'This is the default.',
+          ['--hidden-controls'],
+          {'action': 'store_true', 'default': True,
+           'validator': frontend.validate_boolean}),
+         ('Always show the presentation controls in slideshow mode.  '
+          'The default is to hide the controls.',
+          ['--visible-controls'],
+          {'dest': 'hidden_controls', 'action': 'store_false'}),
+         ('Enable the current slide indicator ("1 / 15").  '
+          'The default is to disable it.',
+          ['--current-slide'],
+          {'action': 'store_true', 'validator': frontend.validate_boolean}),
+         ('Disable the current slide indicator.  This is the default.',
+          ['--no-current-slide'],
+          {'dest': 'current_slide', 'action': 'store_false'}),))
+
+    settings_default_overrides = {'toc_backlinks': 0}
+
+    config_section = 's5_html writer'
+    config_section_dependencies = ('writers', 'html writers',
+                                   'html4css1 writer')
+
+    def __init__(self):
+        html4css1.Writer.__init__(self)
+        self.translator_class = S5HTMLTranslator
+
+
+class S5HTMLTranslator(html4css1.HTMLTranslator):
+
+    s5_stylesheet_template = """\
+<!-- configuration parameters -->
+<meta name="defaultView" content="%(view_mode)s" />
+<meta name="controlVis" content="%(control_visibility)s" />
+<!-- style sheet links -->
+<script src="%(path)s/slides.js" type="text/javascript"></script>
+<link rel="stylesheet" href="%(path)s/slides.css"
+      type="text/css" media="projection" id="slideProj" />
+<link rel="stylesheet" href="%(path)s/outline.css"
+      type="text/css" media="screen" id="outlineStyle" />
+<link rel="stylesheet" href="%(path)s/print.css"
+      type="text/css" media="print" id="slidePrint" />
+<link rel="stylesheet" href="%(path)s/opera.css"
+      type="text/css" media="projection" id="operaFix" />\n"""
+    # The script element must go in front of the link elements to
+    # avoid a flash of unstyled content (FOUC), reproducible with
+    # Firefox.
+
+    disable_current_slide = """
+<style type="text/css">
+#currentSlide {display: none;}
+</style>\n"""
+
+    layout_template = """\
+<div class="layout">
+<div id="controls"></div>
+<div id="currentSlide"></div>
+<div id="header">
+%(header)s
+</div>
+<div id="footer">
+%(title)s%(footer)s
+</div>
+</div>\n"""
+# <div class="topleft"></div>
+# <div class="topright"></div>
+# <div class="bottomleft"></div>
+# <div class="bottomright"></div>
+
+    default_theme = 'default'
+    """Name of the default theme."""
+
+    base_theme_file = '__base__'
+    """Name of the file containing the name of the base theme."""
+
+    direct_theme_files = (
+        'slides.css', 'outline.css', 'print.css', 'opera.css', 'slides.js')
+    """Names of theme files directly linked to in the output HTML"""
+
+    indirect_theme_files = (
+        's5-core.css', 'framing.css', 'pretty.css')
+    """Names of files used indirectly; imported or used by files in
+    `direct_theme_files`."""
+
+    required_theme_files = indirect_theme_files + direct_theme_files
+    """Names of mandatory theme files."""
+
+    def __init__(self, *args):
+        html4css1.HTMLTranslator.__init__(self, *args)
+        # insert S5-specific stylesheet and script stuff:
+        self.theme_file_path = None
+        try:
+            self.setup_theme()
+        except docutils.ApplicationError as e:
+            self.document.reporter.warning(e)
+        view_mode = self.document.settings.view_mode
+        control_visibility = ('visible', 'hidden')[self.document.settings
+                                                   .hidden_controls]
+        self.stylesheet.append(self.s5_stylesheet_template
+                               % {'path': self.theme_file_path,
+                                  'view_mode': view_mode,
+                                  'control_visibility': control_visibility})
+        if not self.document.settings.current_slide:
+            self.stylesheet.append(self.disable_current_slide)
+        self.meta.append('<meta name="version" content="S5 1.1" />\n')
+        self.s5_footer = []
+        self.s5_header = []
+        self.section_count = 0
+        self.theme_files_copied = None
+
+    def setup_theme(self):
+        if self.document.settings.theme:
+            self.copy_theme()
+        elif self.document.settings.theme_url:
+            self.theme_file_path = self.document.settings.theme_url
+        else:
+            raise docutils.ApplicationError(
+                'No theme specified for S5/HTML writer.')
+
+    def copy_theme(self):
+        """
+        Locate & copy theme files.
+
+        A theme may be explicitly based on another theme via a '__base__'
+        file.  The default base theme is 'default'.  Files are accumulated
+        from the specified theme, any base themes, and 'default'.
+        """
+        settings = self.document.settings
+        path = find_theme(settings.theme)
+        theme_paths = [path]
+        self.theme_files_copied = {}
+        required_files_copied = {}
+        # This is a link (URL) in HTML, so we use "/", not os.sep:
+        self.theme_file_path = 'ui/%s' % settings.theme
+        if not settings.output:
+            raise docutils.ApplicationError(
+                'Output path not specified, you may need to copy'
+                ' the S5 theme files "by hand" or set the "--output" option.')
+        dest = os.path.join(
+            os.path.dirname(settings.output), 'ui', settings.theme)
+        if not os.path.isdir(dest):
+            os.makedirs(dest)
+        default = False
+        while path:
+            for f in os.listdir(path):  # copy all files from each theme
+                if f == self.base_theme_file:
+                    continue            # ... except the "__base__" file
+                if (self.copy_file(f, path, dest)
+                    and f in self.required_theme_files):
+                    required_files_copied[f] = True
+            if default:
+                break                   # "default" theme has no base theme
+            # Find the "__base__" file in theme directory:
+            base_theme_file = os.path.join(path, self.base_theme_file)
+            # If it exists, read it and record the theme path:
+            if os.path.isfile(base_theme_file):
+                with open(base_theme_file, encoding='utf-8') as f:
+                    lines = f.readlines()
+                for line in lines:
+                    line = line.strip()
+                    if line and not line.startswith('#'):
+                        path = find_theme(line)
+                        if path in theme_paths:  # check for duplicates/cycles
+                            path = None          # if found, use default base
+                        else:
+                            theme_paths.append(path)
+                        break
+                else:                   # no theme name found
+                    path = None         # use default base
+            else:                       # no base theme file found
+                path = None             # use default base
+            if not path:
+                path = find_theme(self.default_theme)
+                theme_paths.append(path)
+                default = True
+        if len(required_files_copied) != len(self.required_theme_files):
+            # Some required files weren't found & couldn't be copied.
+            required = list(self.required_theme_files)
+            for f in required_files_copied.keys():
+                required.remove(f)
+            raise docutils.ApplicationError(
+                'Theme files not found: %s'
+                % ', '.join('%r' % f for f in required))
+
+    files_to_skip_pattern = re.compile(r'~$|\.bak$|#$|\.cvsignore$')
+
+    def copy_file(self, name, source_dir, dest_dir):
+        """
+        Copy file `name` from `source_dir` to `dest_dir`.
+        Return True if the file exists in either `source_dir` or `dest_dir`.
+        """
+        source = os.path.join(source_dir, name)
+        dest = os.path.join(dest_dir, name)
+        if dest in self.theme_files_copied:
+            return True
+        else:
+            self.theme_files_copied[dest] = True
+        if os.path.isfile(source):
+            if self.files_to_skip_pattern.search(source):
+                return None
+            settings = self.document.settings
+            if os.path.exists(dest) and not settings.overwrite_theme_files:
+                settings.record_dependencies.add(dest)
+            else:
+                with open(source, 'rb') as src_file:
+                    src_data = src_file.read()
+                with open(dest, 'wb') as dest_file:
+                    dest_dir = dest_dir.replace(os.sep, '/')
+                    dest_file.write(src_data.replace(
+                        b'ui/default',
+                        dest_dir[dest_dir.rfind('ui/'):].encode(
+                            sys.getfilesystemencoding())))
+                settings.record_dependencies.add(source)
+            return True
+        if os.path.isfile(dest):
+            return True
+
+    def depart_document(self, node):
+        self.head_prefix.extend([self.doctype,
+                                 self.head_prefix_template %
+                                 {'lang': self.settings.language_code}])
+        self.html_prolog.append(self.doctype)
+        self.head = self.meta[:] + self.head
+        if self.math_header:
+            if self.math_output == 'mathjax':
+                self.head.extend(self.math_header)
+            else:
+                self.stylesheet.extend(self.math_header)
+        # skip content-type meta tag with interpolated charset value:
+        self.html_head.extend(self.head[1:])
+        self.fragment.extend(self.body)
+        # special S5 code up to the next comment line
+        header = ''.join(self.s5_header)
+        footer = ''.join(self.s5_footer)
+        title = ''.join(self.html_title).replace('<h1 class="title">', '<h1>')
+        layout = self.layout_template % {'header': header,
+                                         'title': title,
+                                         'footer': footer}
+        self.body_prefix.extend(layout)
+        self.body_prefix.append('<div class="presentation">\n')
+        self.body_prefix.append(
+            self.starttag({'classes': ['slide'], 'ids': ['slide0']}, 'div'))
+        if not self.section_count:
+            self.body.append('</div>\n')
+        #
+        self.body_suffix.insert(0, '</div>\n')
+        self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
+                              + self.docinfo + self.body
+                              + self.body_suffix[:-1])
+
+    def depart_footer(self, node):
+        start = self.context.pop()
+        self.s5_footer.append('<h2>')
+        self.s5_footer.extend(self.body[start:])
+        self.s5_footer.append('</h2>')
+        del self.body[start:]
+
+    def depart_header(self, node):
+        start = self.context.pop()
+        header = ['<div id="header">\n']
+        header.extend(self.body[start:])
+        header.append('\n</div>\n')
+        del self.body[start:]
+        self.s5_header.extend(header)
+
+    def visit_section(self, node):
+        if not self.section_count:
+            self.body.append('\n</div>\n')
+        self.section_count += 1
+        self.section_level += 1
+        if self.section_level > 1:
+            # dummy for matching div's
+            self.body.append(self.starttag(node, 'div', CLASS='section'))
+        else:
+            self.body.append(self.starttag(node, 'div', CLASS='slide'))
+
+    def visit_subtitle(self, node):
+        if isinstance(node.parent, nodes.section):
+            level = self.section_level + self.initial_header_level - 1
+            if level == 1:
+                level = 2
+            tag = 'h%s' % level
+            self.body.append(self.starttag(node, tag, ''))
+            self.context.append('</%s>\n' % tag)
+        else:
+            html4css1.HTMLTranslator.visit_subtitle(self, node)
+
+    def visit_title(self, node):
+        html4css1.HTMLTranslator.visit_title(self, node)
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt
new file mode 100644
index 00000000..605d08f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt
@@ -0,0 +1,6 @@
+Except where otherwise noted, all files in this
+directory have been released into the Public Domain.
+
+These files are based on files from S5 1.1, released into the Public
+Domain by Eric Meyer.  For further details, please see
+http://www.meyerweb.com/eric/tools/s5/credits.html.
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__
new file mode 100644
index 00000000..f08be9ad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__
@@ -0,0 +1,2 @@
+# base theme of this theme:
+big-white
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css
new file mode 100644
index 00000000..a945abbc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css
@@ -0,0 +1,25 @@
+/* The following styles size, place, and layer the slide components.
+   Edit these if you want to change the overall slide layout.
+   The commented lines can be uncommented (and modified, if necessary)
+    to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#header {top: 0; z-index: 1;}
+div#footer {display:none;}
+.slide {top: 0; width: 92%; padding: 0.1em 4% 4%; z-index: 2;}
+/*  list-style: none;} */
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+  z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css
new file mode 100644
index 00000000..85f07cf0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css
@@ -0,0 +1,109 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: black; color: white;}
+:link, :visited {text-decoration: none; color: cyan;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #CCC;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+.slide {font-size: 3em; font-family: sans-serif; font-weight: bold;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font-size: 120%;}
+.slide h2 {font-size: 110%;}
+.slide h3 {font-size: 105%;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+  width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+  border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+  background: black; color: #CCC;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #AAA;
+  font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 0.3em; top: 0;
+  font-size: 150%; white-space: normal; background: transparent;}
+#slide0 h2 {font: 110%; font-style: italic; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #FCC;}
+
+.incremental, .incremental *, .incremental *:after {
+  color: black; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: lime;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-size: 150%;}
+.big {font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+div.sidebar {background-color: black;}
+
+pre.literal-block, pre.doctest-block {background-color: black;}
+
+tt.docutils {background-color: black;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css
new file mode 100644
index 00000000..45f123f3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+   Edit these if you want to change the overall slide layout.
+   The commented lines can be uncommented (and modified, if necessary)
+   to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#footer {display:none;}
+.slide {top: 0; width: 92%; padding: 0.25em 4% 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+  z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css
new file mode 100644
index 00000000..68fe863a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css
@@ -0,0 +1,107 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #88A !important;}
+#controls :focus {outline: 1px dotted #227;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+.slide {font-size: 3em; font-family: sans-serif; font-weight: bold;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font-size: 120%;}
+.slide h2 {font-size: 110%;}
+.slide h3 {font-size: 105%;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+  width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+  border: none; color: #005; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+  background: #DDD; color: #227;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #444;
+  font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 0.3em; top: 0;
+  font-size: 150%; white-space: normal; background: transparent;}
+#slide0 h2 {font: 110%; font-style: italic; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {
+  color: white; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-size: 150%;}
+.big {font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css
new file mode 100644
index 00000000..b19b1f04
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css
@@ -0,0 +1,25 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+   Edit these if you want to change the overall slide layout.
+   The commented lines can be uncommented (and modified, if necessary)
+   to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#header {position: fixed; top: 0; height: 3em; z-index: 1;}
+div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 5;}
+.slide {top: 0; width: 92%; padding: 2.5em 4% 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+  z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css
new file mode 100644
index 00000000..c9d1148b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css
@@ -0,0 +1,8 @@
+/* This file has been placed in the public domain. */
+/* DO NOT CHANGE THESE unless you really want to break Opera Show */
+.slide {
+	visibility: visible !important;
+	position: static !important;
+	page-break-before: always;
+}
+#slide0 {page-break-before: avoid;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css
new file mode 100644
index 00000000..fa767e22
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css
@@ -0,0 +1,16 @@
+/* This file has been placed in the public domain. */
+/* Don't change this unless you want the layout stuff to show up in the
+   outline view! */
+
+.layout div, #footer *, #controlForm * {display: none;}
+#footer, #controls, #controlForm, #navLinks, #toggle {
+  display: block; visibility: visible; margin: 0; padding: 0;}
+#toggle {float: right; padding: 0.5em;}
+html>body #toggle {position: fixed; top: 0; right: 0;}
+
+/* making the outline look pretty-ish */
+
+#slide0 h1, #slide0 h2, #slide0 h3, #slide0 h4 {border: none; margin: 0;}
+#toggle {border: 1px solid; border-width: 0 0 1px 1px; background: #FFF;}
+
+.outline {display: inline ! important;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css
new file mode 100644
index 00000000..7d48fff5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css
@@ -0,0 +1,120 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+/* Replace the background style above with the style below (and again for
+   div#header) for a graphic: */
+/* background: white url(bodybg.gif) -16px 0 no-repeat; */
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #88A !important;}
+#controls :focus {outline: 1px dotted #227;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#header, div#footer {background: #005; color: #AAB; font-family: sans-serif;}
+/* background: #005 url(bodybg.gif) -16px 0 no-repeat; */
+div#footer {font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.2em;}
+.slide h1 {position: absolute; top: 0.45em; z-index: 1;
+  margin: 0; padding-left: 0.7em; white-space: nowrap;
+  font: bold 150% sans-serif; color: #DDE; background: #005;}
+.slide h2 {font: bold 120%/1em sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+  width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+  background: #005; border: none; color: #779; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+  background: #DDD; color: #227;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #449;
+  font-family: sans-serif; font-weight: bold;}
+
+#slide0 {padding-top: 1.5em}
+#slide0 h1 {position: static; margin: 1em 0 0; padding: 0; color: #000;
+  font: bold 2em sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {visibility: visible;
+  color: white; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 1em 0 0.5em 2em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css
new file mode 100644
index 00000000..9d057cc8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following rule is necessary to have all slides appear in print!
+   DO NOT REMOVE IT! */
+.slide, ul {page-break-inside: avoid; visibility: visible !important;}
+h1 {page-break-after: avoid;}
+
+body {font-size: 12pt; background: white;}
+* {color: black;}
+
+#slide0 h1 {font-size: 200%; border: none; margin: 0.5em 0 0.25em;}
+#slide0 h3 {margin: 0; padding: 0;}
+#slide0 h4 {margin: 0 0 0.5em; padding: 0;}
+#slide0 {margin-bottom: 3em;}
+
+#header {display: none;}
+#footer h1 {margin: 0; border-bottom: 1px solid; color: gray;
+  font-style: italic;}
+#footer h2, #controls {display: none;}
+
+.print {display: inline ! important;}
+
+/* The following rule keeps the layout stuff out of print.
+   Remove at your own risk! */
+.layout, .layout * {display: none !important;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css
new file mode 100644
index 00000000..62e1b7b1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css
@@ -0,0 +1,11 @@
+/* This file has been placed in the public domain. */
+/* Do not edit or override these styles!
+   The system will likely break if you do. */
+
+div#header, div#footer, div#controls, .slide {position: absolute;}
+html>body div#header, html>body div#footer,
+  html>body div#controls, html>body .slide {position: fixed;}
+.handout {display: none;}
+.layout {display: block;}
+.slide, .hideme, .incremental {visibility: hidden;}
+#slide0 {visibility: visible;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css
new file mode 100644
index 00000000..82bdc0ee
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css
@@ -0,0 +1,10 @@
+/* This file has been placed in the public domain. */
+
+/* required to make the slide show run at all */
+@import url(s5-core.css);
+
+/* sets basic placement and size of slide components */
+@import url(framing.css);
+
+/* styles that make the slides look good */
+@import url(pretty.css);
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js
new file mode 100644
index 00000000..cd0e0e43
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js
@@ -0,0 +1,558 @@
+// S5 v1.1 slides.js -- released into the Public Domain
+// Modified for Docutils (https://docutils.sourceforge.io) by David Goodger
+//
+// Please see http://www.meyerweb.com/eric/tools/s5/credits.html for
+// information about all the wonderful and talented contributors to this code!
+
+var undef;
+var slideCSS = '';
+var snum = 0;
+var smax = 1;
+var slideIDs = new Array();
+var incpos = 0;
+var number = undef;
+var s5mode = true;
+var defaultView = 'slideshow';
+var controlVis = 'visible';
+
+var isIE = navigator.appName == 'Microsoft Internet Explorer' ? 1 : 0;
+var isOp = navigator.userAgent.indexOf('Opera') > -1 ? 1 : 0;
+var isGe = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('Safari') < 1 ? 1 : 0;
+
+function hasClass(object, className) {
+	if (!object.className) return false;
+	return (object.className.search('(^|\\s)' + className + '(\\s|$)') != -1);
+}
+
+function hasValue(object, value) {
+	if (!object) return false;
+	return (object.search('(^|\\s)' + value + '(\\s|$)') != -1);
+}
+
+function removeClass(object,className) {
+	if (!object) return;
+	object.className = object.className.replace(new RegExp('(^|\\s)'+className+'(\\s|$)'), RegExp.$1+RegExp.$2);
+}
+
+function addClass(object,className) {
+	if (!object || hasClass(object, className)) return;
+	if (object.className) {
+		object.className += ' '+className;
+	} else {
+		object.className = className;
+	}
+}
+
+function GetElementsWithClassName(elementName,className) {
+	var allElements = document.getElementsByTagName(elementName);
+	var elemColl = new Array();
+	for (var i = 0; i< allElements.length; i++) {
+		if (hasClass(allElements[i], className)) {
+			elemColl[elemColl.length] = allElements[i];
+		}
+	}
+	return elemColl;
+}
+
+function isParentOrSelf(element, id) {
+	if (element == null || element.nodeName=='BODY') return false;
+	else if (element.id == id) return true;
+	else return isParentOrSelf(element.parentNode, id);
+}
+
+function nodeValue(node) {
+	var result = "";
+	if (node.nodeType == 1) {
+		var children = node.childNodes;
+		for (var i = 0; i < children.length; ++i) {
+			result += nodeValue(children[i]);
+		}
+	}
+	else if (node.nodeType == 3) {
+		result = node.nodeValue;
+	}
+	return(result);
+}
+
+function slideLabel() {
+	var slideColl = GetElementsWithClassName('*','slide');
+	var list = document.getElementById('jumplist');
+	smax = slideColl.length;
+	for (var n = 0; n < smax; n++) {
+		var obj = slideColl[n];
+
+		var did = 'slide' + n.toString();
+		if (obj.getAttribute('id')) {
+			slideIDs[n] = obj.getAttribute('id');
+		}
+		else {
+			obj.setAttribute('id',did);
+			slideIDs[n] = did;
+		}
+		if (isOp) continue;
+
+		var otext = '';
+		var menu = obj.firstChild;
+		if (!menu) continue; // to cope with empty slides
+		while (menu && menu.nodeType == 3) {
+			menu = menu.nextSibling;
+		}
+	 	if (!menu) continue; // to cope with slides with only text nodes
+
+		var menunodes = menu.childNodes;
+		for (var o = 0; o < menunodes.length; o++) {
+			otext += nodeValue(menunodes[o]);
+		}
+		list.options[list.length] = new Option(n + ' : '  + otext, n);
+	}
+}
+
+function currentSlide() {
+	var cs;
+	var footer_nodes;
+	var vis = 'visible';
+	if (document.getElementById) {
+		cs = document.getElementById('currentSlide');
+		footer_nodes = document.getElementById('footer').childNodes;
+	} else {
+		cs = document.currentSlide;
+		footer = document.footer.childNodes;
+	}
+	cs.innerHTML = '<span id="csHere">' + snum + '<\/span> ' +
+		'<span id="csSep">\/<\/span> ' +
+		'<span id="csTotal">' + (smax-1) + '<\/span>';
+	if (snum == 0) {
+		vis = 'hidden';
+	}
+	cs.style.visibility = vis;
+	for (var i = 0; i < footer_nodes.length; i++) {
+		if (footer_nodes[i].nodeType == 1) {
+			footer_nodes[i].style.visibility = vis;
+		}
+	}
+}
+
+function go(step) {
+	if (document.getElementById('slideProj').disabled || step == 0) return;
+	var jl = document.getElementById('jumplist');
+	var cid = slideIDs[snum];
+	var ce = document.getElementById(cid);
+	if (incrementals[snum].length > 0) {
+		for (var i = 0; i < incrementals[snum].length; i++) {
+			removeClass(incrementals[snum][i], 'current');
+			removeClass(incrementals[snum][i], 'incremental');
+		}
+	}
+	if (step != 'j') {
+		snum += step;
+		lmax = smax - 1;
+		if (snum > lmax) snum = lmax;
+		if (snum < 0) snum = 0;
+	} else
+		snum = parseInt(jl.value);
+	var nid = slideIDs[snum];
+	var ne = document.getElementById(nid);
+	if (!ne) {
+		ne = document.getElementById(slideIDs[0]);
+		snum = 0;
+	}
+	if (step < 0) {incpos = incrementals[snum].length} else {incpos = 0;}
+	if (incrementals[snum].length > 0 && incpos == 0) {
+		for (var i = 0; i < incrementals[snum].length; i++) {
+			if (hasClass(incrementals[snum][i], 'current'))
+				incpos = i + 1;
+			else
+				addClass(incrementals[snum][i], 'incremental');
+		}
+	}
+	if (incrementals[snum].length > 0 && incpos > 0)
+		addClass(incrementals[snum][incpos - 1], 'current');
+	ce.style.visibility = 'hidden';
+	ne.style.visibility = 'visible';
+	jl.selectedIndex = snum;
+	currentSlide();
+	number = 0;
+}
+
+function goTo(target) {
+	if (target >= smax || target == snum) return;
+	go(target - snum);
+}
+
+function subgo(step) {
+	if (step > 0) {
+		removeClass(incrementals[snum][incpos - 1],'current');
+		removeClass(incrementals[snum][incpos], 'incremental');
+		addClass(incrementals[snum][incpos],'current');
+		incpos++;
+	} else {
+		incpos--;
+		removeClass(incrementals[snum][incpos],'current');
+		addClass(incrementals[snum][incpos], 'incremental');
+		addClass(incrementals[snum][incpos - 1],'current');
+	}
+}
+
+function toggle() {
+	var slideColl = GetElementsWithClassName('*','slide');
+	var slides = document.getElementById('slideProj');
+	var outline = document.getElementById('outlineStyle');
+	if (!slides.disabled) {
+		slides.disabled = true;
+		outline.disabled = false;
+		s5mode = false;
+		fontSize('1em');
+		for (var n = 0; n < smax; n++) {
+			var slide = slideColl[n];
+			slide.style.visibility = 'visible';
+		}
+	} else {
+		slides.disabled = false;
+		outline.disabled = true;
+		s5mode = true;
+		fontScale();
+		for (var n = 0; n < smax; n++) {
+			var slide = slideColl[n];
+			slide.style.visibility = 'hidden';
+		}
+		slideColl[snum].style.visibility = 'visible';
+	}
+}
+
+function showHide(action) {
+	var obj = GetElementsWithClassName('*','hideme')[0];
+	switch (action) {
+	case 's': obj.style.visibility = 'visible'; break;
+	case 'h': obj.style.visibility = 'hidden'; break;
+	case 'k':
+		if (obj.style.visibility != 'visible') {
+			obj.style.visibility = 'visible';
+		} else {
+			obj.style.visibility = 'hidden';
+		}
+	break;
+	}
+}
+
+// 'keys' code adapted from MozPoint (http://mozpoint.mozdev.org/)
+function keys(key) {
+	if (!key) {
+		key = event;
+		key.which = key.keyCode;
+	}
+	if (key.which == 84) {
+		toggle();
+		return;
+	}
+	if (s5mode) {
+		switch (key.which) {
+			case 10: // return
+			case 13: // enter
+				if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return;
+				if (key.target && isParentOrSelf(key.target, 'controls')) return;
+				if(number != undef) {
+					goTo(number);
+					break;
+				}
+			case 32: // spacebar
+			case 34: // page down
+			case 39: // rightkey
+			case 40: // downkey
+				if(number != undef) {
+					go(number);
+				} else if (!incrementals[snum] || incpos >= incrementals[snum].length) {
+					go(1);
+				} else {
+					subgo(1);
+				}
+				break;
+			case 33: // page up
+			case 37: // leftkey
+			case 38: // upkey
+				if(number != undef) {
+					go(-1 * number);
+				} else if (!incrementals[snum] || incpos <= 0) {
+					go(-1);
+				} else {
+					subgo(-1);
+				}
+				break;
+			case 36: // home
+				goTo(0);
+				break;
+			case 35: // end
+				goTo(smax-1);
+				break;
+			case 67: // c
+				showHide('k');
+				break;
+		}
+		if (key.which < 48 || key.which > 57) {
+			number = undef;
+		} else {
+			if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return;
+			if (key.target && isParentOrSelf(key.target, 'controls')) return;
+			number = (((number != undef) ? number : 0) * 10) + (key.which - 48);
+		}
+	}
+	return false;
+}
+
+function clicker(e) {
+	number = undef;
+	var target;
+	if (window.event) {
+		target = window.event.srcElement;
+		e = window.event;
+	} else target = e.target;
+    if (target.href != null || hasValue(target.rel, 'external') || isParentOrSelf(target, 'controls') || isParentOrSelf(target,'embed') || isParentOrSelf(target, 'object')) return true;
+	if (!e.which || e.which == 1) {
+		if (!incrementals[snum] || incpos >= incrementals[snum].length) {
+			go(1);
+		} else {
+			subgo(1);
+		}
+	}
+}
+
+function findSlide(hash) {
+	var target = document.getElementById(hash);
+	if (target) {
+		for (var i = 0; i < slideIDs.length; i++) {
+			if (target.id == slideIDs[i]) return i;
+		}
+	}
+	return null;
+}
+
+function slideJump() {
+	if (window.location.hash == null || window.location.hash == '') {
+		currentSlide();
+		return;
+	}
+	if (window.location.hash == null) return;
+	var dest = null;
+	dest = findSlide(window.location.hash.slice(1));
+	if (dest == null) {
+		dest = 0;
+	}
+	go(dest - snum);
+}
+
+function fixLinks() {
+	var thisUri = window.location.href;
+	thisUri = thisUri.slice(0, thisUri.length - window.location.hash.length);
+	var aelements = document.getElementsByTagName('A');
+	for (var i = 0; i < aelements.length; i++) {
+		var a = aelements[i].href;
+		var slideID = a.match('\#.+');
+		if ((slideID) && (slideID[0].slice(0,1) == '#')) {
+			var dest = findSlide(slideID[0].slice(1));
+			if (dest != null) {
+				if (aelements[i].addEventListener) {
+					aelements[i].addEventListener("click", new Function("e",
+						"if (document.getElementById('slideProj').disabled) return;" +
+						"go("+dest+" - snum); " +
+						"if (e.preventDefault) e.preventDefault();"), true);
+				} else if (aelements[i].attachEvent) {
+					aelements[i].attachEvent("onclick", new Function("",
+						"if (document.getElementById('slideProj').disabled) return;" +
+						"go("+dest+" - snum); " +
+						"event.returnValue = false;"));
+				}
+			}
+		}
+	}
+}
+
+function externalLinks() {
+	if (!document.getElementsByTagName) return;
+	var anchors = document.getElementsByTagName('a');
+	for (var i=0; i<anchors.length; i++) {
+		var anchor = anchors[i];
+		if (anchor.getAttribute('href') && hasValue(anchor.rel, 'external')) {
+			anchor.target = '_blank';
+			addClass(anchor,'external');
+		}
+	}
+}
+
+function createControls() {
+	var controlsDiv = document.getElementById("controls");
+	if (!controlsDiv) return;
+	var hider = ' onmouseover="showHide(\'s\');" onmouseout="showHide(\'h\');"';
+	var hideDiv, hideList = '';
+	if (controlVis == 'hidden') {
+		hideDiv = hider;
+	} else {
+		hideList = hider;
+	}
+	controlsDiv.innerHTML = '<form action="#" id="controlForm"' + hideDiv + '>' +
+	'<div id="navLinks">' +
+	'<a accesskey="t" id="toggle" href="javascript:toggle();">&#216;<\/a>' +
+	'<a accesskey="z" id="prev" href="javascript:go(-1);">&laquo;<\/a>' +
+	'<a accesskey="x" id="next" href="javascript:go(1);">&raquo;<\/a>' +
+	'<div id="navList"' + hideList + '><select id="jumplist" onchange="go(\'j\');"><\/select><\/div>' +
+	'<\/div><\/form>';
+	if (controlVis == 'hidden') {
+		var hidden = document.getElementById('navLinks');
+	} else {
+		var hidden = document.getElementById('jumplist');
+	}
+	addClass(hidden,'hideme');
+}
+
+function fontScale() {  // causes layout problems in FireFox that get fixed if browser's Reload is used; same may be true of other Gecko-based browsers
+	if (!s5mode) return false;
+	var vScale = 22;  // both yield 32 (after rounding) at 1024x768
+	var hScale = 32;  // perhaps should auto-calculate based on theme's declared value?
+	if (window.innerHeight) {
+		var vSize = window.innerHeight;
+		var hSize = window.innerWidth;
+	} else if (document.documentElement.clientHeight) {
+		var vSize = document.documentElement.clientHeight;
+		var hSize = document.documentElement.clientWidth;
+	} else if (document.body.clientHeight) {
+		var vSize = document.body.clientHeight;
+		var hSize = document.body.clientWidth;
+	} else {
+		var vSize = 700;  // assuming 1024x768, minus chrome and such
+		var hSize = 1024; // these do not account for kiosk mode or Opera Show
+	}
+	var newSize = Math.min(Math.round(vSize/vScale),Math.round(hSize/hScale));
+	fontSize(newSize + 'px');
+	if (isGe) {  // hack to counter incremental reflow bugs
+		var obj = document.getElementsByTagName('body')[0];
+		obj.style.display = 'none';
+		obj.style.display = 'block';
+	}
+}
+
+function fontSize(value) {
+	if (!(s5ss = document.getElementById('s5ss'))) {
+		if (!isIE) {
+			document.getElementsByTagName('head')[0].appendChild(s5ss = document.createElement('style'));
+			s5ss.setAttribute('media','screen, projection');
+			s5ss.setAttribute('id','s5ss');
+		} else {
+			document.createStyleSheet();
+			document.s5ss = document.styleSheets[document.styleSheets.length - 1];
+		}
+	}
+	if (!isIE) {
+		while (s5ss.lastChild) s5ss.removeChild(s5ss.lastChild);
+		s5ss.appendChild(document.createTextNode('body {font-size: ' + value + ' !important;}'));
+	} else {
+		document.s5ss.addRule('body','font-size: ' + value + ' !important;');
+	}
+}
+
+function notOperaFix() {
+	slideCSS = document.getElementById('slideProj').href;
+	var slides = document.getElementById('slideProj');
+	var outline = document.getElementById('outlineStyle');
+	slides.setAttribute('media','screen');
+	outline.disabled = true;
+	if (isGe) {
+		slides.setAttribute('href','null');   // Gecko fix
+		slides.setAttribute('href',slideCSS); // Gecko fix
+	}
+	if (isIE && document.styleSheets && document.styleSheets[0]) {
+		document.styleSheets[0].addRule('img', 'behavior: url(ui/default/iepngfix.htc)');
+		document.styleSheets[0].addRule('div', 'behavior: url(ui/default/iepngfix.htc)');
+		document.styleSheets[0].addRule('.slide', 'behavior: url(ui/default/iepngfix.htc)');
+	}
+}
+
+function getIncrementals(obj) {
+	var incrementals = new Array();
+	if (!obj)
+		return incrementals;
+	var children = obj.childNodes;
+	for (var i = 0; i < children.length; i++) {
+		var child = children[i];
+		if (hasClass(child, 'incremental')) {
+			if (child.nodeName == 'OL' || child.nodeName == 'UL') {
+				removeClass(child, 'incremental');
+				for (var j = 0; j < child.childNodes.length; j++) {
+					if (child.childNodes[j].nodeType == 1) {
+						addClass(child.childNodes[j], 'incremental');
+					}
+				}
+			} else {
+				incrementals[incrementals.length] = child;
+				removeClass(child,'incremental');
+			}
+		}
+		if (hasClass(child, 'show-first')) {
+			if (child.nodeName == 'OL' || child.nodeName == 'UL') {
+				removeClass(child, 'show-first');
+				if (child.childNodes[isGe].nodeType == 1) {
+					removeClass(child.childNodes[isGe], 'incremental');
+				}
+			} else {
+				incrementals[incrementals.length] = child;
+			}
+		}
+		incrementals = incrementals.concat(getIncrementals(child));
+	}
+	return incrementals;
+}
+
+function createIncrementals() {
+	var incrementals = new Array();
+	for (var i = 0; i < smax; i++) {
+		incrementals[i] = getIncrementals(document.getElementById(slideIDs[i]));
+	}
+	return incrementals;
+}
+
+function defaultCheck() {
+	var allMetas = document.getElementsByTagName('meta');
+	for (var i = 0; i< allMetas.length; i++) {
+		if (allMetas[i].name == 'defaultView') {
+			defaultView = allMetas[i].content;
+		}
+		if (allMetas[i].name == 'controlVis') {
+			controlVis = allMetas[i].content;
+		}
+	}
+}
+
+// Key trap fix, new function body for trap()
+function trap(e) {
+	if (!e) {
+		e = event;
+		e.which = e.keyCode;
+	}
+	try {
+		modifierKey = e.ctrlKey || e.altKey || e.metaKey;
+	}
+	catch(e) {
+		modifierKey = false;
+	}
+	return modifierKey || e.which == 0;
+}
+
+function startup() {
+	defaultCheck();
+	if (!isOp) createControls();
+	slideLabel();
+	fixLinks();
+	externalLinks();
+	fontScale();
+	if (!isOp) {
+		notOperaFix();
+		incrementals = createIncrementals();
+		slideJump();
+		if (defaultView == 'outline') {
+			toggle();
+		}
+		document.onkeyup = keys;
+		document.onkeypress = trap;
+		document.onclick = clicker;
+	}
+}
+
+window.onload = startup;
+window.onresize = function(){setTimeout('fontScale()', 50);}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__
new file mode 100644
index 00000000..401b621b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__
@@ -0,0 +1,2 @@
+# base theme of this theme:
+medium-white
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css
new file mode 100644
index 00000000..81df4bc1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css
@@ -0,0 +1,115 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: black; color: white;}
+:link, :visited {text-decoration: none; color: cyan;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #CCC;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #AAA;
+  font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.75em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 125% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 110% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+  width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+  border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+  background: black; color: #CCC;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #AAA;
+  font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 1em; top: 0;
+  font: bold 150% sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 125% sans-serif; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font: bold 110% sans-serif;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #FCC;}
+
+.incremental, .incremental *, .incremental *:after {
+  color: black; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: lime;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+div.sidebar {background-color: black;}
+
+pre.literal-block, pre.doctest-block {background-color: black;}
+
+tt.docutils {background-color: black;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css
new file mode 100644
index 00000000..ebb8a573
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+   Edit these if you want to change the overall slide layout.
+   The commented lines can be uncommented (and modified, if necessary)
+    to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 5;}
+.slide {top: 0; width: 92%; padding: 0.75em 4% 0 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+  z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css
new file mode 100644
index 00000000..1c9fafdf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css
@@ -0,0 +1,113 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #222;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #444;
+  font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.75em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 125% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 110% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+  width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+  border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+  background: #DDD; color: #222;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #444;
+  font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 1em; top: 0;
+  font: bold 150% sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 125% sans-serif; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font: bold 110% sans-serif;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {
+  color: white; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__
new file mode 100644
index 00000000..67f4db2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__
@@ -0,0 +1,2 @@
+# base theme of this theme:
+small-white
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css
new file mode 100644
index 00000000..5524e12e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css
@@ -0,0 +1,116 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: black; color: white;}
+:link, :visited {text-decoration: none; color: cyan;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #CCC;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #AAA;
+  font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.2em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 120% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+  width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+  border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+  background: black; color: #CCC;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #AAA;
+  font-family: sans-serif; font-weight: bold;}
+
+#slide0 {padding-top: 0em}
+#slide0 h1 {position: static; margin: 1em 0 0; padding: 0;
+  font: bold 2em sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #FCC;}
+
+.incremental, .incremental *, .incremental *:after {
+  color: black; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: lime;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 1em 0 0.5em 2em;}
+
+div.sidebar {background-color: black;}
+
+pre.literal-block, pre.doctest-block {background-color: black;}
+
+tt.docutils {background-color: black;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css
new file mode 100644
index 00000000..f6578749
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+   Edit these if you want to change the overall slide layout.
+   The commented lines can be uncommented (and modified, if necessary)
+    to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 5;}
+.slide {top: 0; width: 92%; padding: 1em 4% 0 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+  z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css
new file mode 100644
index 00000000..edf4cb5e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css
@@ -0,0 +1,114 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #222;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #444;
+  font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.2em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 120% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+  width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+  margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+  border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+  background: #DDD; color: #222;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #444;
+  font-family: sans-serif; font-weight: bold;}
+
+#slide0 {padding-top: 0em}
+#slide0 h1 {position: static; margin: 1em 0 0; padding: 0;
+  font: bold 2em sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {
+  color: white; visibility: visible; border: 0; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 1em 0 0.5em 2em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py
new file mode 100644
index 00000000..a7bad3fd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+# :Author: Günter Milde <milde@users.sf.net>
+# :Revision: $Revision: 9293 $
+# :Date: $Date: 2022-12-01 22:13:54 +0100 (Do, 01. Dez 2022) $
+# :Copyright: © 2010 Günter Milde.
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+"""
+XeLaTeX document tree Writer.
+
+A variant of Docutils' standard 'latex2e' writer producing LaTeX output
+suited for processing with the Unicode-aware TeX engines
+LuaTeX and XeTeX.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import frontend
+from docutils.writers import latex2e
+
+
+class Writer(latex2e.Writer):
+    """A writer for Unicode-aware LaTeX variants (XeTeX, LuaTeX)"""
+
+    supported = ('latex', 'tex', 'xetex', 'xelatex', 'luatex', 'lualatex')
+    """Formats this writer supports."""
+
+    default_template = 'xelatex.tex'
+    default_preamble = """\
+% Linux Libertine (free, wide coverage, not only for Linux)
+\\setmainfont{Linux Libertine O}
+\\setsansfont{Linux Biolinum O}
+\\setmonofont[HyphenChar=None,Scale=MatchLowercase]{DejaVu Sans Mono}"""
+
+    config_section = 'xetex writer'
+    config_section_dependencies = ('writers', 'latex writers')
+
+    # use a copy of the parent spec with some modifications:
+    settings_spec = frontend.filter_settings_spec(
+        latex2e.Writer.settings_spec,
+        # removed settings
+        'font_encoding',
+        # changed settings:
+        template=('Template file. Default: "%s".' % default_template,
+                  ['--template'],
+                  {'default': default_template, 'metavar': '<file>'}),
+        latex_preamble=('Customization by LaTeX code in the preamble. '
+                        'Default: select "Linux Libertine" fonts.',
+                        ['--latex-preamble'],
+                        {'default': default_preamble}),
+        )
+
+    def __init__(self):
+        latex2e.Writer.__init__(self)
+        self.settings_defaults.update({'fontencoding': ''})  # use default (TU)
+        self.translator_class = XeLaTeXTranslator
+
+
+class Babel(latex2e.Babel):
+    """Language specifics for XeTeX.
+
+    Use `polyglossia` instead of `babel` and adapt settings.
+    """
+    language_codes = latex2e.Babel.language_codes.copy()
+    # Additionally supported or differently named languages:
+    language_codes.update({
+        # code        Polyglossia-name  comment
+        'cop':        'coptic',
+        'de':         'german',         # new spelling (de_1996)
+        'de-1901':    'ogerman',        # old spelling
+        'dv':         'divehi',         # Maldivian
+        'dsb':        'lsorbian',
+        'el-polyton': 'polygreek',
+        'fa':         'farsi',
+        'grc':        'ancientgreek',
+        'ko':         'korean',
+        'hsb':        'usorbian',
+        'sh-Cyrl':    'serbian',        # Serbo-Croatian, Cyrillic script
+        'sh-Latn':    'croatian',       # Serbo-Croatian, Latin script
+        'sq':         'albanian',
+        'sr':         'serbian',        # Cyrillic script (sr-Cyrl)
+        'th':         'thai',
+        'vi':         'vietnamese',
+        # zh-Latn:    ???               # Chinese Pinyin
+        })
+    # normalize (downcase) keys
+    language_codes = {k.lower(): v for k, v in language_codes.items()}
+
+    # Languages without Polyglossia support:
+    for key in ('af',           # 'afrikaans',
+                'de-AT',        # 'naustrian',
+                'de-AT-1901',   # 'austrian',
+                # TODO: use variant=... for English variants
+                'en-CA',        # 'canadian',
+                'en-GB',        # 'british',
+                'en-NZ',        # 'newzealand',
+                'en-US',        # 'american',
+                'fr-CA',        # 'canadien',
+                'grc-ibycus',   # 'ibycus', (Greek Ibycus encoding)
+                'sr-Latn',      # 'serbian script=latin'
+                ):
+        del language_codes[key.lower()]
+
+    def __init__(self, language_code, reporter):
+        self.language_code = language_code
+        self.reporter = reporter
+        self.language = self.language_name(language_code)
+        self.otherlanguages = {}
+        self.warn_msg = 'Language "%s" not supported by Polyglossia.'
+        self.quote_index = 0
+        self.quotes = ('"', '"')
+        # language dependent configuration:
+        # double quotes are "active" in some languages (e.g. German).
+        self.literal_double_quote = '"'  # TODO: use \textquotedbl ?
+
+    def __call__(self):
+        setup = [r'\usepackage{polyglossia}',
+                 r'\setdefaultlanguage{%s}' % self.language]
+        if self.otherlanguages:
+            setup.append(r'\setotherlanguages{%s}' %
+                         ','.join(sorted(self.otherlanguages.keys())))
+        return '\n'.join(setup)
+
+
+class XeLaTeXTranslator(latex2e.LaTeXTranslator):
+    """
+    Generate code for LaTeX using Unicode fonts (XeLaTex or LuaLaTeX).
+
+    See the docstring of docutils.writers._html_base.HTMLTranslator for
+    notes on and examples of safe subclassing.
+    """
+
+    def __init__(self, document):
+        self.is_xetex = True  # typeset with XeTeX or LuaTeX engine
+        latex2e.LaTeXTranslator.__init__(self, document, Babel)
+        if self.latex_encoding == 'utf8':
+            self.requirements.pop('_inputenc', None)
+        else:
+            self.requirements['_inputenc'] = (r'\XeTeXinputencoding %s '
+                                              % self.latex_encoding)